Zookeeper学习笔记
源码篇
将 Zookeeper 源码导入到Idea
由于 Zookeeper 是使用 Ant 进行构建的,所以若想将 Zookeeper 的源码导入 Eclipse 或 IDEA 中进行源码分析,需要首先安装 Ant 工具,然后再执行 ant eclipse 命令,会生成 Eclipse 工程,然后再导入到 Eclipse 或 IDEA 中。
下载并安装Ant
- Ant官网:http://ant.apache.org/
- 将下载的 Ant 安装 zip 包进行解压,然后在系统环境变量中添加 ANT_HOME,并将 Ant 安装目录下的 bin 目录添加到系统环境变量 Path 中。
- 在命令行窗口的任意目录下执行 ant -version 命令,可以看到版本号,则说明 Ant 安装成功。
构建Eclipse工程
- 在命令行窗口中进入到 zk 解压目录,执行 ant eclipse 命令。其会根据 build.xml 文件对当前的 Zookeeper 源码进行构建,将其构建为一个 Eclipse 工程。不过,对于该构建过程,其 JDK 最好能够满足 build.xml 文件的要求。打开 build.xml 文件,可以看到 zk 源码使用 JDK6 编写并编译的,所以最好使用该指定的 JDK,这样可以保证将来构建出的 Eclipse 工程中没有错误。
- 这里,我们使用的是 JDK8 构建出的 Eclipse 工程的相关代码并没有报错:
- 构建成功后,在 zk 源码目录中可以看到 Eclipse 工程的相关文件
导入到IDEA
- 打开 IDEA,选择导入工程,找到 zk 的源码解压目录,直接导入:
Leader选举源码
选举算法源码中的总思路
Zookeeper支持三种选举算法,但默认的是 FastLeaderElection 算法,该算法是 ZAB 协议在 Leader 选举中的工程应用,所以直接找到该类进行分析。该类中的最为重要的方法为 lookForLeader(),是选举 Leader 的核心方法。该方法大体思路可以划分为三块:
选举前的准备工作
- 选举前需要做一些准备工作,例如:创建选举对象、创建选举过程中需要用到的集合、初始化选举时限等。
将自己作为初始化 Leader 投出去
- 在当前 Server 第一次投票时会先将自己作为 Leader,然后将自己的选票广播给其它所有 Server。
验证自己的投票与大家的投票谁更适合做Leader
- 在“我选我”后,当前 Server 同样会接收到其它 Server 发送来的选票通知(Notification)。通过 while 循环,遍历所有接收到的选票通知,比较谁更适合做 Leader。若找到一个比自己更适合的 Leader,则修改自己选票,重新将新的选票广播出去。
- 最终,在对某 Server 的选票超过半数时,新的 Leader 产生。然后再做一些收尾工作,例如清空选举过程中所使用的集合,以备下次使用;再例如,生成最终的选票,以备其它 Server 来同步数据。
源码解读
- 需要注意,对源码的阅读主要包含两方面。一个是对重要类、重要成员变量、重要方法的注释的阅读;一个是对重要方法的业务逻辑的分析。
一些重要的类
- FastLeaderElection:
- QuorumCnxManager:
- ServerId 为 1 的 Server 中 QuorumCnxManager 对象维护的消息发送 Map
Key(ServerId) | Value(队列) |
---|---|
2 | 队列(存放向2号Server发送失败的消息副本) |
3 | 队列(存放向3号Server发送失败的消息副本) |
4 | 队列(存放向4号Server发送失败的消息副本) |
… | … |
* 若所有队列都为空:说明 Server1 的消息发送全部成功
* 若所有队列均不空:说明 Server1 发送给所有其它server的消息全部失败,即说明 Server1 与集群已经失联
* 若某一队列不为空:说明 Server1 与集群的连接是没有问题的,但个别Server与Server1间的连接出现了问题
- QuorumPeer:
一些重要的成员变量
- 在 FastLeaderElection 类中定义了这样一些成员变量:
- 其中还有一个重要的静态内部类 Notification:
重要方法 lookForLeader()
第一部分:选举前的准备工作
- 选举前的准备工作:记录选举开始时间,初始化两个Map用于统计Leader选举的选票和非法选票。
第二部分:毛遂自荐
- 将自己作为初始化Leader投出去
- sendNotifications()方法:
第三部分(核心):接收选票
- 比较自己的选票与接收到的其它Server的选票,谁更合适做Leader。
- 核心代码如下:
- 流程图
第四部分:统计选票
- 统计选票:判断当前选举是否可以结束了,当前的选票若在recvset中有过半的支持,则结束选举。
- 合法性校验的方法——validVoter:
// 只要被判别的主机出现在QuorumServer中,其就是合法的
private boolean validVoter(long sid) {
return self.getVotingView().containsKey(sid);
}
- 比较谁更适合做Leader的方法——totalOrderPredicate:
// 比较new选票与cur选票谁更适合做Leader,若new更适合,则返回true,否则返回false
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
LOG.debug("id: " + newId + ", proposed id: " + curId + ", zxid: 0x" +
Long.toHexString(newZxid) + ", proposed zxid: 0x" + Long.toHexString(curZxid));
// 判断new主机的权重是否为0。注意,只有Observer的权重才是0
// 即判断new主机是否是Observer
if(self.getQuorumVerifier().getWeight(newId) == 0){
return false;
}
/*
* We return true if one of the following three cases hold:
* 1- New epoch is higher
* 2- New epoch is the same as current epoch, but new zxid is higher
* 3- New epoch is the same as current epoch, new zxid is the same
* as current zxid, but server id is higher.
*/
// Leader产生的比较规则
return ((newEpoch > curEpoch) ||
((newEpoch == curEpoch) &&
((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
}
- 判断是否可以终止选举的方法——termPredicate:
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());
}
}
// 判断当前set集合中的元素数量是否过半(zk集群数量的一半)
return self.getQuorumVerifier().containsQuorum(set);
}
- 判断当前主机与集群是否失联和重连方法:
// QuorumCnxManager.java
/**
* Try to establish a connection with each server if one
* doesn't exist.
*/
public void connectAll(){
long sid;
// 连接其它每一台主机
for(Enumeration<Long> en = queueSendMap.keys();
en.hasMoreElements();){
sid = en.nextElement();
connectOne(sid);
}
}
/**
* Check if all queues are empty, indicating that all messages have been delivered.
*/
// 检测,若所有队列为空,则表明当前主机与集群没有失联,那么它的消息已经全部发送出去了
boolean haveDelivered() {
for (ArrayBlockingQueue<ByteBuffer> queue : queueSendMap.values()) {
LOG.debug("Queue size: " + queue.size());
if (queue.size() == 0) {
return true;
}
}
return false;
}
第五部分:无需选举的情况
- ① n.state == OBSERVING:
- ② n.state == FOLLOWING 或 LEADING:以下三种场景会接收到Follower或Leader发送来的通知
- 场景一:当有新的主机(非Observer)要加入一个正常运行的集群时,其初始状态为LOOKING,其会调用lookForLeader(),然后向所有集群中的主机发送通知。当Leader、Follower接收到该主机所发送的通知后,它们就会向其回复通知,此时的通知状态就是FOLLOWING或LEADING。
- 场景二:当Leader挂了,并不是所有Follower同时感知到的,会有个先后顺序。对于先感知到Leader挂了的主机,其状态马上会变为LOOKING,然后向其他主机发送通知。其它主机由于还没有感知到Leader已经挂了,所以它们的状态仍为FOLLOWING。所以它们在向这台主机回复的通知状态为FOLLOWING。
- 场景三:在本轮选举中已经选举出了新的Leader,但并不是所有participant同时都知道的,也会有个先后顺序。新选举出的Leader的状态为LEADING,知道Leader选举出来的主机的状态为Following,但还不知道的主机状态仍为 LOOKING。此时的主机在向其它主机发送过通知后,接收到的通知就有LEADING、FOLLOWING状态的通知。
- 代码分析:
客户端连接Server源码解析
创建Client对象-zkClient
- 分析入口:
public class ZKClientTest {
public static void main(String[] args){
// 创建 zkclient
ZkClient zkClient = new ZkClient(Constants.ZK_CLUSTER_SERVER);
// ...
}
}
- 连接 zk 集群:
启动Client-Curator
- 分析入口:
- 从start()跟进去:
- 然后再继续跟踪 reset()方法:
创建zk对象
- 客户端连接请求处理线程:
- 获取连接地址:
- 进行连接:
- 获取NIO的channel,并完成注册和连接
小结
- 1、创建一个zk集群字符串解析器,将解析出的zk server地址写入到了一个列表
- 2、创建一个地址解析器并将解析出的地址进行一次shuffle
- 3、从第一次shuffle的server地址列表中轮询选出一个server地址进行连接
- 3.1、获取指定主机名对应的所有 由ip构成的地址
- 3.2、如果地址直接就是由ip构成的情况,直接返回该ip构成的地址即可
- 3.3、否则,对一个主机名获取到的所有由ip构成的地址再进行一次shuffle
- 3.4、对shuffle过的结果,直接取第一个地址
会话连接超时——客户端维护
- 连接 zk 服务器后,更新连接请求的发送时间和心跳时间:
- 判断连接状态,并处理:
会话空闲超时管理——服务端维护
分桶策略
- 分桶策略是指,将空闲超时时间相近的会话放到同一个桶中来进行管理,以减少管理的复杂度。在检查超时时,只需要检查桶中剩下的会话即可,因为没有超时的会话已经被移出了桶,而桶中存在的会话就是超时的会话。
- zk 对于会话空闲的超时管理并非是精确的管理,即并非是一超时马上就执行相关的超 时操作。
- 分桶依据:一个桶的大小为 ExpirationInterval 时间。只要 ExpirationTime 落入到同一个桶中,系统就会对其中的会话超时进行统一管理。
ExpirationTime = CurrentTime + SessionTimeout;
BucketTime = (ExpirationTime/ExpirationInterval + 1) * ExpirationInterval
创建sessionTracker源码解析
- 分析入口:ZooKeeperServer#startup
/**
* 该方法在 zk 启动时会被调用
*/
public synchronized void startup() {
if (sessionTracker == null) {
// 创建一个会话跟踪器
createSessionTracker();
}
// 启动会话跟踪器线程
startSessionTracker();
setupRequestProcessors();
registerJMX();
setState(State.RUNNING);
notifyAll();
}
- 跟下方法createSessionTracker:
- 进入SessionTrackerImpl:
- addSession()方法:
- touchSession()方法:
处理客户端连接请求
- 分析入口:ZooKeeperServer#processConnectRequest
- 重新打开会话reopenSession:
- 创建新的会话:createSession
处理客户端读写请求
- 分析入口:ZooKeeperServer#processPacket
- 提交会话请求:
- 这里小结下touchSession()调用关系:这个方法比较重要
sessionTracker线程的启动
- 分析入口:
- startSessionTracker()方法: