分布式系统设计中,几乎都需要在服务可用性性 和数据一致性之间做权衡,也因此诞生了一系列的一致性协议,常见的有2PC一致性协议、3PC一致性协议、Paxos一致性算法、Raft一致性算法。
2PC,也叫做二阶段提交协议,是指将在分布式环境下多个分布节点的事务提交过程分成两个阶段来进行处理:
第一个阶段是事务执行,即多个节点先提交,但是不commit事务;
第二个阶段是事务提交,即协调者统计多个节点第一个阶段是否都执行成功,都执行成功再让多个节点commit事务。
(1)阶段一:执行事务
(2)阶段二:执行事务提交
以电商购买商品为例,减库存和扣减账户金额需要保证事务一致性。正常情况下阶段一、阶段二操作如下:
(3)事务中断
如果在阶段一或阶段二任一节点协调者与参与者失去联系,都要进行事务中断操作。
若在第2个阶段中,协调者发现有节点返回"NO"或者没有接收到所有参与者节点返回的"YES"响应,就会发起事务中断操作。
2PC优缺点:
优点:原理简单
缺点:
3PC,三阶段提交,在2PC的基础上优化了阶段一执行事务操作过程,最终形成CanCommit、Precommit和do Commit 三个阶段组成的事务处理协议。
三阶段协议看起来与二阶段似乎并无太大差别,但是作为2PC的改进版,3PC与2PC的区别在于:
(1)第一个阶段是不阻塞的,降低了参与者阻塞范围;
(2)3PC对于参与者设置了超时机制(2PC只有协调者有):2PC在第二阶段协调者节点发送Commit
操作时,如果协调者节点发生单点故障问题,会导致部分参与者节点接收并执行,部分参与者节点无法接收到不会执行,出现分布式节点数据不一致的问题;而对应的在3PC第三阶段,协调者节点发完doCOmmit
操作发生单点故障,部分节点接收、部分节点未接收,为了防止出现数据不一致的问题,在第三节点3PC对参与者节点加上了等待超时的概念,如果部分节点在指定时间内未接收到协调者发送的doCOmmit
操作后,会自动进行事务提交,最终保证所有节点数据一致性;
注:如果3PC第三阶段发送的是中断事务
指令,部分节点接收到进行事务回滚,部分节点未接收到等待超时后依然执行doCommit
操作,就出现数据不一致情况;
三阶段优缺点
优点:降低参与者的阻塞范围,并且能够在出现单点故障后继续达成一致;
缺点:参与者接收到preCommit消息后,如果此时与协调者发生网络分区,该参与者依然会进行事务的提交。会出现数据的不一致性。
Paxos算法是Leslie Lamport 与1990年提出的基于消息传递且具有高度容错性的一致性算法,用于解决在一个可能发生异常的分布式系统中,如何集群中快速在对某个值达成一致的算法。
在Paxos算法中,有3种角色:Proposer(提议者)、Acceptor(接受者)、Learner(学习者):
Proposer(提议者):提案发起者;
Acceptor(接受者):提案决策者,对每个提议的值进行投票,并存储接受的值;
Learner(学习者):最终决策的学习者,被告知投票的结果,接受达成共识的值进行保存,一般用于数据备份。
关于以上3个角色,我们可以这样形象地去比喻:公司某个广告方案设计会议,Proposer好比公司中多个设计人员就某个广告提出的不同设计方案,而Acceptor好比CEO,去决定选择哪个方案(一般怕CEO突然不来会议或者挂了,会选择多个副的CEO来共同投票选择方案);而Learner就像普通员工,在Acceptor宣布选择哪个方案后,去告知Learner投票结果。
在实际开发场景中,Proposer代表接收到客户端请求,发起二阶段提交,请求对某个值达成一致;Acceptor代表投票选择对提议的值进行投票达成共识的值;学习者不参与共识协商,只接受达成共识的值进行存储保存。一个节点既可以是Proposer也可以是Acceptor。
提案生成
Paxos算法中提案生成主要分为两个阶段(二阶段提交):
(1)阶段一:Proposer生成提案
(2)阶段二:Acceptor接受提案
整个过程如下:
下面举一个多个节点就某个值达成一致的过程熟悉Paxos算法。假设节点Node4、Node5、Node6中保存有某件商品数量;客户端Node1、Node2同时发起请求修改该商品数量,假设Node1请求中需要将该商品数量修改为v1;Node2请求中需要将该商品数量修改为v2。
(1)Proposer生成提案:节点Node1、Node分别生成编号为1001、1002的唯一值作为提案。
首先Node4、5先收到客户端Node1的请求[1001, ],由于Node4、5之前并未批准任何提案,所以响应给Node1:【Prepare响应:尚未提案】,同时承诺不再响应编号 <= 1002 的提案,不会批准小于 < 1002的提案;;
之后再收到客户端Node2的请求[1002, ],由于Node4、5之前并未批准任何提案,所以也响应给Node2:【Prepare响应:尚未提案】,同时承诺不再响应编号 <= 1002 的提案,不会批准小于 < 1002的提案;
Node6先收到客户端Node2的请求[1002, ],由于Node6之前并未批准任何提案,所以响应给Node2:【Prepare响应:尚未提案】,并同时承诺不再响应编号 <= 1002 的提案,不会批准小于 < 1002的提案;
(2)Acceptor接受提案:
客户端Node1收到半数接受者的响应后,由于没有指定提案值,所以将自己将要修改的值设置为提案的值:[1001, v1],发起accept请求;同样Node2也发起提案为[1002, v2]的Accept请求;
Node4、5、6接收到客户端的Accept请求:[1001, v1], 由于承诺不再批准比1002小的提案,所以抛弃;
接着接收到客户端Node2的Accept请求:[1002, v2], 由于提案1002不比之前承诺的1002小,所以3个节点都通过该提案,将该商品数量改为v2。这样3个节点就达成了一致。
提案获取
当Acceptors选定提案后,Learner需要获取提案。
所有的Acceptors将批准的提案发送特定的Learner,为避免出现单点故障问题,Learner一般设置多个主Learner集群,由主Learner将同步到的结果再同步给其他Learner。
Raft算法是目前分布式系统开发选用较多的共识算法,它在Paxos算法的基础上做了简化和限制。
Raft算法的本质是通过领导者为准的方式来实现值的共识和日志的一致。
Raft算法中有3种基本角色:
Raft算法主要有两个过程:Leader选举和日志复制。
(1)Server启动时,各个节点初始状态均为Follower,每个节点会给自己设置一个选举超时时间(一般为150~300ms);
(2)先如果在各自设置的超时时间内收到来自Leader或Candidate的心跳消息,则定时器重启;否则会发起一次选举过程,向其他服务器发出选举自己的请求(先超时的节点先发起投票);
如图,节点A因为超时时间最短,最先因没有收到心跳消息而向其他节点发起投票请求,请求他们选举自己为领导人(增加自己的任期,并推荐自己为候选人)。
(3)其他节点(C、D)在编号为1的任期内未投票,则将选票投给节点A,并增加自己的任期编号;
(4)即使节点B宕机,只要节点A收到半数以上选票(3票)则当选为Leader,并不断发生心跳消息给其他节点,防止其他节点篡权再次发起投票。
关于Leader选举有如下注意点:
Raft算法中,Leader节点收到来自客户端请求后,会将处理过程中以复制和应用日志项到状态机中。为了实现数据一致性,会将日志同步到跟随者节点中,副本数据是以日志的形式存在。Raft中日志必须是连续的。
Raft算法中日志项是一种数据格式,包括指令、索引值、任期编号等,如下:
(图片来源:https://time.geekbang.org/column/article/205784)
指令:客户端请求指定的数据;
索引值:日志项对应的索引值,连续、单调递增,用来标识日志项;
任期编号:创建日志项的领导者的任期编号;
那么领导者是如何将日志项复制到别的客户端上呢?
注意:如果在日志复制过程中,领导者会通过日志复制RPC一致性检查,找到跟随者节点上与自己相同日志项的最大索引值,然后通过复制并更新覆盖索引值之后的日志项,实现节点间日志的一致。