zab协议
zk集群中有一个leader负责处理所有客户端的请求, 通过广播的的形式将事物传播到各个follower节点形成副本,每当follower接到事物后会以日志的形式持久化到本地,并向leader发送ack;
当leader接收到过半的follower回复ack后,leader会再次广播commit消息进行事物提交。
zab保证按照客户端事物请求的顺序执行事物提交
从client的角度看,client可以向集群中的任意一台机器发送请求,如果是写请求,就会转发到leader节点;任何节点都可以直接处理client的读请求;
在zab中,所有更新事物都会由一个64位的事物编号zxid表示, zxid用高32位和低32位代表不同的含义
high : 代表leader的epoch , 新产生一个leader后,会从该服务器上找到最大的事物Proposal的zxid, 高32位加1, 低32位清零;
low : 递增的计数器,leader接收到客户端的一个请求,在提交事物的时候会进行加1;
三个角色 leader , follower, observe
leader : 负责读,写,当有写请求时会先把消息广播给所有follower, 等待follower的ack ,如果收到一半以上的ack, leader就commit广播给所有follower
follower : 负责读, 有写请求会转发给leader , 收到leader的广播会做出ack回应
observe : 只负责读 , 引入observe的目的是 : 当集群规模大的时候,leader接收写请求后会广播给所有的其他节点, 会有很大的消耗,
observe 没有发言权, 只负责数据同步并提供读服务 , 既提供了集群的读,又不会给写带来性能消耗
选举算法 FastLeaderElection
1. 在看选举算法之前,先看几个变量和辅助方法
/***投票意向的几个变量**/
long proposedLeader; //本机投的zk id, zk配置文件中配置的每个实例一个id
long proposedZxid; //目标zk的 事物id或者叫数据id , 此id越大,证明数据越新,越有可能被选成leader
long proposedEpoch; //目标zk 事物id的高32位, 代表的leader的朝代,每换一个leader,朝代加1
long logicalclock; //本机的投票计数器, 每投一次票,该数加1, 大的表示新一轮投票,小的表示老一轮投票不参考 ,
至于为什么会出现不一致的现象是因为 网络传输可能会造成延迟等不确定因素,如果超时了,不能总等着对方
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>(); //本机统计的投票信息
updateProposal(long leader, long zxid, long epoch); //更新本地的投票意向,每进行一轮投票,本地的投票意向可能会改变
sendNotifications(); //向集群中的每个zk发送自己的投票意向 , 把自己的投票意向和投票计数器封装成对象放进队列里, 单用一个线程异步发送 , tcp
2. 选举loop
只有本机为looking状态即选举状态时进入选举loop,其他状态表示已经出结果了
while(self is looking && running){
Notification n = recvqueue.poll(notTimeout,TimeUnit.MILLISECONDS); //拿到一份别人的投票
如果对方是也是looking,执行下面操作
n is looking :
if(n.clock > self.clock) { // 自己的这轮选举已经落后了
1. recvset.clear(); //本地统计的投票过期了
2. 参考别人的信息,更新自己的投票意愿
更新条件 : 如果别人的 朝代 或者 数据id 比自己大, 表示他的数据更新, 就把对方的投票信息设置自己的投票意愿 , 否则还不如自己的数据新呢, 干脆不更新了
3.更新完自己的意愿,本地留了一份,然后在通知出去 sendNotifications();
} else if(n.clock < self.clock){ //对方的选举是过期的
//啥也不用干
}else { //这种情况就是两者的选举属于同一轮,都是有效的, 接下来就要看谁的朝代更大或数据更新
if(对方数据更新){
//更新一下本地投票意向,把自己的投票意愿通知出去
}
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); // 把对方的投票意愿缓存起来,用于最终的统计
if(少数服从多数,可以终止投票){
1. 在做一次最终的投票校验,看是否还有变化,若没有进行下面操作,否则重头开始选
2.没有变化即 别人也不在发送自己的投票信息了接下来开始判断选出来的是谁了,然后更新自己的状态
3.如果选出来的是自己,把自己设为leading状态,
否则设为following或observe (而follow和observe的区别是 一个参与投票一个不参与投票,引入observe的目的是在增大集群规模的情况下不影响 写操作时消息ack 和 commit带来的开销)
4.然后把leader的信息保存到自己本地,可以时刻知道leader是谁
}
}
如果所接收服务器不在选举状态,也就是在FOLLOWING或者LEADING状态
做以下两个判断:
a) 如果逻辑时钟相同,将该数据保存到recvset,如果所接收服务器宣称自己是leader,那么将判断是不是有半数以上的服务器选举它,如果是则设置选举状态退出选举过程
b) 否则这是一条与当前逻辑时钟不符合的消息,那么说明在另一个选举过程中已经有了选举结果,于是将该选举结果加入到outofelection集合中,再根据outofelection来判断是否可以结束选举,如果可以也是保存逻辑时钟,设置选举状态,退出选举过程
}
下面是终止选举的判断方法
/**
* 第一个参数是本地缓存的其他人的投票信息,第二个是自己的投票信息,
* 因为经过一轮交换意见,本地的投票信息已经是最新的朝代或数据id了
*
* 只要判断缓存中的其他人的信息跟自己一致,则可统计出跟自己意见一致的人数,如果是少数服从多数则可终止
*
*/
protected boolean termPredicate(HashMap<Long, Vote> votes, Vote vote) { //每经历一轮投票都要判断是否能尽快终止投票,以便尽早提供服务
HashSet<Long> set = new HashSet<Long>();
/*
* First make the views consistent. Sometimes peers will have
* different zxids for a server depending on timing.
*/
for (Map.Entry<Long,Vote> entry : votes.entrySet()) {
if (vote.equals(entry.getValue())){
set.add(entry.getKey());
}
}
return self.getQuorumVerifier().containsQuorum(set);
}