Zookeeper-集群模式(Zab协议)
文章系列
【一、Zookeeper原理解析-单机模式】
【二、Zookeeper原理解析-集群模式(Zab协议)】
一、ZK简介
zookeeper是一个分布式协调中间件。通过 Zookeeper 可以实现分布式锁(节点唯一性、顺序节点)、注册中心(临时节点、持久化节点)、配置中心、Leader选举等等。
zookeeper 采用文件目录树结构方式存储,一个目录代表一个节点(ZNode)。
ZooKeeper 提供的命名空间与标准文件系统非常相似。名称是由斜杠 (/) 分隔的路径元素序列。ZooKeeper 命名空间中的每个节点都有一个路径标识。
与标准文件系统不同的是,节点上除了存储数据内容外,还存储了数据节点本身的一些状态信息。Zk节点数据结构:
定义 | 描述 |
---|---|
czxid | 即Create ZXID,表示该数据节点被创建时的事务ID。 |
mzxid | 即Modified ZXID,表示该数据节点最后一次被更新的事务ID。 |
ctime | 即Create time,节点创建时间。 |
mtime | 即Modified time,节点最后一次被修改的时间。 |
version | 版本号。 |
cversion | 即Children version,子节点版本号。 |
aversion | 即ACL version,节点 ACl 版本号。 |
ephemeralOwner | 创建该临时节点的会话SessionID。如果为持久化节点,ephemeralOwner=0。 |
dataLength | 数据内容长度。 |
numChildren | 子节点个数。 |
pzxid | 表示该节点的子节点列表最后一次被修改时的事务ID。注意:只有子节点列表变更才会变更pxid,子节点内容变更不会影响pzxid。 |
在Zookeeper中,节点存在不同类型:
- 持久化节点:节点被创建后会一直存在服务器,直到删除操作主动清除。可用于实现分布式锁。
- 持久化顺序节点:有顺序的持久节点,节点特性和持久节点是一样的,只是额外特性表现在顺序上。
- 临时节点:会被自动清理掉的节点,它的生命周期和客户端会话绑在一起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建子节点。
- 临时顺序节点:有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后面加上数字后缀。可用于实现分布式锁。
- 容器节点:3.5.3 版本新增的,容器节点主要用来容纳子节点,如果没有给其创建子节点,容器节点表现和持久化节点一样,如果给容器节点创建了子节点,后续又把子节点清空,容器节点也会被zookeeper删除。定时任务默认60s 检查一次。
- TTL节点:默认禁用,只能通过系统配置 zookeeper.extendedTypesEnabled=true 开启,不稳定。
Zookeeper作为一个分布式微服务协调中间件,在服务启动过程中,分为了单机模式、集群模式两种类型,下面分别对两种类型的服务启动过程进行分析。
本系列的文章,所有源码摘至 zookeeper-release-3.7.0
版本。
二、Zookeeper服务端启动
Zookeeper服务端启动,依据不同系统平台提供了不同的脚本,如下:
其中,zkServer.cmd 为 Windows 系统启动执行脚本,zkServer.sh 为 Linux 系统启动执行脚本。下面我们看一下 zkSever.cmd 脚本内容:
@echo off
setlocal
call "%~dp0zkEnv.cmd"
set ZOOMAIN=org.apache.zookeeper.server.quorum.QuorumPeerMain
set ZOO_LOG_FILE=zookeeper-%USERNAME%-server-%COMPUTERNAME%.log
echo on
call %JAVA% "-Dzookeeper.log.dir=%ZOO_LOG_DIR%"
"-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%"
"-Dzookeeper.log.file=%ZOO_LOG_FILE%"
"-XX:+HeapDumpOnOutOfMemoryError"
"-XX:OnOutOfMemoryError=cmd /c taskkill /pid %%%%p /t /f"
-cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %*
endlocal
可见,Zookeeper不管在单机模式还是集群模式,都是通过运行一个 org.apache.zookeeper.server.quorum.QuorumPeerMain
主函数,完成服务启动:
QuorumPeerMain 源码:
@InterfaceAudience.Public
public class QuorumPeerMain {
private static final Logger LOG = LoggerFactory.getLogger(QuorumPeerMain.class);
private static final String USAGE = "Usage: QuorumPeerMain configfile";
protected QuorumPeer quorumPeer;
/**
* 启动zk,需要指定zk配置文件路径
* @param args[0] 配置文件的路径
*/
public static void main(String[] args) {
QuorumPeerMain main = new QuorumPeerMain();
try {
// initialize and run
main.initializeAndRun(args);
} catch (IllegalArgumentException e) {
// 省略其他无关源码....
} catch (ConfigException e) {
// 省略其他无关源码....
} catch (DatadirException e) {
// 省略其他无关源码....
} catch (AdminServerException e) {
// 省略其他无关源码....
} catch (Exception e) {
// 省略其他无关源码....
}
LOG.info("Exiting normally");
ServiceUtils.requestSystemExit(ExitCode.EXECUTION_FINISHED.getValue());
}
protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException {
// 读取并解析配置文件,封装到 QuorumPeerConfig
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
config.parse(args[0]);
}
// Start and schedule the the purge task
// 启动一个清除数据目录的定时任务
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(
config.getDataDir(), // 数据快照目录
config.getDataLogDir(), // 事务日志目录
config.getSnapRetainCount(), // 清除后要保留的快照数
config.getPurgeInterval()); // 清除间隔(小时)
purgeMgr.start();
// config.isDistributed():判断是否为集群模式
if (args.length == 1 && config.isDistributed()) {
// 集群模式
runFromConfig(config);
} else {
LOG.warn("Either no config or no quorum defined in config, running in standalone mode");
// 单机模式
ZooKeeperServerMain.main(args);
}
}
// 省略其他无关源码....
}
Zookeeper 通过 boolean config.isDistributed()
方法判断单机模式 or 集群模式,该方法逻辑如下:
public class QuorumPeerConfig {
// 验证者:用于集群选举
protected QuorumVerifier quorumVerifier = null, lastSeenQuorumVerifier = null;
// 是否采用单机模式:true-单机模式
private static boolean standaloneEnabled = true;
public boolean isDistributed() {
return quorumVerifier != null && (!standaloneEnabled || quorumVerifier.getVotingMembers().size() > 1);
}
// 省略其他无关源码....
}
2.1 Zookeeper配置文件详解
基本参数配置
参数 | 描述 |
---|---|
clientPort | 主要定义客户端连接zookeeper server的端口,默认情况下为2181 |
dataDir | 主要用来配置zookeeper server数据的存放路径 |
dataLogDir | 主要用来存储事物日志,如果该参数不配置,则事物日志存储在dataDir路下 |
tickTime | zookeeper中使用的基本时间度量单位,单位为毫秒。zookeeper客户端与服务器之间的心跳时间就是一个tickTime单位。默认值为3000毫秒,即3秒 |
集群参数配置
参数 | 描述 |
---|---|
initLimit | Follower连接到Leader并同步数据的最大时间,如果zookeeper数据比较大,可以考虑调大这个值来避免报错 |
syncLimit | Follower同步Leader的最大时间 |
leaderServes | 用于配制Leader节点是否接收客户端请求,默认情况下这个值是yes ,当集群中节点数量超过3个,建议设置为false,关闭leader节点接收客户端请求 |
server.x | 主要用来设置集群中某台server的参数,格式 [hostname]:port1:port2[:observer] zookeeper server启动的时候,会根据dataDirxia的myid文件确定当前节点的id。 注意: - port1:用于follower连接leader同步数据和转发请求 - port2:用于leader选举 |
cnxTimeout | 设置连接Leader接收通知的最大超时时间,该参数只在使用 electionAlg=3 时生效 |
electionAlg | leader选举算法: electionAlg=1:基于UDP通信的不进行权限验证算法 electionAlg=2:表示进行基于UDP通信的进行权限验证算法 electionAlg=3:表示基于TCP通信的fast leader选举 |
2.2 单机模式
2.3 集群模式(Zab协议)
zookeeper 作为一个分布式协调中间件,肯定具备高可靠、高可用等特性。
- 所有 ZooKeeper Server必须相互进行通信(数据同步、Leader选举、ACK、心跳)。
- 客户端连接到单个 ZooKeeper 服务器。客户端维护一个 TCP 连接,通过该连接发送请求、获取响应、获取监视事件并发送心跳。如果与服务器的 TCP 连接断开,客户端将连接到其他服务器。
zookeeper 集群模式,想对于单机模式下,多了几个核心功能点:
- Leader选举
- 数据同步(Leader 与 Follower、Observer数据同步)
- 崩溃恢复、原子广播(Zab协议)
2.3.1 ZK集群角色
- Leader:Leader服务器是整个zookeeper集群的核心,主要的工作任务有两项
- 事物请求的唯一调度和处理者,保证集群事物处理的顺序性
- 集群内部各服务器的调度者
- Follower:Follower角色的主要职责是
- 处理客户端非事物请求、转发事物请求给leader服务器
- 参与事物请求Proposal的投票(需要半数以上服务器通过才能通知leader commit数据; Leader发起的提案,要求Follower投票)
- 参与Leader选举的投票
- Observer:只提供非事物请求服务,转发事物请求给leader服务器,通常在于不影响集群事物处理能力的前提下提升集群非事物处理的能力。
- 不参与选举。
- 不参与数据事务提交的ack应答。
2.3.2 源码分析
在 org.apache.zookeeper.server.quorum.QuorumPeerMain#initializeAndRun()
方法中,如果采用集群模式,执行 org.apache.zookeeper.server.quorum.QuorumPeerMain#runFromConfig(QuorumPeerConfig config)
方法。
protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException {
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
config.parse(args[0]);
}
// Start and schedule the the purge task
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(
config.getDataDir(),
config.getDataLogDir(),
config.getSnapRetainCount(),
config.getPurgeInterval());
purgeMgr.start();
if (args.length == 1 && config.isDistributed()) {
// 集群模式
runFromConfig(config);
} else {
LOG.warn("Either no config or no quorum defined in config, running in standalone mode");
// 单机模式
ZooKeeperServerMain.main(args);
}
}
1. QuorumPeerMain.runFromConfig(config)
@InterfaceAudience.Public
public class QuorumPeerMain {
public void runFromConfig(QuorumPeerConfig config) throws IOException, AdminServerException {
try {
ManagedUtil.registerLog4jMBeans();
} catch (JMException e) {
LOG.warn("Unable to register log4j JMX control", e);
}
LOG.info("Starting quorum peer, myid=" + config.getServerId());
final MetricsProvider metricsProvider;
try {
// step1:创建一个MetricsProvider,用于收集 Metrics(指标) 并将当前值发布到外部设施。
// 可配置为 PrometheusMetricsProvider,向Prometheus 发送指标监控
metricsProvider = MetricsProviderBootstrap.startMetricsProvider(
config.getMetricsProviderClassName(),
config.getMetricsProviderConfiguration());
} catch (MetricsProviderLifeCycleException error) {
throw new IOException("Cannot boot MetricsProvider " + config.getMetricsProviderClassName(), error);
}
try {
ServerMetrics.metricsProviderInitialized(metricsProvider);
// step2:初始化 ProviderRegistry,用于注册各种身份验证
ProviderRegistry.initialize();
ServerCnxnFactory cnxnFactory = null;
ServerCnxnFactory secureCnxnFactory = null;
if (config.getClientPortAddress() != null) {
// step3:初始化客户端连接(普通)Factory
cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), config.getClientPortListenBacklog(), false);
}
if (config.getSecureClientPortAddress() != null) {
// step3:初始化客户端连接(安全)Factory
secureCnxnFactory = ServerCnxnFactory.createFactory();
secureCnxnFactory.configure(config.getSecureClientPortAddress(), config.getMaxClientCnxns(), config.getClientPortListenBacklog(), true);
}
quorumPeer = getQuorumPeer();
// step4:创建FileTxnSnapLog(FileTxnLog(config.dataLogDir)、FileSnap(config.dataDir))
quorumPeer.setTxnFactory(new FileTxnSnapLog(config.getDataLogDir(), config.getDataDir()));
quorumPeer.enableLocalSessions(config.areLocalSessionsEnabled());
quorumPeer.enableLocalSessionsUpgrading(config.isLocalSessionsUpgradingEnabled());
//quorumPeer.setQuorumPeers(config.getAllMembers());
quorumPeer.setElectionType(config.getElectionAlg()); // 选举类型
quorumPeer.setMyid(config.getServerId()); // myId
quorumPeer.setTickTime(config.getTickTime());
quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
quorumPeer.setInitLimit(config.getInitLimit());
quorumPeer.setSyncLimit(config.getSyncLimit());
quorumPeer.setConnectToLearnerMasterLimit(config.getConnectToLearnerMasterLimit());
quorumPeer.setObserverMasterPort(config.getObserverMasterPort());
quorumPeer.setConfigFileName(config.getConfigFilename());
quorumPeer.setClientPortListenBacklog(config.getClientPortListenBacklog());
quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory())); // zk数据库
quorumPeer.setQuorumVerifier(config.getQuorumVerifier(), false);
if (config.getLastSeenQuorumVerifier() != null) {
quorumPeer.setLastSeenQuorumVerifier(config.getLastSeenQuorumVerifier(), false);
}
quorumPeer.initConfigInZKDatabase(); // 初始化zk db config
quorumPeer.setCnxnFactory(cnxnFactory); // 客户端连接Factory
quorumPeer.setSecureCnxnFactory(secureCnxnFactory); // 客户端安全连接Factory
quorumPeer.setSslQuorum(config.isSslQuorum());
quorumPeer.setUsePortUnification(config.shouldUsePortUnification());
quorumPeer.setLearnerType(config.getPeerType());
quorumPeer.setSyncEnabled(config.getSyncEnabled());
quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
if (config.sslQuorumReloadCertFiles) {
quorumPeer.getX509Util().enableCertFileReloading();
}
quorumPeer.setMultiAddressEnabled(config.isMultiAddressEnabled());
quorumPeer.setMultiAddressReachabilityCheckEnabled(config.isMultiAddressReachabilityCheckEnabled());
quorumPeer.setMultiAddressReachabilityCheckTimeoutMs(config.getMultiAddressReachabilityCheckTimeoutMs());
// sets quorum sasl authentication configurations
quorumPeer.setQuorumSaslEnabled(config.quorumEnableSasl);
if (quorumPeer.isQuorumSaslAuthEnabled()) {
quorumPeer.setQuorumServerSaslRequired(config.quorumServerRequireSasl);
quorumPeer.setQuorumLearnerSaslRequired(config.quorumLearnerRequireSasl);
quorumPeer.setQuorumServicePrincipal(config.quorumServicePrincipal);
quorumPeer.setQuorumServerLoginContext(config.quorumServerLoginContext);
quorumPeer.setQuorumLearnerLoginContext(config.quorumLearnerLoginContext);
}
quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize);
quorumPeer.initialize(); // 初始化
if (config.jvmPauseMonitorToRun) {
// jvm监视器
quorumPeer.setJvmPauseMonitor(new JvmPauseMonitor(config));
}
/**
* 最重要的方法:采用集群模式,启动zk server
*/
quorumPeer.start();
ZKAuditProvider.addZKStartStopAuditLog();
quorumPeer.join();
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Quorum Peer interrupted", e);
} finally {
try {
metricsProvider.stop();
} catch (Throwable error) {
LOG.warn("Error while stopping metrics", error);
}
}
}
}
2. auorumPeer.start()
AuorumPeer
中的 start() 方法,主要是启动zk server、开启客户端监听、Leader选举等。
@InterfaceAudience.Public
public class QuorumPeerMain {
@Override
public synchronized void start() {
if (!getView().containsKey(myid)) {
throw new RuntimeException("My id " + myid + " not in the peer list");
}
// step1:加载zk数据
loadDataBase();
// step2:启动客户端连接监听
startServerCnxnFactory();
try {
// step3:启动管理服务器
adminServer.start();
} catch (AdminServerException e) {
LOG.warn("Problem starting AdminServer", e);
System.out.println(e);
}
// step4:开始Leader选举
startLeaderElection();
// step5:启动Jvm监听
startJvmPauseMonitor();
// step6:启动一个线程,用于Leader选举
super.start();
}
}
其中step1、step2、step3、step5 同zookeeper单机模式启动类型,可以参照 【Zookeeper原理解析-单机模式 】进行详细查看,这里不作过多分析,重点分析一下 step4(开始Leader选举)、step6。
step4、step6包括了zookeeper集群中的Zab协议核心逻辑:
- Leader选举
- 崩溃恢复
- 原子广播
2.3.3 Zab协议
zookeeper 集群中存在三种角色(Leader,Follower,Observer),其中 Observer 不参与不参与选举以及数据事务提交的ack应答。
zookeeper server在服务启动到Leader选举结束,公存在5个状态:
- LOOKING:刚启动 zookeeper 时状态,表示正在进行 Leader 选举
- OBSERVING:对应 Observer 集群角色
- FOLLOWING:成为 Follower 后的 server 所处状态
- LEADING:已被选举为 Leader 后的 server 所处状态
1. 开始Leader选举
zookeeper leader选举,通过 QuorumPeerMain.start()
方法调用其中的 startLeaderElection()
开始进行。在 QuorumPeerMain.startLeaderElection()
方法中,主要做了两件事:
- 创建Leader选举的票据Vote
- 创建选举算法
逻辑如下:
@InterfaceAudience.Public
public class QuorumPeerMain {
// 开始Leader选举
public synchronized void startLeaderElection() {
try {
// 获取节点状态:初始为LOOKING
if (getPeerState() == ServerState.LOOKING) {
/**
* 创建选举票据
* myid:当前机器id,集群唯一
* getLastLoggedZxid():获取最后的zxid,事务id
* getCurrentEpoch():获取当前选举的 epoch(时代)
*/
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
}
} catch (IOException e) {
RuntimeException re = new RuntimeException(e.getMessage());
re.setStackTrace(e.getStackTrace());
throw re;
}
// 创建选举算法:electionType 默认为3
this.electionAlg = createElectionAlgorithm(electionType);
}
// 创建选举算法:electionAlgorithm 默认为3
protected Election createElectionAlgorithm(int electionAlgorithm) {
Election le = null;
switch (electionAlgorithm) {
case 1:
throw new UnsupportedOperationException("Election Algorithm 1 is not supported.");
case 2:
throw new UnsupportedOperationException("Election Algorithm 2 is not supported.");
case 3:
// 创建一个Leader选举连接管理器
QuorumCnxManager qcm = createCnxnManager();
QuorumCnxManager oldQcm = qcmRef.getAndSet(qcm);
if (oldQcm != null) {
LOG.warn("Clobbering already-set QuorumCnxManager (restarting leader election?)");
oldQcm.halt();
}
QuorumCnxManager.Listener listener = qcm.listener;
if (listener != null) {
// 开启一个线程:用于接收zk server选举投票,拿到投票后,向FastLeaderElection 阻塞队列中put数据
listener.start();
// 创建一个快速选举Leader算法:内部包括两个阻塞队列(接收、发送),以及队列消费者线程
FastLeaderElection fle = new FastLeaderElection(this, qcm);
fle.start();
le = fle;
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
}
2. 启动Leader选举线程
在 auorumPeer.start()
方法中,启动了一个 Leader选举线程,代码如下:
@InterfaceAudience.Public
public class QuorumPeerMain {
@Override
public synchronized void start() {
// 省略其他代码....
// step6:启动一个线程,用于Leader选举
super.start();
}
}
super.start()
方法表示运行 AuorumPeer 方法中的 run()
方法。下面我们具体分析一下 run 方法中的核心逻辑。
@InterfaceAudience.Public
public class QuorumPeerMain {
@Override
public void run() {
// 修改线程名称
updateThreadName();
LOG.debug("Starting quorum peer");
try {
// 注册 jmx 监控
jmxQuorumBean = new QuorumBean(this);
MBeanRegistry.getInstance().register(jmxQuorumBean, null);
for (QuorumServer s : getView().values()) {
ZKMBeanInfo p;
if (getId() == s.id) {
p = jmxLocalPeerBean = new LocalPeerBean(this);
try {
MBeanRegistry.getInstance().register(p, jmxQuorumBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
jmxLocalPeerBean = null;
}
} else {
RemotePeerBean rBean = new RemotePeerBean(this, s);
try {
MBeanRegistry.getInstance().register(rBean, jmxQuorumBean);
jmxRemotePeerBean.put(s.id, rBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
}
}
}
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
jmxQuorumBean = null;
}
/****************Leader 选举主线程****************/
try {
while (running) {
if (unavailableStartTime == 0) {
unavailableStartTime = Time.currentElapsedTime();
}
// 获取当前zk server节点状态,初始为LOOKING
switch (getPeerState()) {
case LOOKING:
LOG.info("LOOKING");
ServerMetrics.getMetrics().LOOKING_COUNT.add(1);
// 是否开启只读模式:从未开启,所以看 else
if (Boolean.getBoolean("readonlymode.enabled")) {
// 省略其他干扰代码......
} else {
try {
reconfigFlagClear();
// 检查当前 FastLeaderElection 选举算法状态,如果关闭,则启动 FastLeaderElection
if (shuttingDownLE) {
shuttingDownLE = false;
// 启动
startLeaderElection();
}
// setCurrentVote() 设置当前投票
// makeLEStrategy() 获取选举算法
// lookForLeader() 开始新一轮的Leader选举。每当我们的 QuorumPeer 将其状态更改为 LOOKING 时,就会调用此方法,并向所有其他对等方发送通知。
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
}
}
break;
case OBSERVING:
try {
LOG.info("OBSERVING");
// 设置Observer
setObserver(makeObserver(logFactory));
// 观察Leader:进行数据同步、心跳等
observer.observeLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
observer.shutdown();
setObserver(null);
updateServerState();
// Add delay jitter before we switch to LOOKING
// state to reduce the load of ObserverMaster
if (isRunning()) {
Observer.waitForObserverElectionDelay();
}
}
break;
case FOLLOWING:
try {
LOG.info("FOLLOWING");
// 设置Follower
setFollower(makeFollower(logFactory));
// 跟随Leader:连接Leader、数据同步、心跳、2PC提交等
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
follower.shutdown();
setFollower(null);
updateServerState();
}
break;
case LEADING:
LOG.info("LEADING");
try {
// 设置Leader:完成Leader选举
setLeader(makeLeader(logFactory));
// 启动zk Leader:等待Follower、Observer连接,广播数据,发送心跳等
leader.lead();
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
updateServerState();
}
break;
}
}
} finally {
LOG.warn("QuorumPeer main thread exited");
MBeanRegistry instance = MBeanRegistry.getInstance();
instance.unregister(jmxQuorumBean);
instance.unregister(jmxLocalPeerBean);
for (RemotePeerBean remotePeerBean : jmxRemotePeerBean.values()) {
instance.unregister(remotePeerBean);
}
jmxQuorumBean = null;
jmxLocalPeerBean = null;
jmxRemotePeerBean = null;
}
}
}
3. FastLeaderElection
FastLeaderElection
为一个 Leader 选举算法,通过调用其 FastLeaderElection.lookForLeader
方法,选举出 Leader,逻辑如下:
public class FastLeaderElection implements Election {
// 选举Leader
public Vote lookForLeader() throws InterruptedException {
// jmx 监控
try {
self.jmxLeaderElectionBean = new LeaderElectionBean();
MBeanRegistry.getInstance().register(self.jmxLeaderElectionBean, self.jmxLocalPeerBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
self.jmxLeaderElectionBean = null;
}
self.start_fle = Time.currentElapsedTime();
try {
// 存储Vote Leader选举票据
Map<Long, Vote> recvset = new HashMap<Long, Vote>();
// 先前Leader选举的投票以及当前Leader选举的投票
Map<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = minNotificationInterval;
synchronized (this) {
// 当前节点epoch +1
logicalclock.incrementAndGet();
// 更新Leader相关信息
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
LOG.info(
"New election. My id = {}, proposed zxid=0x{}",
self.getId(),
Long.toHexString(proposedZxid));
// 广播Vote Leader选举票据
sendNotifications();
SyncedLearnerTracker voteSet;
// 循环,直到找出Leader
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {
// 接收票据
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
if (n == null) {
if (manager.haveDelivered()) {
// 广播Vote Leader选举票据
sendNotifications();
} else {
manager.connectAll();
}
int tmpTimeOut = notTimeout * 2;
notTimeout = Math.min(tmpTimeOut, maxNotificationInterval);
LOG.info("Notification time out: {}", notTimeout);
} else if (validVoter(n.sid) && validVoter(n.leader)) { // 校验投票者 以及 票据选择节点 sid 是否在列表中
// 判断投票者状态
switch (n.state) {
case LOOKING:
if (getInitLastLoggedZxid() == -1) {
LOG.debug("Ignoring notification as our zxid is -1");
break;
}
if (n.zxid == -1) {
LOG.debug("Ignoring notification from member with -1 zxid {}", n.sid);
break;
}
// step1:先比较 epoch
if (n.electionEpoch > logicalclock.get()) {
// 远程Epoch大,更新本地Epoch
logicalclock.set(n.electionEpoch);
// 清空本轮投票
recvset.clear();
// 如果以下三种情况之一成立,则返回 true:
// 1- 先比较Epoch,远程Epoch大 -> true
// 2- Epoch相同,但远程 zxid 更高 -> true
// 3- Epoch相同,zxid 相同,但远程 sid 大 -> true
if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
// 更新本地票据为远程票据
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {
// 更新本地票据为当前票据
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
// 广播新的Vote Leader选举票据
sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {
// 本地Epoch大,远程的为废弃票据,直接break
LOG.debug(
"Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x{}, logicalclock=0x{}",
Long.toHexString(n.electionEpoch),
Long.toHexString(logicalclock.get()));
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
// 更新本地票据为远程票据
updateProposal(n.leader, n.zxid, n.peerEpoch);
// 广播新的Vote Leader选举票据
sendNotifications();
}
// 存储所有Vote
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
// 归纳投票
voteSet = getVoteTracker(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch));
// 决断:判断当前Epoch中,是否存在节点投票数 > 节点总数的一半
if (voteSet.hasAllQuorums()) {
// 再次校验
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) {
setPeerState(proposedLeader, voteSet);
Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
leaveInstance(endVote);
return endVote;
}
}
break;
case OBSERVING:
LOG.debug("Notification from observer: {}", n.sid);
break;
case FOLLOWING:
case LEADING:
// 当前Epoch的Leader
if (n.electionEpoch == logicalclock.get()) {
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
voteSet = getVoteTracker(recvset, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
if (voteSet.hasAllQuorums() && checkLeader(recvset, n.leader, n.electionEpoch)) {
setPeerState(n.leader, voteSet);
Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
outofelection.put(n.sid, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
voteSet = getVoteTracker(outofelection, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
if (voteSet.hasAllQuorums() && checkLeader(outofelection, n.leader, n.electionEpoch)) {
synchronized (this) {
logicalclock.set(n.electionEpoch);
setPeerState(n.leader, voteSet);
}
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());
}
}
}
4. 选举流程
分析可知,FastLeaderElection 选举算法逻辑如下:
- 先比较Epoch,远程Epoch大,下次投票投远程sid
- Epoch相同,但远程 zxid 更高,下次投票投远程sid
- Epoch相同,zxid 相同,但远程 sid 大,下次投票投远程sid
- 比较当前Epoch中,是否存在zk server投票数大于总节点的 1/2,如果存在,则Leader选举成功
- 否则,进入下一轮投票
5. zk投票远程通信流程
Zookeeper 在进行进行 Leader 选举时,主要通过 FastLeaderElection 以及 QuorumCnxManager 完成投票选举、票据传递,其中主要用到了生产者消费者模式,流程如下: