要了解zookeeper选举流程, 首先需要了解程序的入口, 那怎么知道程序的入口呢? 使用过zookeeper服务知道启动命令 zkServer.sh、zkServer.cmd, 查看zkServer.sh shell脚本, 对ZOOMAIN处理都包含QuorumPeerMain类的全路径声明, 如:
ZOOMAIN="org.apache.zookeeper.server.quorum.QuorumPeerMain"
显然QuorumPeerMain类是服务启动入口, 通过Idea查询QuorumPeerMain类, main函数包含下面的内容
// 根据指定的配置文件, 启动Server服务
public static void main(String[] args) {
QuorumPeerMain main = new QuorumPeerMain();
try {
main.initializeAndRun(args);
} catch(Exception e) {
//省略部分异常处理代码
}
LOG.info("Exiting normally");
System.exit(0);
}
Leader在zookeeper集群环境中只存在一个, 那什么时候会触发Leader选举呢 ? 主要包含下面场景
从2.1节代码可以知道, main方法内部调用了initializeAndRun方法进行服务启动, 下面是具体代码逻辑
//QuoRumPeerMain
protected void initializeAndRun(String[] args)
throws ConfigException, IOException{
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
//1. 解析配置文件, 获得配置参数
config.parse(args[0]);
}
// Start and schedule the the purge task
//2. 创建并启动线程, 用于清理日志文件
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
.getDataDir(), config.getDataLogDir(), config
.getSnapRetainCount(), config.getPurgeInterval());
purgeMgr.start();
//3. 启动服务
if (args.length == 1 && config.servers.size() > 0) {
//3.1 如果存在配置文件, 按照配置文件启动服务
runFromConfig(config);
} else {
LOG.warn("Either no config or no quorum defined in config, running "
+ " in standalone mode");
// there is only server in the quorum -- run as standalone
//3.2 如果没有给定配置文件, 按照单例模式启动, 即args=null
ZooKeeperServerMain.main(args);
}
}
服务启动如果存在配置文件, 会对配置文件进行解析, args[0] 其实就是配置文件**zoo.cfg**路径信息, 比如下面zookeeper启动命令
# zkServer.sh ../conf/zoo.cfg
initializeAndRun方法的主要逻辑是
创建配置对象QuorumPeerConfig解析配置文件 (配置解析流程比较简单, 这里不分析, 使用到的地方会提一下)
创建对象DatadirCleanupManager用于定期清理日志信息
从config中获取必要的参数: dataDir(数据目录)、dataLogDir(日志目录)、snapRetainCount(快照保留数量)、purgeInterval(清理间隔)
参数对应zoo.cfg配置信息如下
dataDir=../data
dataLogDir=../log
snapRetainCount=5
purgeInterval=3600
服务启动
生产环境肯定使用zookeeper集群方式启动服务, 也就是肯定存在配置文件信息, 因此这里只分析runFromConfig启动服务进行选举流程, ZooKeeperServerMain.main(args)按照单例的方式启动服务暂时不分析。
//QuoRumPeerMain
public void runFromConfig(QuorumPeerConfig config) throws IOException {
try {
//1. 注册log4j JMX mbeans信息, 可以通过设置zookeeper.jmx.log4j.disable=true来禁用
ManagedUtil.registerLog4jMBeans();
} catch (JMException e) {
LOG.warn("Unable to register log4j JMX control", e);
}
LOG.info("Starting quorum peer");
try {
//2. 创建ServerCnxnFactory用于服务连接
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
//3. 创建线程 QuorumPeer, 并根据config进行属性信息设置
quorumPeer = getQuorumPeer();
//... 省略从config获取配置参数, 设置quorumPeer属性的代码
quorumPeer.initialize();
//4. 启动线程 quorumPeer
quorumPeer.start();
//5. join住线程quorumPeer, 让主线程等待子线程执行完
quorumPeer.join();
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Quorum Peer interrupted", e);
}
}
从代码逻辑可以看出runFromConfig方法主要是创建QuorumPeer线程, 并进行启动。方法的主要流程如下:
注册log4j JMX mbeans信息, 用于日志输出, 可以通过设置**zookeeper.jmx.log4j.disable=true**来禁用
创建ServerCnxnFactory对象用于服务连接, 这里主要是 NIOServerCnxnFactory
创建线程 QuorumPeer, 并根据config进行属性信息设置
config.parse(args[0])
调用start方法进行服务启动, 并执行join方法保证子线程能够正常执行结束
QuorumPeer其实是一个线程, 它内部继承了Thread, 下面是类继承关系图, 因此QuorumPeer类启动其实就是线程的启动, 只是包含了部分其它逻辑
下面是QuorumPeer启动入口
//QuorumPeer
public synchronized void start() {
//1. 加载数据, 进行快照信息回复
loadDataBase();
//2. 启动服务连接线程, 其实就是NIOServerCnxnFactory内线程启动
cnxnFactory.start();
//3. 进行Leader选举
startLeaderElection();
//4. 调用父类进行线程启动
super.start();
}
从start方法看出在进行服务启动前会进行必要信息处理, 之后会调用父类start方法进行线程启动, 主要流程是
加载数据进行快照信息恢复, 这里通过 FileTxnSnapLog 进行数据加载, 主要加载dataDir、dataLogDir目录中信息, 这两个目录是在下面代码处配置
//QuoRumPeerMain#runFromConfig方法
quorumPeer.setTxnFactory(new FileTxnSnapLog(
new File(config.getDataLogDir()),
new File(config.getDataDir())));
通过 NIOServerCnxnFactory 启动服务连接线程
在第四节中创建了 cnxnFactory 对象, 并创建了 ZookeeperThread线程对象
//QuoRumPeerMain#runFromConfig
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
//NioServerCnxnFactory#configure
public void configure(InetSocketAddress addr, int maxcc) throws IOException {
configureSaslLogin();
thread = new ZooKeeperThread(this, "NIOServerCxn.Factory:" + addr);
thread.setDaemon(true);
maxClientCnxns = maxcc;
this.ss = ServerSocketChannel.open();
ss.socket().setReuseAddress(true);
LOG.info("binding to port " + addr);
ss.socket().bind(addr);
ss.configureBlocking(false);
ss.register(selector, SelectionKey.OP_ACCEPT);
}
进行服务启动, 实际就是ZookeeperThread线程的启动
public void start() {
// 线程是NEW状态, 还没有启动
if (thread.getState() == Thread.State.NEW) {
thread.start();
}
}
覆写run方法, 采用多路复用技术, 根据SelectKey来分别处理OP_ACCEPT、OP_READ、OP_WRITE
执行节点Leader选举(细节后面分析)
调用父类进行线程启动, 即调用Thread的start方法
在进入startLeaderElection方法之前, QuorumPeerMain为服务启动做了相应处理, 简要概括如下:
现在我们具体分析 startLeaderElection 方法, 首先看下代码
//QuorumPeer
synchronized public void startLeaderElection() {
try {
//1. 创建投票, 票据Vote包含myid, zxid, epoch三个主要信息
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
} catch(IOException e) {
RuntimeException re = new RuntimeException(e.getMessage());
re.setStackTrace(e.getStackTrace());
throw re;
}
//2. 从所有view中找到当前节点信息, 设置myQuorumAddr信息
for (QuorumServer p : getView().values()) {
if (p.id == myid) {
myQuorumAddr = p.addr;
break;
}
}
if (myQuorumAddr == null) {
throw new RuntimeException("My id " + myid + " not in the peer list");
}
//3. 如果选举方式为0, 设置相关信息
if (electionType == 0) {
try {
udpSocket = new DatagramSocket(myQuorumAddr.getPort());
responder = new ResponderThread();
responder.start();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
//设置选举类型, 默认是3
this.electionAlg = createElectionAlgorithm(electionType);
}
从代码执行流程可以看出, 虽然方法名称叫startLeaderElection, 实际上是进行Leader选举前相关信息创建,具体包含下面内容
创建选举使用的投票信息Vote, 主要包含: myid、zxid、epoch 三个主要信息
查找当前服务节点 myQuorumAddr 信息, 通过比较QuorumServer.id 、myid来识别是否属于同一节点
getView信息来源于解析zoo.cfg时. 会解析下面内容获取QuorumServer信息
server.1=IP1:2888:3888
server.2=IP2.2888:3888
server.3=IP3.2888:3888
// 集群节点配置如下
server.A=B:C:D
A 数字, 表示服务器编号, 和myid文件内容相对应
B 服务器节点IP
C 当前服务器节点和Leader服务器进行信息交换的端口
D 选举时, 服务器相互通信的端口
myid信息来源于, data目录下myid文件配置的内容
创建选举算法, 默认情况下electionType=3
//QuorumPeer
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le=null;
switch (electionAlgorithm) {
//... 省略部分代码
case 3:
//1. 创建QuorumCnxManager, 以TCP的方式为每一对Server维护一个连接connection
qcm = createCnxnManager();
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
//2. 启动listener, 用于处理连接(connection)请求
listener.start();
//3. 创建Leader选举使用的算法, 这里是FastLeaderElection
le = new FastLeaderElection(this, qcm);
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
Leader选举存在多种算法, 主要包括LeaderElection
、AuthFastLeaderElection
、AuthFastLeaderElection
、FastLeaderElection
, 默认情况下electionType=3(FastLeaderElection
), 其实通过源码也可以看出前三种方式都添加了 @Deprecated
注解, 表示这种选举方式已经废弃。这里只讲解 electionType=3创建选举方法。分析上面的代码其主要逻辑如下
执行createElectionAlgorithm方法后, 选择使用FastLeaderElection算法进行投票选举, 这个算法主要做了什么呢?
//FastLeaderElection
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){
this.stop = false;
this.manager = manager;
starter(self, manager);
}
private void starter(QuorumPeer self, QuorumCnxManager manager) {
//1. 设置成员属性信息
this.self = self;
proposedLeader = -1;
proposedZxid = -1;
//2. 实例化 发送、接受 两个阻塞队列, 用于消息的存储
sendqueue = new LinkedBlockingQueue<ToSend>();
recvqueue = new LinkedBlockingQueue<Notification>();
//3. 创建Message对象, 内部创建了两个线程用于接收、发送消息
this.messenger = new Messenger(manager);
}
//FastLeaderElection.Messenger
Messenger(QuorumCnxManager manager) {
this.ws = new WorkerSender(manager);
Thread t = new Thread(this.ws,
"WorkerSender[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
this.wr = new WorkerReceiver(manager);
t = new Thread(this.wr,
"WorkerReceiver[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
}
创建FastLeaderElection对象时, 主要做了下面的事情
1) 会创建两个阻塞队列, 分别存储ToSend、Notification信息
2) 创建Messager对象, Messager内部维护了两个ZookeeperThread子类, 分别是WorkerSend、WorkerReceiver, 创建Messager对象时, 实际创建的就是WorkerSend、WorkerReceiver两个线程, 分别用于消息的发送和消息的接收
zookeeper集群节点存在四种状态,在进行leader选举时会改变节点状态为 LOOKING, 选举结束后会改变节点状态为 LEADING 或 FOLLOWING
从上面内容可以知道,选举过程中参与选举的节点都是 LOOKING 状态, 针对线程执行方法run, 为了节省篇幅这里先只分析 LOOKING 对应的内容
//QuorumPeer
@Override
public void run() {
//... 省略部分代码
try {
/*
Main loop
*/
while (running) {
//1. 获取节点状态, 初始状态为默认值 LOOKING
switch (getPeerState()) {
case LOOKING:
LOG.info("LOOKING");
//2. 判断节点是否以只读模式启动, 通过 readonlymode.enabled 配置
if (Boolean.getBoolean("readonlymode.enabled")) {
LOG.info("Attempting to start ReadOnlyZooKeeperServer");
//... 省略部分其它代码
} else {
try {
//向后兼容, 设置bcVote
//TODO 分析的代码是3.5.x, 这个向后兼容意思不太了解 ??
setBCVote(null);
//3. 根据选举算法进行Leader选举
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
}
}
break;
//... 省略部分其它代码
}
}
} finally {
//4. 主线程退出, 将run方法开始处设置的信息恢复
LOG.warn("QuorumPeer main thread exited");
try {
MBeanRegistry.getInstance().unregisterAll();
} catch (Exception e) {
LOG.warn("Failed to unregister with JMX", e);
}
jmxQuorumBean = null;
jmxLocalPeerBean = null;
}
}
为了节约篇幅, run方法剔除了部分暂时不分析的代码, 核心的代码在Case条件为 LOOKING中, 具体执行流程如下
首先通过 getPeerState() 获取节点的状态, 初始的时候节点的状态为默认值 LOOKING
//QuorumPeer
private ServerState state = ServerState.LOOKING;
进入 LOOKING 条件后, 会根据 readonlymode.enabled 判断Server是否以只读模式启动, 什么是只读模式 ?
查看代码, 只读启动模式和非只读模式区别是: 只读模式创建了线程 roZkMgr , 用于程序等待若干时间, 选举相关的代码都是一样的
核心代码如下
setBCVote(null);
setCurrentVote(makeLEStrategy().lookForLeader());
保证向后兼容特性, 设置bcVote = null, 这样做的目的, 目前不知道 ??
根据设置的选举策略, 进行Leader选举 makeLEStrategy().lookForLeader()
无论什么时候QuorumPeer状态变更为 LOOKING , 都会开始新一轮的投票选举, 选举过程中会调用lookForLeader()方法, 它会发送Notification通知到所有其它服务器节点。lookForLeader()具体实现逻辑如下
public Vote lookForLeader() throws InterruptedException {
//省略部分其它代码
try {
//接受的投票集合
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
//发送的投票集合
HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = finalizeWait;
synchronized(this){
//1. AtomicLong 更新逻辑时钟, 用于校验是否在同一轮选举周期
logicalclock.incrementAndGet();
//2. 更新票据信息,实际是将QuorumPeer中myid、zxid、epoch更新到FastLeaderElection中
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
LOG.info("New election. My id = " + self.getId() +
", proposed zxid=0x" + Long.toHexString(proposedZxid));
//3. 创建ToSend信息, 然后存入阻塞队列 sendqueue 中, 异步发送选举信息
sendNotifications();
/*
* Loop in which we exchange notifications until we find a leader
*/
//4. 不断循环方式, 服务节点没有停止 且 服务状态为 LOOKING 时 不断处理
while ((self.getPeerState() == ServerState.LOOKING) &&
(!stop)){
/*
* Remove next notification from queue, times out after 2 times
* the termination time
*/
//5. 从阻塞队列 recvqueue 中获取其它服务节点发送的 Notification 信息
Notification n = recvqueue.poll(notTimeout,
TimeUnit.MILLISECONDS);
/*
* Sends more notifications if haven't received enough.
* Otherwise processes new notification.
*/
//6. 如果阻塞队列 recvqueue 中没有收到其它节点的 Notification 信息, 执行这部分逻辑
if(n == null){
//6.1 如果 所有的消息都已经发送, 那么再次发送自己的投票
if(manager.haveDelivered()){
sendNotifications();
//6.2 如果存在消息没有发送, 向所有其它节点发起连接操作
} else {
manager.connectAll();
}
/*
* Exponential backoff
*/
int tmpTimeOut = notTimeout*2;
notTimeout = (tmpTimeOut < maxNotificationInterval?
tmpTimeOut : maxNotificationInterval);
LOG.info("Notification time out: " + notTimeout);
}
//7. 如果接收到其它服务器的投票, 校验服务器节点是否有效, 即sid是否在配置的myid范围内
else if(validVoter(n.sid) && validVoter(n.leader)) {
/*
* Only proceed if the vote comes from a replica in the
* voting view for a replica in the voting view.
*/
//校验节点的状态
switch (n.state) {
//7.1 如果节点状态为 LOOKING , 说明其它节点也在寻找Leader
case LOOKING:
// If notification > current, replace and send messages out
//7.2 如果收到的投票逻辑时钟 > 当前节点的逻辑时钟, 说明需要更新逻辑时钟为最新的逻辑时钟
if (n.electionEpoch > logicalclock.get()) {
logicalclock.set(n.electionEpoch);
//7.3 说明之前 recvset 中存储的信息失效, 需要清空掉
recvset.clear();
//7.4 进行Vote票据比较 epoch、zxid、sid(myid)
//7.4.1 如果接收的票据较新, 更新票据为收到的票据
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
updateProposal(n.leader, n.zxid, n.peerEpoch);
//7.4.2 如果当前票据较新, 更新票据信息为当前票据
} else {
updateProposal(getInitId(),
getInitLastLoggedZxid(),
getPeerEpoch());
}
//7.5 再次创建ToSend信息, 然后存入阻塞队列 sendqueue 中, 异步发送选举信息
sendNotifications();
//7.6 如果 收到的票据的 逻辑时钟 < 当前票据的逻辑时钟, 输出日志, 跳过处理
} else if (n.electionEpoch < logicalclock.get()) {
if(LOG.isDebugEnabled()){
LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x"
+ Long.toHexString(n.electionEpoch)
+ ", logicalclock=0x" + Long.toHexString(logicalclock.get()));
}
break;
//7.7 接收的票据 和 当前节点的票据一样, 按照 epoch、zxid、sid(myid)进行比较
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)) {
// 7.7.1 如果接受的票据较新, 更新票据信息
updateProposal(n.leader, n.zxid, n.peerEpoch);
sendNotifications();
}
//7.8 将接收的票据信息存入 recvset 集合中
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
//7.9 校验是不是最后一次投票, 只要投票结果超过半数即可, 如果超过 投票结束
if (termPredicate(recvset,
new Vote(proposedLeader, proposedZxid,
logicalclock.get(), proposedEpoch))) {
// Verify if there is any change in the proposed leader
//7.9.1 等待 finalizeWait(单位毫秒) 时间用于接收新的票据, 如果收到可能改变Leader的新选票, 需要重新计票
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
/*
* This predicate is true once we don't read any new
* relevant message from the reception queue
*/
//7.10 如果 notifaction 为空, 说明在 finalizeWait 时间内没有接收新的票据,Leader节点可以确定了
if (n == null) {
//7.10.1 比较 proposedLeader 和当前节点的sid是否一致, 来确定当前节点是Leader还是follower角色
self.setPeerState((proposedLeader == self.getId()) ?
ServerState.LEADING: learningState());
Vote endVote = new Vote(proposedLeader,
proposedZxid,
logicalclock.get(),
proposedEpoch);
//7.10.2 清空 recvqueue 队列信息
leaveInstance(endVote);
return endVote;
}
}
break;
case OBSERVING: // observer 不参与选举, 跳过
LOG.debug("Notification from observer: " + n.sid);
break;
case FOLLOWING:
case LEADING:
/*
* Consider all notifications from the same epoch
* together.
*/
//检查是否已选出领导人
//8. 考虑来自同一逻辑时钟的所有通知
if(n.electionEpoch == logicalclock.get()){
recvset.put(n.sid, new Vote(n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch));
if(ooePredicate(recvset, outofelection, n)) {
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
Vote endVote = new Vote(n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
/*
* Before joining an established ensemble, verify
* a majority is following the same leader.
*/
//9. 确认大多数节点都跟随相同的Leader
outofelection.put(n.sid, new Vote(n.version,
n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch,
n.state));
if(ooePredicate(outofelection, outofelection, n)) {
synchronized(this){
logicalclock.set(n.electionEpoch);
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
}
Vote endVote = new Vote(n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
break;
default:
LOG.warn("Notification state unrecognized: {} (n.state), {} (n.sid)",
n.state, n.sid);
break;
}
} else {
if (!validVoter(n.leader)) {
LOG.warn("Ignoring notification for non-cluster member sid {} from sid {}", n.leader, n.sid);
}
if (!validVoter(n.sid)) {
LOG.warn("Ignoring notification for sid {} from non-quorum member sid {}", n.leader, n.sid);
}
}
}
return null;
} finally {
try {
if(self.jmxLeaderElectionBean != null){
MBeanRegistry.getInstance().unregister(
self.jmxLeaderElectionBean);
}
} catch (Exception e) {
LOG.warn("Failed to unregister with JMX", e);
}
self.jmxLeaderElectionBean = null;
LOG.debug("Number of connection processing threads: {}",
manager.getConnectionThreadCount());
}
}
可以看出选举逻辑还是有点复杂的, 其主要的步骤如下
首先更新当前节点的逻辑时钟 logicalclock, 并更新FastLeaderElection投票信息为当前节点相关信息, 包含epoch、zxid、myid
向其它节点发送 notifaction 通知
self.getVotingView().values() 获取zoo.cfg中配置的所有server节点信息, for循环遍历处理
这是一个异步操作, 先创建ToSend信息, 然后存入阻塞队列 sendqueue, 进行异步处理
private void sendNotifications() {
for (QuorumServer server : self.getVotingView().values()) {
long sid = server.id;
//创建 ToSend信息
ToSend notmsg = new ToSend(ToSend.mType.notification,
proposedLeader,
proposedZxid,
logicalclock.get(),
QuorumPeer.ServerState.LOOKING,
sid,
proposedEpoch);
if(LOG.isDebugEnabled()){
LOG.debug("Sending Notification: " + proposedLeader + " (n.leader), 0x" +
Long.toHexString(proposedZxid) + " (n.zxid), 0x" + Long.toHexString(logicalclock.get()) +
" (n.round), " + sid + " (recipient), " + self.getId() +
" (myid), 0x" + Long.toHexString(proposedEpoch) + " (n.peerEpoch)");
}
//存入阻塞队列 sendQueue, 进行异步处理
sendqueue.offer(notmsg);
}
}
如果当前节点是 LOOKING 状态 且没有停止服务, while循环持续不断进行处理
从 recvqueue 队列中获取 其它节点的 Notification
如果接收的节点状态 state=LOOKING , 说明其它节点也在寻在Leader, 然后进行逻辑时钟比较
从8.1节分析, 在进行 票据 比较后, 会判断最新的票据信息是否已经超过了半数,如果超过半数, 进行选举退出逻辑处理
//FastLeaderElection
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()); // 过滤出recvSet中和当前节点具有相同epoch、zxid、logiticLock、leaderId的票据
}
}
return self.getQuorumVerifier().containsQuorum(set); // 校验 票据是否 超过一半
}
FastLeaderElection在每次选举后, 会构造出本次最新票据(Vote), 然后从接收的 recvSet集合中查找所有和当前票据(Vote) 相同票据, 最后校验,这些票据数量是否超过一半
是,返回 true
否,返回false
//QuorumMaj
public boolean containsQuorum(Set<Long> set){
return (set.size() > half);
}
在讲解Leader选举过程中, 对投票的网络通信有提及过, 比如6.3节, 这里对通信内容再讲解一下
ToSend | Notification | |
---|---|---|
leader | 被推荐的服务器sid | 被推荐的服务器sid |
zxid | 被推荐服务器当前最新事物id | 被推荐服务器当前最新事物id |
peerEpoch | 被推荐服务器当前所处epoch | 被推荐服务器当前所处epoch |
electionEpoch | 当前服务器所处epoch | 竞选服务器所处epoch |
stat | 当前服务器当前状态 | 竞选服务器当前状态 |
sid | 当前接收消息服务器sid(即: myid) | 竞选服务器的sid |
ToSend、Notification是FastLeaderElection的内部类, 它对投票信息进行了包装(主要包含上面表格中的内容), 而进行投票选举主要比较的内容包括: electionEpoch、epoch、zxid、sid
, 这些信息都存储在了ToSend、Notification
QuorumPeer类通过方法createElectionAlgorithm创建选举算法时, 会创建 QuorumCnxManager 对象用于管理服务节点之间的连接, 那 QuorumCnxManager 是怎么管理连接的呢? QuorumCnxManager内部有个继承自 ZookeeperThread 线程变量Listener, 既然Listener是线程, 查看run方法代码
public void run() {
int numRetries = 0;
InetSocketAddress addr;
while((!shutdown) && (numRetries < 3)){
try {
// 创建客户端连接 ServerSocket
ss = new ServerSocket();
ss.setReuseAddress(true);
// listenOnAllIPs 是否监听端口的所有ip, 默认初始值 = false, 通过构造函数 改变值
if (listenOnAllIPs) {
int port = view.get(QuorumCnxManager.this.mySid)
.electionAddr.getPort();
addr = new InetSocketAddress(port);
} else {
addr = view.get(QuorumCnxManager.this.mySid)
.electionAddr;
}
LOG.info("My election bind port: " + addr.toString());
// 设置线程名称
setName(view.get(QuorumCnxManager.this.mySid)
.electionAddr.toString());
// 启动socket
ss.bind(addr);
// 服务没有停止, 就一直进行监听
while (!shutdown) {
Socket client = ss.accept();
setSockOpts(client);
// 根据zoo.cfg 配置文件内容 确定是异步连接、还是同步连接
//QuorumPeerMain -> quorumPeer.setQuorumSaslEnabled(config.quorumEnableSasl);
if (quorumSaslAuthEnabled) {
receiveConnectionAsync(client);
} else {
receiveConnection(client);
}
numRetries = 0; // 重置服务启动重试次数, 否则重试次数递增
}
} catch (IOException e) {
//... 省略部分逻辑
}
}
//... 省略部分逻辑
}
在第八节分析Leader选举代码时, 当前节点会将自己的信息封装成Vote票据, 然后通过 sendNotifications() 方法进行消息发送, 以便参与Leader投票, sendNotifications() 方法主要的逻辑如下。
从代码逻辑可以看出, 会将zoo.cfg配置的服务节点依次遍历, 然后将必要的信息封装到ToSend实例中, 最后存入阻塞队列 sendqueue进行异步处理。
//FaseLeaderElection
private void sendNotifications() {
/**
* 1. 给所有节点发送投票信息
* self: QuorumPeer
* getVotingView: 获取zoo.cfg 中配置的集群节点地址信息
*/
for (QuorumServer server : self.getVotingView().values()) {
long sid = server.id;
//1.1 将当前节点的最新投票信息 封装为ToSend
ToSend notmsg = new ToSend(ToSend.mType.notification,
proposedLeader,
proposedZxid,
logicalclock.get(),
QuorumPeer.ServerState.LOOKING,
sid,
proposedEpoch);
//1.2 将信息存入同步阻塞队列 sendqueue 中
sendqueue.offer(notmsg);
}
}
为什么说消息发送是WorkerSender呢 ? 首先再看下 6.3 节提到的创建 FastLeaderElection 对象构造函数执行流程, 从代码执行流程可以看出, 实例化 FastLeaderElection 时内部会创建处理消息对象 Messenger , 而Messenger 内部创建了两个线程WorkerSender、WorkerReceiver, 它们分别处理消息的发送和消息的接收。
//FastLeaderElection
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){
this.stop = false;
this.manager = manager;
starter(self, manager);
}
private void starter(QuorumPeer self, QuorumCnxManager manager) {
//1. 设置成员属性信息
this.self = self;
proposedLeader = -1;
proposedZxid = -1;
//2. 实例化 发送、接受 两个阻塞队列, 用于消息的存储
sendqueue = new LinkedBlockingQueue<ToSend>();
recvqueue = new LinkedBlockingQueue<Notification>();
//3. 创建Message对象, 内部创建了两个线程用于接收、发送消息
this.messenger = new Messenger(manager);
}
//FastLeaderElection.Messenger
Messenger(QuorumCnxManager manager) {
this.ws = new WorkerSender(manager);
Thread t = new Thread(this.ws,
"WorkerSender[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
this.wr = new WorkerReceiver(manager);
t = new Thread(this.wr,
"WorkerReceiver[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
}
既然WorkerSender是消息发送的处理类, 我们看下它是如何进行消息发送处理的, 下面是WokerSender实现类
//FastLeaderElection.WorkerSender
class WorkerSender extends ZooKeeperThread {
volatile boolean stop;
QuorumCnxManager manager;
WorkerSender(QuorumCnxManager manager){
super("WorkerSender");
this.stop = false;
this.manager = manager;
}
public void run() {
while (!stop) {
try {
//从阻塞队列 sendqueue 获取需要发送的ToSend对象, 超时时间是 3000ms
ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
if(m == null) continue;
//处理消息发送入口, 有新的message需要发送时,会调用这个方法
process(m);
} catch (InterruptedException e) {
break;
}
}
LOG.info("WorkerSender is down");
}
/**
* Called by run() once there is a new message to send.
*
* @param m message to send
*/
void process(ToSend m) {
ByteBuffer requestBuffer = buildMsg(m.state.ordinal(),
m.leader,
m.zxid,
m.electionEpoch,
m.peerEpoch);
//QuorumCnxManager#toSend进行消息发送
manager.toSend(m.sid, requestBuffer);
}
}
WorkerSender线程进行消息发送时, 实际调用的是QuorumCnxManager#toSend方法, 下面是具体代码
//QuorumCnxManager
public void toSend(Long sid, ByteBuffer b) {
/*
* If sending message to myself, then simply enqueue it (loopback).
*/
if (this.mySid == sid) {
// 如果消息发送给自己, 直放入自己接收队列中
b.position(0);
addToRecvQueue(new Message(b.duplicate(), sid));
/*
* Otherwise send to the corresponding thread to send.
*/
} else {
/*
* Start a new connection if doesn't have one already.
*/
// 如果消息发送给其它节点, 看是否有已经建立 connection
//1. 如果没有可以使用的connection, 开启一个新的连接
//2. 如果有可以使用的连接, 使用现有连接发送
ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
// 判断当前的sid是否已经存在于发送队列, 如果存在(bqExisting)则直接将已经存在的数据发送出去
ArrayBlockingQueue<ByteBuffer> bqExisting = queueSendMap.putIfAbsent(sid, bq);
if (bqExisting != null) {
addToSendQueue(bqExisting, b);
} else {
addToSendQueue(bq, b);
}
connectOne(sid);
}
}
跟踪代码可以发现,创建连接链路为 connectOne -> initiateConnection -> startConnection
,后面直接分析 startConnection
startConnection是创建连接的入口, zookeeper连接按照sid大小, 只能大的连接小的, 下面查看具体代码
//QuorumCnxmanager
private boolean startConnection(Socket sock, Long sid)
throws IOException {
// If lost the challenge, then drop the new connection
//1. 比较远程服务器节点和当前节点sid的大小
if (sid > this.mySid) {
//1.1 如果当前节点的 mySid < 远程节点sid, 说明 小sid的节点在连接大sid的节点, 需要关闭连接
LOG.info("Have smaller server identifier, so dropping the " +
"connection: (" + sid + ", " + this.mySid + ")");
closeSocket(sock);
// Otherwise proceed with the connection
} else {
//1.2 当前节点的mysid > 远程节点的sid , 建立连接
SendWorker sw = new SendWorker(sock, sid); // SendWorker继承自zookeeperThread的线程
RecvWorker rw = new RecvWorker(sock, din, sid, sw); // RecvWorker继承自zookeeperThread的线程
sw.setRecv(rw);
//2. 如果节点编号为sid的服务器之前创建过连接, 现在关闭, 然后更新SendWorker信息
SendWorker vsw = senderWorkerMap.get(sid);
if(vsw != null)
vsw.finish();
senderWorkerMap.put(sid, sw);
queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY));
//3. 启动发送、接收信息线程
sw.start();
rw.start();
return true;
}
return false;
}
SendWorker启动后, 会监听对应sid的阻塞队列, 如果启动时阻塞队列为空, 会从 lastMessageSent
获取数据进行发送, 以确保最后一条消息能被其它节点接收, 防止上次服务器处理时异常退出,造成消息没有正常处理。
如果队列有要发送的内容,不断监听阻塞队列, 有消息时调用send方法进行消息发送。
public void run() {
threadCnt.incrementAndGet();
//1. 如果sid对应的阻塞队列为空, 从lastMessageSent获取最后一次发送的数据, 避免服务器处理时异常退出, 消息没有正常处理
try {
//1.1 获取sid对应的阻塞队列
ArrayBlockingQueue<ByteBuffer> bq = queueSendMap.get(sid);
//1.2 如果阻塞队列为空, 从lastMessageSent获取最后一次发送的数据进行发送
if (bq == null || isSendQueueEmpty(bq)) {
ByteBuffer b = lastMessageSent.get(sid);
if (b != null) {
LOG.debug("Attempting to send lastMessage to sid=" + sid);
//1.3 发送数据
send(b);
}
}
} catch (IOException e) {
LOG.error("Failed to send last message. Shutting down thread.", e);
this.finish();
}
try {
//2. sid对应的阻塞队列不为空, 监听阻塞队列, 有消息时就通过send发送消息
while (running && !shutdown && sock != null) {
ByteBuffer b = null;
try {
//2.1 从queueSendMap获取sid对应的阻塞队列
ArrayBlockingQueue<ByteBuffer> bq = queueSendMap
.get(sid);
if (bq != null) {
b = pollSendQueue(bq, 1000, TimeUnit.MILLISECONDS);
} else {
LOG.error("No queue of incoming messages for " +
"server " + sid);
break;
}
//2.2 更新lastMessageSent中sid存储的消息, 然后发送消息
if(b != null){
lastMessageSent.put(sid, b);
send(b);
}
} catch (InterruptedException e) {
}
}
} catch (Exception e) {
}
}
}
根据9.2节 QuorumCnxManager#Listener 代码分析, 消息接收方法是 receiveConnection
, 该方法会调用 handleConnection
方法做最后的处理
//QuorumCnxManager
/**
* 如果服务器收到连接请求, 则放弃新的连接, 它会检测是否有连接已经连接到服务器
* 如果有服务器连接, 发送最小可能失去连接的节点信息
*/
public void receiveConnection(final Socket sock) {
DataInputStream din = null;
try {
din = new DataInputStream(
new BufferedInputStream(sock.getInputStream()));
handleConnection(sock, din);
} catch (IOException e) {
LOG.error("Exception handling connection, addr: {}, closing server connection",
sock.getRemoteSocketAddress());
closeSocket(sock);
}
}
handleConnection是处理连接请求的入口, 它主要做了什么呢? 首先看下其具体代码
private void handleConnection(Socket sock, DataInputStream din)
throws IOException {
//... 省略部分代码
LOG.debug("Authenticating learner server.id: {}", sid);
authServer.authenticate(sock, din);
//1. 如果 远程连接请求 的sid < 当前节点的连接请求, 需要将旧的连接请求关闭, 重新创建连接, 由自己发起连接
// 因为服务集群中, 只能 较大的sid 连接 较小的sid
//If wins the challenge, then close the new connection.
if (sid < this.mySid) {
/*
* This replica might still believe that the connection to sid is
* up, so we have to shut down the workers before trying to open a
* new connection.
*/
//1.1 远程节点的sid比较小, 停止旧的 SendWorker线程工作
SendWorker sw = senderWorkerMap.get(sid);
if (sw != null) {
sw.finish();
}
/*
* Now we start a new connection
*/
LOG.debug("Create new connection to server: " + sid);
//1.2 关闭connection
closeSocket(sock);
//1.3 创建新的连接, 由当前节点 连接 远程节点, 因为当前节点的sid 比较大
connectOne(sid);
// Otherwise start worker threads to receive data.
} else {
//2. 已经建立了连接, 在这个连接基础上创建 发送、接收工作线程
SendWorker sw = new SendWorker(sock, sid);
RecvWorker rw = new RecvWorker(sock, din, sid, sw);
sw.setRecv(rw);
//2.1 更新 sid 对应 sendWorker 信息
SendWorker vsw = senderWorkerMap.get(sid);
if(vsw != null)
vsw.finish();
senderWorkerMap.put(sid, sw);
//2.2 创建sid对应的阻塞队列, 并存入 queueSendMap 中
queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY));
//2.3 启动 用于消息 接收、发送 的工作线程
sw.start();
rw.start();
return;
}
}
第七节我们分析了 LOOKING 角色节点进行Leader选举的逻辑, 但是leader 选举出来后, 逻辑处理并没有结束, QuorumPeer.run() 方法仍然需要继续执行, 我们再次进入 QuorumPeer.run()方法 分析后续处理逻辑
public void run() {
//... 省略部分逻辑
try {
/*
* Main loop
*/
while (running) {
switch (getPeerState()) {
//... 省略部分逻辑
case OBSERVING:
try {
LOG.info("OBSERVING");
setObserver(makeObserver(logFactory));
observer.observeLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e );
} finally {
observer.shutdown();
setObserver(null);
setPeerState(ServerState.LOOKING);
}
break;
case FOLLOWING:
try {
LOG.info("FOLLOWING");
setFollower(makeFollower(logFactory));
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
follower.shutdown();
setFollower(null);
setPeerState(ServerState.LOOKING);
}
break;
case LEADING:
LOG.info("LEADING");
try {
setLeader(makeLeader(logFactory));
leader.lead();
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
setPeerState(ServerState.LOOKING);
}
break;
}
}
} finally {
//... 省略部分逻辑
}
}
FastLeaderElection选举之后, 会根据当前节点情况设置节点角色为 OBSERVING、FOLLOWING、LEADING
, 线程再次执行run方法时, switch会根据不同的角色走不同的逻辑, 分析上面的代码, 区别主要是 makeObserver()、makeFollower()、makeLeader()
如果需要重新leader选举, 会执行finally代码块, 更新节点的角色为 LOOKING
, 重新执行 lookForLeader()
方法
// 创建 Follower, 核心是创建 FollowerZooKeeperServer, 表示follower节点处理请求服务
protected Follower makeFollower(FileTxnSnapLog logFactory) throws IOException {
return new Follower(this, new FollowerZooKeeperServer(logFactory,
this,new ZooKeeperServer.BasicDataTreeBuilder(), this.zkDb));
}
// 创建 Leader, 核心是创建 LeaderZooKeeperServer, 表示leader节点处理请求服务
protected Leader makeLeader(FileTxnSnapLog logFactory) throws IOException {
return new Leader(this, new LeaderZooKeeperServer(logFactory,
this,new ZooKeeperServer.BasicDataTreeBuilder(), this.zkDb));
}
// 创建 Observer, 核心是创建 ObserverZooKeeperServer, 表示observer节点处理请求服务
protected Observer makeObserver(FileTxnSnapLog logFactory) throws IOException {
return new Observer(this, new ObserverZooKeeperServer(logFactory,
this, new ZooKeeperServer.BasicDataTreeBuilder(), this.zkDb));
}
这里有个重要的点, 每个节点更新自己角色信息后, 是如何和Leader建立连接的呢 ? 分析代码有下面代码 observer.observeLeader()、follower.followLeader()、leader.lead()
, 它们的作用分别是
首先看下下面代码
void followLeader() throws InterruptedException {
//... 省略部分代码
try {
//1. 根据最新投票的sid 和 所有服务节点的sid进行比较, 找到leader角色节点
QuorumServer leaderServer = findLeader();
try {
//2. 将follower连接到leader节点
connectToLeader(leaderServer.addr, leaderServer.hostname);
//3. 将follower的zxid、myid信息封装后去leader进行 epoch 信息同步
long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);
//check to see if the leader zxid is lower than ours
//this should never happen but is just a safety check
long newEpoch = ZxidUtils.getEpochFromZxid(newEpochZxid);
//4. 如果leader节点的 epoch信息比当前节点的epoch信息要小, 抛出异常
if (newEpoch < self.getAcceptedEpoch()) {
LOG.error("Proposed leader epoch " + ZxidUtils.zxidToString(newEpochZxid)
+ " is less than our accepted epoch " + ZxidUtils.zxidToString(self.getAcceptedEpoch()));
throw new IOException("Error: Epoch of leader is lower");
}
//5. 和leader进行数据同步
syncWithLeader(newEpochZxid);
QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
readPacket(qp);
processPacket(qp);
}
} catch (Exception e) {
//...
}
} finally {
zk.unregisterJMX((Learner)this);
}
}
分析上面的代码, 其主要逻辑是