zk代码不多,简短清晰,读起来朗朗上口。
1 先看流程
(1)再怎么说,zk也就是个main函数,从QuorumPeerMain里启动,执行initializeAndRun方法,首先启动个DatadirCleanupManager线程,用来定期处理事务日志文件和数据快照。然后创建一个面向客户端的ServerCnxnFactory,默认端口2181,这个现在先不看后面说。
(2) 核心:quorumPeer的启动~~~~
quorumPeer = getQuorumPeer();
......
//选举类型,现在只有一种快速选举
quorumPeer.setElectionType(config.getElectionAlg());
....
//设置ZKDatabase,zk的内存数据库
quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
...
// -------------初始化当前zk服务节点的配置---------------
quorumPeer.initialize();
//重点是qurumpeer的启动
quorumPeer.start();
quorumPeer.join();
创建QuorumPeer,并根据配置为属性赋值。代表了zk节点,是集群模式下特有的对象,是Zookeeper服务器实例的托管者,在运行期间它会不断检查当前服务器实例运行的状态。然后根据情况进行Leader选举。start()其实就是个while方法,根据节点状态做不同事情,核心逻辑伪代码如下:
while (running) {
switch (getPeerState()) {
case LOOKING:
//指定leader选举算法-快速选举,初始化QuorumCnxManager
startLeaderElection();
lookForLeader();
case OBSERVING:
observer.observeLeader();
case FOLLOWING:
follower.followLeader();
case LEADING:
leader.lead();
(3)一开始都是looking状态,创建quorumCnxManager组件,这是zk的集群通信组件,负责收发请求的。两个重要变量,
//SendWorker封装了Socket的发送器,senderWorkerMap用来记录其他服务器id以及对应的SendWorker,sendWorker里面也包含receiveworker
final ConcurrentHashMap senderWorkerMap;
//zkServer需要发送给其他服务器选票信息
final ConcurrentHashMap> queueSendMap;
// 从其他服务器接收到的投票信息
public final ArrayBlockingQueue recvQueue;
这里说明,这个server为每个server都准备了个sendworker负责网络收发,同时自己也准备了个发票桶和收票桶。
(4)然后启动快速选举组件FastLeaderElection,包含两个线程,发请求线程workSender和收请求线程WorkerReceiver,分别操作发送和接受队列。是具有独立队列和处理逻辑的组件,和QuorumCnxManager的队列和处理区分开,这个思路值得学习。然后就是FastLeaderElection的lookForLeader选举过程。对应上面图的3.0后面。
(5)顺着lookForLeader往下看,梳理出这么一条逻辑
3.0: while循环从FastLeaderElection的接收队列里拿选票信息,跳出条件是选举结束。
3.1: 一开始接受队列当然是空的,拿不到选票信息。connectall向所有节点发起连接,
3.1.2~3.1.5: 连接建立,生成serverid对应的收发线程,存到quorumCnxManager的map中,以后这个线程就专门给这个serverid用。如果当前server收到了别人的连接,也会生成这个收发线程。需要注意的是,为了避免两边连接重复建立,如果目标机器sid比你大,就会关掉连接,不允许小id向大id建立连接。连接建立后生成这个sid专有的sendWorker负责收发请求。
3.2~3.3: 连接建立后,通过sendNotification,把选票发到FastLeaderElection的发送队列。选票一开始都是投自己。
3.4~3.5: FastLeaderElection的发送线程WorkSender把自己的发送队列翻译到QuorumCnxManager的发票箱queueSendMap,
3.6~3.8: 由QuorumCnxManager的发送线程发到其他server。
4.1~4.3: 其他服务发来选票,通过QuorumCnxManager的接受线程,拿到选票桶,然后FastLeaderElection的接受线程把选票翻译到自己的接收队列。
4.5~4.7: 一开始while循环拿到了选票,和自己的选票进行pk,得到最优选票,然后重复3.2的过程,归档所有收到的选票,通过选主规则n/2+1判断是否已投出leader,如果已满足leader条件,就进入case Leader分之,进行lead()方法。
2 zk的选举过程
(1)上面分析到,server不断收到选票,然后进行计算,伪代码如下
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){
// 从recvqueue中获取接收到的投票信息
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
if (获得的投票为空) {
// 连接其他服务器
} else {
// 处理投票
}
}
(2)PK选票 进行周期,事务id大小,serverid大小的判断
if (接收到的投票的选举周期 > 本服务器当前的选举周期) {
// 修改本服务器的选举周期为接收到的投票的选举周期
// 清空本服务器的投票箱(表示选举周期落后,重新开始投票)
// 比较接收到的选票所选择的服务器与本服务器的数据谁更新,本服务器将选票投给数据较新者
// 发送选票
} else if(接收到的投票的选举周期 < 本服务器当前的选举周期){
// 接收到的投票的选举周期落后了,本服务器直接忽略此投票
} else if(选举周期一致) {
// 比较接收到的选票所选择的服务器与本服务器当前所选择的服务器的数据谁更新,本服务器将选票投给数据较新者
// 发送选票
}
(3)过半机制验证
本服务器的选票经过不停的PK会将票投给数据更新的服务器,PK完后,将接收到的选票以及本服务器自己所投的选票放入投票箱中,然后从投票箱中统计出与本服务器当前所投服务器一致的选票数量,判断该选票数量是否超过集群中所有跟随者的一半(选票数量 > 跟随者数量/2),如果满足这个过半机制就选出了一个准Leader。
(4)最终确认
选出准Leader之后,再去获取其他服务器的选票,如果获取到的选票所代表的服务器的数据比准Leader更新,则准Leader卸职,继续选举。如果没有准Leader更新,则继续获取投票,直到没有获取到选票,则选出了最终的Leader。Leader确定后,其他服务器的角色也确定好了。
只有当集群中服务器的角色确定了之后,while才会进行下一次循环,当进入下一次循环后,就会根据服务器的角色进入到对应的初始化逻辑,初始化完成之后才能对外提供服务。