Zookeeper选举(fastleaderelection算法)
1、选举相关概念:
选票:(myid,zxid,当前节点选取轮次,被推举服务器选举轮次,状态(looking))。
选举发生情况:启动时选举,运行时选举。
外部投票:其他服务器发送来的投票。
内部投票:服务器自身当前的投票。
选举轮次:epoch--leader选举周期。
pk:比较内部选票和外部选票,确定是否变更内部选票,主要是比较zxid和myid。
(1)每个服务器节点先初始化自己的选票,即(myid,zxid,当前节点选取轮次,被推举服务器选举轮次,状态(looking))。
(2)发送初始化选票到所有集群中的节点。
(3)接收外部选票
(4)判断选举轮次,内部选票选举轮次要是大于外部选票,就继续接收外部选票,如果小于等于外部的选举轮次,就进行选票pk,即判断自己是否要变更内部选票。
(5)变更内部选票,将选票发送至集群中。
(6)归档。每个节点将收到的所有外部选票进行归档。
(7)统计。判断是否有过半的服务器认可当前内部选票,如果是,那就选举结束,即超过一半选票同意新leader,那就成功。
2、选举过程:
Zookeeper选举Leader分为启动时选举和服务运行期间的选举。在往下看之前需要先了解服务器的几个状态:
- LOOKING:观望状态,此时还未选举出Leader
- LEADING:该服务器为Leader
- FOLLOWING:该服务器为Follower
- OBSERVING:该服务器为Observer
在构建Zookeeper集群时,最少服务器数量是2,但是服务器数量最好为奇数(因为需要过半服务器的支持才能完成投票和决策,如果是6台,那么最多允许挂掉2台服务器,存活4台进行投票决策;而如果是5台,那么最多允许挂掉的台数也是2台,但是只有3台进行投票决策。因此,相比较而言,奇数台服务器数量容错率更高的同时还降低了网络通信负担),因此,这里使用3台服务器说明,分别是server1、server2、server3。
启动时选举
启动服务器,当选举完成前,所有服务器的状态都是LOOKING。当启动server1(我们假设其myid为1,myid就是服务器id,需要自己配置,后面实际操作时会讲)时,一台服务器无法完成选举,因为需要过半服务器;然后启动server2(假设myid为2),这时两台服务器开始通信,并进入选举流程竞选Leader。
- 首先每台服务器都会将自己的myid和ZXID作为投票发送给其它服务器,因为是第一轮投票,所以假设ZXID都会0,那么server1的投票为(1,0),server2的投票为(2, 0)
- 服务器接收其它服务器的投票并检测投票是否有效,包括是否为同一轮投票,以及是否来自于LOOKING服务器
- 检验通过后,每个服务器都会将自己的投票和收到的投票进行比较。首先比较ZXID,选出最大的并更新为自己新的一轮投票,这里ZXID都是一样的,所以继续比较myid,同样选出最大的作为自己新的一轮投票。因此,这里server1会更新自己的投票为(2,0),而server2则是将自己之前的投票再重新投一次即可。
- 每一轮投票结束后,服务器都会统计投票信息,看看是否已经有服务器受到过半服务器的支持,若有,就将其作为Leader服务器,并将状态修改为LEADING,其它服务器状态则变为FOLLOWING。这里就是server2成为了Leader,选举结束。
当server3启动时,发现集群中已经存在Leader,则只需作为Follower服务器将入进去即可,不需要重新选举,除非Leader服务器挂掉。
服务运行期间的选举
服务运行期间Leader的选举其实在崩溃恢复那一节已经提及到,这里再详细说说。
- 当server2服务器崩溃时,剩余服务器因为找不到Leader,就会将自己的状态更新为LOOKING并进入Leader选举流程,这时候服务是不可用的。
- 存活服务器都会生成投票信息,因为服务已经运行一段时间,所以ZXID可能是不一样的,我们假设server1的投票为(1, 10),server3的投票为(3,11)。后面的流程就会启动时的选举是一样的了,只不过,server1在收到(3,11)投票后,只要比较ZXID即可。所以最终确定server3为新的LEADER。
Leader选举源码分析
从上文我们了解了Leader选举的核心原理,但代码层面是如何实现的呢?这就要通过分析其源码才能明白。
首先我们需要找到一个入口类,即Zookeeper集群启动的主类QuorumPeerMain:
public static void main(String[] args) {
QuorumPeerMain main = new QuorumPeerMain();
main.initializeAndRun(args);
}
protected void initializeAndRun(String[] args)
throws ConfigException, IOException
{
// 加载配置的类
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
// 从配置文件加载配置到内存中
config.parse(args[0]);
}
if (args.length == 1 && config.servers.size() > 0) {
// 配置集群
runFromConfig(config);
} else {
// there is only server in the quorum -- run as standalone
ZooKeeperServerMain.main(args);
}
}
public void runFromConfig(QuorumPeerConfig config) throws IOException {
try {
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
// 将配置信息设置到QuorumPeer类
quorumPeer = new QuorumPeer();
quorumPeer.setClientPortAddress(config.getClientPortAddress());
quorumPeer.setTxnFactory(new FileTxnSnapLog(
new File(config.getDataLogDir()),
new File(config.getDataDir())));
quorumPeer.setQuorumPeers(config.getServers());
quorumPeer.setElectionType(config.getElectionAlg());
quorumPeer.setMyid(config.getServerId());
quorumPeer.setTickTime(config.getTickTime());
quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
quorumPeer.setInitLimit(config.getInitLimit());
quorumPeer.setSyncLimit(config.getSyncLimit());
quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
quorumPeer.setCnxnFactory(cnxnFactory);
quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
quorumPeer.setLearnerType(config.getPeerType());
quorumPeer.setSyncEnabled(config.getSyncEnabled());
quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
quorumPeer.start(); // 开启子线程加载db信息以及开始选举
quorumPeer.join(); // 主线程等待子线程完成
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Quorum Peer interrupted", e);
}
}
主要逻辑都在QuorumPeer类中,该类继承自Thread类:
public synchronized void start() {
loadDataBase(); // 这里是恢复DB信息,如epoch和ZXID等
cnxnFactory.start();
startLeaderElection(); //开始选举的流程
super.start(); // 启动线程
}
synchronized public void startLeaderElection() {
try {
// 投自己一票
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
} catch(IOException e) {
RuntimeException re = new RuntimeException(e.getMessage());
re.setStackTrace(e.getStackTrace());
throw re;
}
// 创建选举算法,默认是使用FastLeaderElection选举算法
this.electionAlg = createElectionAlgorithm(electionType);
}
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le=null;
//TODO: use a factory rather than a switch
switch (electionAlgorithm) {
case 0:
le = new LeaderElection(this);
break;
case 1:
le = new AuthFastLeaderElection(this);
break;
case 2:
le = new AuthFastLeaderElection(this, true);
break;
case 3:
qcm = new QuorumCnxManager(this);
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
listener.start();
// 默认会进入到这里,可以在zoo.cfg配置文件中配置
le = new FastLeaderElection(this, qcm);
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
从上述代码中我们可以看到QuorumPeer创建选举算法的流程,默认是使用的是FastLeaderElection类(早期的版本中有LeaderElection、UDP版本的FastLeaderElection和TCP版本的FastLeaderElection,自3.4.0版本开始只保留了TCP版本的FastLeaderElection,我们也可以自己实现一个选举算法,然后在zoo.cfg配置文件配置即可)。选定选举算法后,调用了线程的start方法,那我们只需要找到run方法即可:
public void run() {
try {
while (running) {
// 根据服务器状态进入相应的流程,因为是选举流程,所以都是LOOKING状态,其它的流程可以暂时忽略
switch (getPeerState()) {
case LOOKING:
if (Boolean.getBoolean("readonlymode.enabled")) { // 当前服务器为只读服务器,与我们的流程无关
final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer(
logFactory, this,
new ZooKeeperServer.BasicDataTreeBuilder(),
this.zkDb);
Thread roZkMgr = new Thread() {
public void run() {
try {
// lower-bound grace period to 2 secs
sleep(Math.max(2000, tickTime));
if (ServerState.LOOKING.equals(getPeerState())) {
roZk.startup();
}
} catch (InterruptedException e) {
LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");
} catch (Exception e) {
LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);
}
}
};
try {
roZkMgr.start();
setBCVote(null);
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
setPeerState(ServerState.LOOKING);
} finally {
// If the thread is in the the grace period, interrupt
// to come out of waiting.
roZkMgr.interrupt();
roZk.shutdown();
}
} else {
try {
setBCVote(null);
// 选出leader
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
}
}
break;
}
}
} finally {
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;
}
}
Leader选举的细节主要就在FastLeaderElection.lookForLeader()(makeLEStrategy().lookForLeader())方法中:
// 发送和接收选票队列
LinkedBlockingQueue<ToSend> sendqueue;
LinkedBlockingQueue<Notification> recvqueue;
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){
logicalclock++;
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
// 发送自己的投票信息
sendNotifications();
// 循环直到选出Leader
while ((self.getPeerState() == ServerState.LOOKING) &&
(!stop)){
// 从队列中按顺序取出投票
Notification n = recvqueue.poll(notTimeout,
TimeUnit.MILLISECONDS);
if(n == null){ // 未收到任何投票信息
if(manager.haveDelivered()){ // 发送队列空闲情况下,就继续发送自己的投票信息
sendNotifications();
} else { // 发送队列不为null,可能是其它服务器还未启动,尝试重连
manager.connectAll();
}
}
else if(self.getVotingView().containsKey(n.sid)) { // 该消息是否属于当前集群
switch (n.state) { // 判断收到消息的节点的状态
case LOOKING:
// 判断是否是新一轮的选举
if (n.electionEpoch > logicalclock) {
logicalclock = n.electionEpoch; // 更新logicalclock
recvset.clear(); // 清空前一轮收到的投票信息
// 比较epoch、myid和zxid,并更新自己的投票为胜出的节点
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {
updateProposal(getInitId(),
getInitLastLoggedZxid(),
getPeerEpoch());
}
// 发送新的投票
sendNotifications();
} else if (n.electionEpoch < logicalclock) { // 收到的投票信息已经过期,直接忽略
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)) {
// 同一轮投票直接比较myid和zxid
updateProposal(n.leader, n.zxid, n.peerEpoch);
sendNotifications();
}
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
// 判断选举是否结束,默认算法是超过半数同意
if (termPredicate(recvset,
new Vote(proposedLeader, proposedZxid,
logicalclock, proposedEpoch))) {
// 等待所有notification都被处理完,直到超时
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
// leader已经确定
if (n == null) {
// 修改状态为leader或者follower
self.setPeerState((proposedLeader == self.getId()) ?
ServerState.LEADING: learningState());
Vote endVote = new Vote(proposedLeader,
proposedZxid,
logicalclock,
proposedEpoch);
leaveInstance(endVote);
return endVote;
}
}
break;
case OBSERVING: // observer不参与投票
LOG.debug("Notification from observer: " + n.sid);
break;
// follower和leader都要参与投票
case FOLLOWING:
case LEADING:
/*
* Consider all notifications from the same epoch
* together.
*/
if(n.electionEpoch == logicalclock){
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.
*/
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 = 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 {
LOG.warn("Ignoring notification from non-cluster member " + 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;
}
}
至此,Leader选举流程就结束了,但还有个问题,消息是如何广播的?就是sendNotifications方法:
private void sendNotifications() {
for (QuorumServer server : self.getVotingView().values()) {
long sid = server.id;
ToSend notmsg = new ToSend(ToSend.mType.notification,
proposedLeader,
proposedZxid,
logicalclock,
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) +
" (n.round), " + sid + " (recipient), " + self.getId() +
" (myid), 0x" + Long.toHexString(proposedEpoch) + " (n.peerEpoch)");
}
sendqueue.offer(notmsg);
}
}
这个方法主要是将封装一个ToSend对象并加入到发送队列中,那这个队列是被谁消费的呢?浏览FastLeaderElection类结构,我们会看到WorkerSender和WorkerReceiver两个类,不用想,一个是接收,一个发送。我们这里是广播消息,那肯定就是WorkerSender类了(这两个类都是继承自Thread类的)。我们直接看run方法:
public void run() {
while (!stop) {
try {
ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
if(m == null) continue;
process(m); // 发送ToSend
} catch (InterruptedException e) {
break;
}
}
}
最终会调用QuorumCnxManager的toSend方法:
public void toSend(Long sid, ByteBuffer b) {
if (self.getId() == sid) { // 发送给自己不需要走网络通信,直接放到接收队列中即可
b.position(0);
addToRecvQueue(new Message(b.duplicate(), sid));
} else {
// 发送给其它节点,需要判断之前是否发送过
if (!queueSendMap.containsKey(sid)) {
// 未发送过则新建发送队列,SEND_CAPACITY为1,表示每次只发送一个消息
ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(
SEND_CAPACITY);
queueSendMap.put(sid, bq);
addToSendQueue(bq, b);
} else {
// 前一个消息还未发送完成,则重新发送
ArrayBlockingQueue<ByteBuffer> bq = queueSendMap.get(sid);
if(bq != null){
addToSendQueue(bq, b);
} else {
LOG.error("No queue for server " + sid);
}
}
connectOne(sid); // 真正的底层传输逻辑
}
}
至此Leader的选举分析就全部完成,总的也就涉及到以下几个类
- QuorumPeerMain:启动类
- QuorumPeer:集群环境辅助初始化类
- FastLeaderElection:选举算法的实现,其中包含了以下几个内部类:Notification:表示收到的投票信息ToSend:表示发送给其它服务器的投票信息Messager:包含了WorkerSender发送类和WorkerReceiver接受类,通过这两个类去发送和接收投票信息
补充:
zxid:
事务id, 为了保证事务的顺序一致性,zookeeper 采用了递增的事 务 id 号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了 zxid。实现中 zxid 是一个 64 位的 数字,它高32位是epoch(ZAB协议通过epoch编号来 区分 Leader 周期变化的策略)用来标识 leader 关系是否 改变,每次一个 leader 被选出来,它都会有一个新的 epoch=(原来的epoch+1),标识当前属于那个leader的 统治时期。低32位用于递增计数epoch的变化大家可以做一个简单的实验
- 启动一个zookeeper集群。
- 在/tmp/zookeeper/VERSION-2 路径下会看到一个 currentEpoch文件。文件中显示的是当前的epoch。
- 把 leader 节点停机,这个时候在看 currentEpoch 会有 变化。 随着每次选举新的leader,epoch都会发生变化。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)