Paxos算法理论与应用
引言
Paxos算法是著名的分布式强一致性算法。它是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
1 原理
Paxos算法解决各个节点如何就某个值或操作达成一致,这个过程称为决议。有三种角色参与决议,即Proposer、Acceptor和Learner。Proposer是决议的提出者;Acceptor是决议的投票者;Learner是决议的记录者。只有过半的Acceptor通过的决议才有效,三个角色都可以是多个节点。
1.1 Basic Paxos算法
Basic Paxos算法称为原始Paxos算法,它是其他Paxos算法变种的基础。总共分为三个阶段,如下图:
1.1.1 算法过程
- prepare阶段
注1:reqN是Proposer每次prepare决议的请求序号,MaxN是所有的Proposer对该Acceptor节点prepare后的最大请求序号,AcceptN和AcceptN是Acceptor在accept阶段成功接收的请求序号和决议;
注2:算法保证同一个请求序号reqN只能有一个Proposer节点能prepare成功,且只能成功prepare一次(即下次prepare同样的序号就会失败)。
- accept阶段
注1:reqN是prepare阶段的reqN。reqV只能在prepare所有返回均是success(null,null)时才可以由Proposer自定义;否则,只能取最大的AcceptN对应的AcceptV值,保证已经过半的决议不被篡改(之前一个Proposer发起的、且只被少数Acceptor通过的决议也极有可能被另一个Proposer再次发起);
注2:算法保证较大的AcceptN值对应的AcceptV是较后发起的决议;
注3:算法保证同一个AcceptN值只能被一个Proposer节点在所有Acceptor节点成功设置一次,即prepare阶段返回值中相同的AcceptN对应的AcceptV相同;
- learn阶段
注:记录/执行(reqV),根据业务可以是添加一个确认状态等相关操作;
- 总结
在整个过程的prepare和accept阶段,若prepare或accept未过半是不能退出的。否则当前Proposer期望发起的决议是否已通过、是否可能被下次重新发起、是否可能被其他Proposer重新发起,都是未知的。
下述情况之一,可以判定Proposer期望发起的决议没有被通过,且不会被下次重新发起、也不会被其他Proposer重新发起:
1 查询已决议数据,有过半acceptN大于当前reqN、且这些acceptN对应的acceptV与当前Proposer期望发起的决议不等;
2 如果全部Acceptor返回的acceptV都不等于当前Proposer期望发起的决议;
1.1.2 美中不足
通过对Basic Paxos算法的介绍。首先,多个决议V无法同时执行;此外,当多个Proposer时,会出现死循环。假设两个Proposer,即PA和PB。按照如下顺序:
- PA请求reqN=K的prepare,获得半数以上Acceptor通过。此时MaxN=K;
- PB请求reqN=K+1的prepare,获得半数以上的Acceptor通过。此时MaxN=K+1;
- PA提交reqN=K的accept,发现MaxN=K+1而失败。又重新请求reqN=K+2的prepare,此时MaxN=K+2;
- PB提交reqN=K+1的accept,发现MaxN=K+2而失败。又重新请求reqN=K+3的prepare,此时MaxN=K+3;
- 以此,循环下去。。。
解决上面的办法,可以设置一个中心Proposer进行统一提交;也可以Acceptor对每个Proposer节点每连续prepare次数达到上限M就停止一定时间段的prepare响应(但可以继续响应accept)。再假设有两个Acceptor(A1和A2),后者也会产生如下的问题:
- A1收到了PA请求reqN=K的prepare,设置MaxN=K;
- A2收到了PB请求reqN=K的prepare,设置MaxN=K;
- A1收到了PB请求reqN=K的prepare,此时因不大于MaxN而失败;
- A2收到了PA请求reqN=K的prepare,此时因不大于MaxN而失败;
- 以此,A1、A2分别对PA、PB响应间隔一会儿后又开始死循环。
解决这个办法,可以让Acceptor对暂停响应的Proposer节点按mac地址大小优先级逐步放行。
注:此外,prepare阶段返回success时,一定要带AcceptN;accept阶段也一定要记录AcceptN。
1.2 Multi Paxos算法
1.2.1 算法思想
为了解决Basic Paxos算法的美中不足,提出了Multi Paxos算法。该算法的思想如下:
- 所有的Proposer节点通过Basic Paxos算法选举一个leader节点。由leader节点统一调度Multi Paxos算法;
- leader节点调度Multi Paxos算法时,忽略prepare过程,将三阶段变为两阶段;
- 不同的决议分配不同的事务标识符(即transID),accept和learn阶段的sendToMust方法都需要带transID参数,让Acceptor/Learner可以区分不同的决议。
1.2.2 再度升华
Multi Paxos算法是多个决议在一个leader上跑,如果能分散在各个Proposer节点就可以做到负载分散。于是在一个决议开始前,由leader分配一个Proposer节点作为决议执行节点,保证同一个transID分配到固定的Proposer节点执行。
2 应用
2.1 无中心多节点备份
2.1.1 数据操作算法
首先在所有数据节点都有操作过程表,即proc_records。包含字段trans_num(操作序号,为主键且连续递增,不能为null)、max_tick(竞选序号,即算法中的ResN,不能为null)、accept_tick(已通过序号,即算法中的AcceptN,默认值null)、opt_cid(操作唯一标示符,默认值null)、opt_cmd(操作命令,默认值null)、sync_state(同步状态,有unsync和synced两个状态且默认值为unsync)、sync_img(同步镜像,用于后续节点加入时获取镜像,默认值null)。当某个节点需要增删改操作时调用方法proc(new_cid, new_cmd),该方法循环执行:
- 在state_records表的sync_state==synced的数据中,获取最大的trans_num加1作为prepare函数的num参数、以当前时间戳作为tick参数、以new_cid作为cid参数、以new_cmd作为cmd参数;
- 待第1步执行完毕,在state_records表中查询opt_cid=new_cid且sync_state=synced的数据。若能找到,就退出循环;否则,若捕获了NullCmdParamExcetion也退出循环、反之,返回第1步继续执行;
整个算法的核心思想就是通过paxos算法实现操作串行化,总共有三类节点,caller是访问数据集群的外部节点、OptNode是接收Caller调用的集群节点之一、OtherNode是其他集群节点。具体如下:
- 操作发起阶段
注:数据sync_state==synced中的最大trans_num值没有变化,不可能发生。除非程序有BUG。此外,可以调用proc(null,null)同步该节点内容到最新。
- prepare阶段
注:整个算法相对于Basic Paxos多了一个num,可以理解为事务编号。此外,节点响应时多了一个节点状态不为ready的条件,该条件在节点加入/移除时防止数据丢失(accept状态同样判断了ready,也起此作用)。
- accept阶段
注:在Array<accept_tick, opt_cid, opt_cmd>全是null的添加下,如果cid或cmd有一个为null,就会层层退出、直到外面的proc退出。该机制可以通过在节点调用proc(null,null)实现将操作同步的功能。
- learn阶段
注:execute就是命令的执行过程,cmd可以是对数据库的增删改、也可以是对文件的保存、也可以是对内存的操作。
2.1.2 节点加入/移除
上述在节点稳定后运行没有问题。然而,运维过程中经常添加或移除节点,添加/移除节点时做到能正常工作、且保持操作一致性,是必不可少的。
- 所有节点从加入、工作、离开、停止,总共有三个状态,即enter、ready、leave。如上面的算法,其中只有ready状态才有机会成功响应prepare和accept请求。
- 所有ready节点都在本地的节点信息表中缓存全部ready节点信息,包括自己的节点信息也缓存在本地节点信息表中。本地节点信息表名称为node_records,包括字段node_id(节点唯一ID,为主键且不能为null)、node_info(节点信息,不能为null)、sync_max(节点最新已同步的记录序号、也就是proc_records表sync_state=synced记录中的最大trans_num值,默认null)。
注:添加/移除节点也是两种cmd类型,执行execute(cmd)就是执行insert_or_update或删除node_records中相应节点。特别地,当执行删除node_records中节点是自己这个节点时,节点状态变为leave。
- 节点加入
注:节点加入需要在一个ready节点运行添加节点的命令,然后返回操作序号;再通过序号拉取当前img;依次执行。。。这个过程是必不可少的,为数据一致性提供保障。新节点拉取img的时候,第一个参数existTransNum为null;一个stoped的节点再次加入的时候,existTransNum就是当前sync_state=synced的数据中的最大trans_num值。
- 节点移除
注1:上述过程中,并没有与被移除节点的交互。原因如下:
如果被移除节点异常宕机,那么自然无法与之交互;反之,若正常移除,被移除节点当同步到自己被移除操作时会自动变为leave状态。
注2:此外,如果不把剩下的ready节点全部同步到最新,那么就会出现数据无法同步的现象。例如:
当前有节点1、2、3,且所有的accept阶段刚好只有2、3号成功accept;现在移除2号节点(3号为OneReadyNode)、添加4号节点(3号为OneReadyNode)、移除3号节点(4号为OneReadyNode),那么后续1号节点因为访问不到2、3号节点,就无法同步2号节点移除之前的状态。特别地,当在下次节点加入时执行所有节点同步、多次移除节点累计到一定值(必须小于一半),再执行所有节点同步也是可以的;但是这给运维带来了失误的风险!
2.1.3 运维事项
- 节点状态图
注:节点的状态图如上,正常的流程是按照上图所示转换。当然,异常操作除外。
- 节点加入
1 第一个节点加入时,notifyEnter的参数readyNode为null;
2 返回超时或error,未必表示没有成功加入。需要通过运维工具查看参数readyNode这个节点的node_records表是否有该节点;也可以不用查看、而是直接断开EnterNode的网络,然后执行移除EnterNode操作;
3 ManagerNode可以是一个单独的节点、也可以运行在要加入的节点上、甚至还可以运行在ready节点上。
- 节点移除
1 在选定的ReadyNode执行完移除操作后,最后必须让全部的ready节点实现数据同步;
2 节点每次最多只能移除不超过一半的节点,否则会造成数据丢失的风险。
- 数据同步
1 假如有若干个节点组成集群,刚好一直都是少部分节点在操作数据。那么日积月累,造成大部分节点未同步的数据太多。当这些节点被访问时,再同步数据就会特耗时。
2 当所有节点的最新同步数据对应的trans_num>10000,那么理论上proc_records中<=10000的数据是可以删除的。
为了解决上述问题,通过配置实现定时同步(此外,也可以在ready节点learn阶段,发出同步组播到其他节点)。同步过程如下:
注:首先调用proc传入null同步数据到最新。然后执行设置节点最新synced的trans_num操作;当cmd被learn阶段execute的时候,它会将本地node_records中当前节点对应的sync_max进行修改;当其他节点同步到这条操作时,也会在它们本地node_records中设置该节点的sync_max;每当node_records中节点的sync_max值发生变化的时候,相关节点就会删除本地proc_records中trans_num<min(sync_max)的记录。