概述
一致性协议基本都是以Paxos演进而来,zab(Zookeeper原子广播协议)也是,从设计上来看,zab和raft很类似;关于一些介绍网上也挺多的这里就不再说明,重点介绍下消息复制以及有序性一致保证。以下讲解是以源码3.8.0为基础。
消息广播
流程大致如下:
- 客户端发起一个写请求;ps:会路由到Leader服务器,只有其有写能力
- Leader 服务器将客户端请求转化为事务 proposal 提案,同时为每个 proposal 分配一个全局ID,即zxid(每次计数加1);
- Leader 服务器会为每个 Follower 分配一个FIFO的队列(LinkedBlockingQueue),Leader 将需要广播的消息依次放入到每个Follewer队列中;然后由另外的线程按FIFO策略发送消息(TCP);ps:这里通过FIFO队列以及TCP的顺序性保证了消息在Leader发送和Follower接收时的有序性
- Follower 在收到 Proposal 后,会首先将其以事物日志的方式写入到本地磁盘,写入成功后会给Leader 返回 ACK 响应;
- Leader 异步等待响应,当收到超过一半Follower的响应时,则认为消息发送成功,可以发送commit 消息;ps:这里在前一个提案提交之前,不会提交后一个提案,也保证了有序性,同一个任期zxid是连续的;
- Leader 向所有Follower 广播commit消息,并向所有observer发送inform信令,是执行这个proposal的动作;ps:这里都只是加入到发送队列,并不需要等待后续处理;另外针对观察者是直接执行,而不需要走类两阶段
- Leader自己执行已经被commit的proposal所对应的操作,并回复响应结果。
以下是5-7流程涉及的代码截图
Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦。 Leader 和 Follower 之间只需要往各自队列中发消息即可。
保证消息有序一致
- 每个提案都会加上zxid,按大小顺序发送;通过FIFO和TCP结合
- 前的提案未提交之前不会提交后一个提案,同个任期zxid是连续的
这一点与Raft的实现是不一样的,当然Raft的日志复制实现也是不一样的(省掉了一个阶段);
- Raft要求所有日志(索引、任期、命令组成)不允许有空洞,即log索引是连续的,因此有可能发送次序靠后的Log Entry先于发送次序靠前的Log Entry达到Follower,但是Raft规定Follower必须按次序接受Log Entry,就意味着即使发送次序靠后的Log Entry已经写入磁盘了(实际上不能落盘得等之前的Log Entry达到)也必须等到前面所有缺失的Log Entry达到后才能返回)
- Raft的日志都是顺序提交的,不允许乱序提交
领导者模式的局限考虑
领导者模式简化了共识的过程,像zab、raft、chubby等都是选举唯一的领导者,只有领导者有写入能力,这样集群写相当于单机的能力(甚至还不如单机,因为还需要复制日志),而且当集群规模壮大后需要同步和维护的内容更多。这里其实就可以考虑分治的思想,比如类似kafka和es中的分片设计,通过协议实现多副本同步;又比如zk考虑分多个集群等。