在 ZK 中我们最常提到的就是“大多数”,一搜一大堆:

那么这个“大多数”到底是什么意思呢?理科生本不该纠结文字游戏,但是这个“大多数”是本文要探讨的关键。
先抛出几个问题,让大伙知道我在纠结啥。

问题

以一篇博客中的描述为例:

广播模式:leader写入数据时会发起提议,当大多数follower都同意之后,leader就会更新数据并广播给其他follower。
https://www.cnblogs.com/wlwl/p/10715065.html

我对这段描述的理解是,写数据需要大多数 Follower 同意(这里大多数没有包含 Leader 本身?)才行,假设现在是三节点集群,一个 Leader 两个 Follower,2 的大多数是 2,也就是说现在写一个数据要 2 个 Follower 都同意才行?
再进一步想想,这里的大多数 Follower 是指现在存活的 Follower 中的大多数还是所有记录在册的 Server 中的非observer Server 中的大多数。比如一个五节点 ZK 集群,一个 Leader 四个 Follower,假设五个节点都正常存活,大多数 Follower 就是 3,配置的 Server 中的大多数肯定也是 3;假设现在挂了一个 Follower,也就是剩下一个 Leader 三个 Follower。存活的 Follower 中的大多数是 2,但是配置的 Server 中 Follower 的大多数肯定仍然是 3,那这个大多数到底是 2 还是 3 呢?
再看一个例子:

对于2n+1台server,只要有n+1台(大多数)server可用,整个系统保持可用。
https://www.cnblogs.com/felixzh/p/5873252.html

很明显这里的 Server 肯定指的是配置的 Server,包括 Leader 和 Follower。也就是说假设一个五节点 ZK 集群,一个 Leader 四个 Follower,只要有 3 台机器可用,那么整个集群就是可用的。
综合来看,笼统得来说现在问题是(后面还有更细节的问题):

  • 大多数以配置的为准还是存活的为准?
  • 大多数包不包括 Leader 自己?

接下来通过调试 ZK 集群进行验证。

集群情况

先看下我这里 ZK 集群的节点情况:

➜  bin telnet localhost 2181
Trying ::1...
Connected to localhost.
Escape character is '^]'.
stat
Zookeeper version: 3.8.0-SNAPSHOT-1590a424cb7a8768b0ae01f2957856b1834dd68d-dirty, built on 2021-04-23 10:09 UTC
Clients:/0:0:0:0:0:0:0:1:62163[0](queued=0,recved=1,sent=0)/127.0.0.1:62157[1](queued=0,recved=2,sent=2)Latency min/avg/max: 3/3.0/3
Received: 3
Sent: 2
Connections: 2
Outstanding: 0
Zxid: 0x100000006
Mode: follower
Node count: 6
Connection closed by foreign host.
➜  bin telnet localhost 2182
Trying ::1...
Connected to localhost.
Escape character is '^]'.
stat
Zookeeper version: 3.8.0-SNAPSHOT-1590a424cb7a8768b0ae01f2957856b1834dd68d-dirty, built on 2021-04-23 10:09 UTC
Clients:/127.0.0.1:62154[1](queued=0,recved=10,sent=10)/0:0:0:0:0:0:0:1:62204[0](queued=0,recved=1,sent=0)Latency min/avg/max: 0/1.2222/4
Received: 11
Sent: 10
Connections: 2
Outstanding: 0
Zxid: 0x200000000
Mode: leader
Node count: 6
Proposal sizes last/min/max: -1/-1/-1
Connection closed by foreign host.
➜  bin telnet localhost 2183
Trying ::1...
Connected to localhost.
Escape character is '^]'.
stat
Zookeeper version: 3.8.0-SNAPSHOT-1590a424cb7a8768b0ae01f2957856b1834dd68d-dirty, built on 2021-04-23 10:09 UTC
Clients:/0:0:0:0:0:0:0:1:62206[0](queued=0,recved=1,sent=0)/0:0:0:0:0:0:0:1:62162[1](queued=0,recved=12,sent=12)Latency min/avg/max: 0/0.5455/2
Received: 13
Sent: 12
Connections: 2
Outstanding: 0
Zxid: 0x100000006
Mode: follower
Node count: 6
Connection closed by foreign host.

也就是:

localhost:2182->leader
localhost:2183->follower
localhost:2181->follower

选举

先看选举的情况。
选举其实主要看 org.apache.zookeeper.server.quorum.flexible.QuorumMaj#containsQuorum 方法就行:

    public boolean containsQuorum(Set<Long> ackSet) {return (ackSet.size() > half);}

方法很简单,就是收到的 ack 数量大于 half 就行,那么 half 是什么呢:

    public QuorumMaj(Properties props) throws ConfigException {for (Entry<Object, Object> entry : props.entrySet()) {String key = entry.getKey().toString();String value = entry.getValue().toString();if (key.startsWith("server.")) {int dot = key.indexOf('.');long sid = Long.parseLong(key.substring(dot + 1));QuorumServer qs = new QuorumServer(sid, value);allMembers.put(Long.valueOf(sid), qs);if (qs.type == LearnerType.PARTICIPANT) {votingMembers.put(Long.valueOf(sid), qs);} else {observingMembers.put(Long.valueOf(sid), qs);}} else if (key.equals("version")) {version = Long.parseLong(value, 16);}}half = votingMembers.size() / 2;}

half 就是 votingMembers 的一半数量,而 votingMembers 就是我们在 zoo.cfg 文件里面配制的服务节点中的参与者的数量,所谓的参与者就是非 OBSERVER 节点。

所以选举其实就很好理解了,即只要有半数配置的参与者投了某个节点,那么这个节点就是新的 Leader。
也就是说由 3 台机器组成了一个 ZK 集群,只要有2台机器认为某台机器是 Leader,那么它就可以成为 Leader。3 台的一半是 1 台,可以容忍不超过一半的机器宕机,也就是说 3 台机器最多可以挂一台。
目前我本地 localhost:2182 是 Leader,现在将这个进程杀掉:

➜  ~ sudo lsof -i:2182
COMMAND   PID       USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    42425 dongguabai   76u  IPv6 0x28993377dc22331b      0t0  TCP *:cgn-stat (LISTEN)
➜  ~ kill -9 42425

接下来查看剩余进程的情况:

➜  ~ telnet localhost 2181
Trying ::1...
Connected to localhost.
Escape character is '^]'.
stat
Zookeeper version: 3.8.0-SNAPSHOT-1590a424cb7a8768b0ae01f2957856b1834dd68d-dirty, built on 2021-04-23 10:09 UTC
Clients:/0:0:0:0:0:0:0:1:60792[0](queued=0,recved=1,sent=0)Latency min/avg/max: 0/0.0/0
Received: 1
Sent: 0
Connections: 1
Outstanding: 0
Zxid: 0x600000000
Mode: follower
Node count: 7
Connection closed by foreign host.
➜  ~ telnet localhost 2183
Trying ::1...
Connected to localhost.
Escape character is '^]'.
stat
Zookeeper version: 3.8.0-SNAPSHOT-1590a424cb7a8768b0ae01f2957856b1834dd68d-dirty, built on 2021-04-23 10:09 UTC
Clients:/0:0:0:0:0:0:0:1:60793[0](queued=0,recved=1,sent=0)Latency min/avg/max: 0/0.0/0
Received: 1
Sent: 0
Connections: 1
Outstanding: 0
Zxid: 0x700000000
Mode: leader
Node count: 7
Proposal sizes last/min/max: -1/-1/-1
Connection closed by foreign host.

可以发现现在 localhost:2183 是新的 Leader 了。接下来将 2183 也杀掉:

➜  ~ sudo lsof -i:2183
COMMAND   PID       USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    42441 dongguabai   76u  IPv6 0x28993377d6d95cfb      0t0  TCP *:cgn-config (LISTEN)
➜  ~ kill -9 42441

会发现此时仅剩的 2181 是无法继续提供服务的:

2021-04-24 11:34:50,533 [myid:1] - WARN  [NIOWorkerThread-3:NIOServerCnxn@380] - Close of session 0x0
java.io.IOException: ZooKeeperServer not runningat org.apache.zookeeper.server.NIOServerCnxn.readLength(NIOServerCnxn.java:554)at org.apache.zookeeper.server.NIOServerCnxn.doIO(NIOServerCnxn.java:339)at org.apache.zookeeper.server.NIOServerCnxnFactory$IOWorkRequest.doWork(NIOServerCnxnFactory.java:508)at org.apache.zookeeper.server.WorkerService$ScheduledWorkRequest.run(WorkerService.java:154)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at java.lang.Thread.run(Thread.java:748)

因为现在只有 2181 一台,但是配置的是 3 台,大于一半大多数就是 3/2+1 = 2,所以现在它自己是无法进行 Leader 选举的,我这边在 org.apache.zookeeper.server.quorum.flexible.QuorumMaj#containsQuorum 方法上加了断点,但是一直不会进去。
但如果我此时将 2182 启起来,那么就会触发新一轮选举:

此时 2181 是新的 Leader:

➜  ~ telnet localhost 2181
Trying ::1...
Connected to localhost.
Escape character is '^]'.
stat
Zookeeper version: 3.8.0-SNAPSHOT-1590a424cb7a8768b0ae01f2957856b1834dd68d-dirty, built on 2021-04-23 10:09 UTC
Clients:/0:0:0:0:0:0:0:1:60980[0](queued=0,recved=1,sent=0)/127.0.0.1:60974[1](queued=0,recved=2,sent=2)Latency min/avg/max: 1/7.5/14
Received: 3
Sent: 2
Connections: 2
Outstanding: 0
Zxid: 0xa00000001
Mode: leader
Node count: 7
Proposal sizes last/min/max: 48/48/48
Connection closed by foreign host.

但是当只有一台节点存活的时候整个集群是无法正常提供服务的。

写操作

问题

还有一个问题,我们知道过半写是 Leader 收到大多数 Follower 的 Ack 才会给所有的 Follower 发 Commit 消息,那么 N 个节点的 ZK 集群,其实 Leader 只能收到 N-1 个 Ack ,也就是说三节点的 ZK 集群,挂了一个 Follower,那么现在就是一个 Leader 一个 Follower,正常选举肯定是没问题的,但是执行写操作的时候,Leader 最多只能收到一个 Follower 的 Ack,1 肯定是小于 3 的多数 2 的,那是不是说明三节点的 ZK 集群挂了一个选举是可以的,但是无法提供写操作?

调试准备

现在现将集群重新启起来,现在集群情况:

localhost:2182->follower
localhost:2183->leader
localhost:2181->follower

写操作过半写的源码流程是:
org.apache.zookeeper.server.quorum.Leader#processAck ->
org.apache.zookeeper.server.quorum.Leader#tryToCommit ->
org.apache.zookeeper.server.quorum.SyncedLearnerTracker#hasAllQuorums ->
org.apache.zookeeper.server.quorum.flexible.QuorumMaj#containsQuorum
为了便于调试,在 org.apache.zookeeper.server.quorum.SyncedLearnerTracker#hasAllQuorums 方法中加几行日志:

org.apache.zookeeper.server.quorum.Leader#tryToCommit 方法中打上断点。
一个简单的客户端代码:

package dongguabai.zookeeper;import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;import java.util.concurrent.CountDownLatch;import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
/*** @author Dongguabai* @description* @date 2021-04-23 13:53*/
public class OriginalZkTest {//static final String CONNECT_ADDR = "172.16.140.131:2181,172.16.140.131:2181,172.16.140.131:2181";static final String CONNECT_ADDR = "localhost:2182";static final int SESSION_OUTTIME = 2000;//msstatic final CountDownLatch connectedSemaphore = new CountDownLatch(1);public static void main(String[] args) throws Exception {ZooKeeper zk = new ZooKeeper(CONNECT_ADDR, SESSION_OUTTIME, new Watcher() {public void process(WatchedEvent event) {KeeperState keeperState = event.getState();EventType eventType = event.getType();if (KeeperState.SyncConnected == keeperState) {if (EventType.None == eventType) {connectedSemaphore.countDown();System.out.println("zk connected");}}}});connectedSemaphore.await();/* List<String> children = zk.getChildren("/", null);System.out.println(children);*/String s = zk.create("/6666", "7777".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);System.out.println(s);System.out.println("connected..");zk.close();}
}

调试

启动客户端代码,很快 Leader 节点就进入了 org.apache.zookeeper.server.quorum.Leader#tryToCommit 方法,接着会进入 org.apache.zookeeper.server.quorum.SyncedLearnerTracker#hasAllQuorums 方法,此时发现打印出来的日志:

...
--------->>>acks:[2]
2021-04-24 15:16:09,602 [myid:2] - INFO  [SessionTracker:ZooKeeperServer@628] - Expiring session 0x200046d6dcb004a, timeout of 4000ms exceeded
...

现在 2182 是 Leader,它的 myid 配置的就是 2,也就是说明在统计过半 Ack 的时候是包含 Leader 自己的。
继续往下走,会发现当 Ack 集合中包含了其中一个 Follower 的 Ack 之后就已经算通过了“大多数”的限制:
![image.png](https://img-blog.csdnimg.cn/img_convert/739b46415234686f7be9189458614f69.png#clientId=u6485c70a-8eb6-4&from=paste&height=332&id=u9bcb9920&margin=[object Object]&name=image.png&originHeight=448&originWidth=964&originalType=binary&size=115865&status=done&style=none&taskId=ub8a3cd33-5e50-494d-8675-43b2328f1d7&width=714)
换句话说,三节点 ZK 集群,写数据的时候,Leader 只需要收到半数的 Follower 响应的 Ack 即可。
因为其实在处理 Ack 的时候是会算上 Leader 自己的,可以看 org.apache.zookeeper.server.quorum.AckRequestProcessor#processRequest 方法:

    /*** Forward the request as an ACK to the leader*/public void processRequest(Request request) {QuorumPeer self = leader.self;if (self != null) {request.logLatency(ServerMetrics.getMetrics().PROPOSAL_ACK_CREATION_LATENCY);leader.processAck(self.getId(), request.zxid, null);} else {LOG.error("Null QuorumPeer");}}

总结

“大多数”首先是看配置的 Server(不包含 observer);票选的时候 Leader 自己就已经算了一个 Ack。
首先 ZK集群存活,如果配置了 2N+1 台机器(不包含 observer),必须要 N+1 台机器存活整个集群才能正常服务(正常选举,正常提供读写等操作),2N 台就是需要 N+1 机器存活才能提供正常服务。
ZK 选举的时候,只要有超过一半的配置的参与选举的机器选出了 Leader,那么 Leader 就选出来了。三节点 ZK集群,最终两台机器选了同一台机器,那么 Leader 就选出来了;五节点 ZK 集群,三台机器选了同一台机器,那么 Leader 就选出来了;四节点 ZK 集群,必须要最终三台机器选了同一个机器,才能选出 Leader,也就是说四节点集群,最多只能挂一台。
半数写的时候,只需要一般的 Follower 有 Ack 就可以继续后面的全局 Commit 了。三节点 ZK 集群,Leader 只需要等一台 Follower 的 Ack;四节点的 ZK 集群 Leader 需要等两台 Follower 的 Ack。
换句话说,ZK 集群基数台是比较划算的,一是可容忍宕机的数量一致,且通信效率更快。

欢迎关注公众号:

深入理解 ZK 中的 “大多数” 机制相关推荐

  1. 再次理解STM32中的堆栈机制

    再次理解STM32中的堆栈机制 刚拿到STM32时,你只编写一个死循环 void main() { while(1); }BUILD://Program Size: Code=340 RO-data= ...

  2. 跟着 Event loop 规范理解浏览器中的异步机制

    原文发自我的 GitHub blog,欢迎关注 前言 我们都知道 JavaScript 是一门单线程语言,这意味着同一事件只能执行一个任务,结束了才能去执行下一个.如果前面的任务没有执行完,后面的任务 ...

  3. 深入理解CV中的Attention机制之SE模块

    CV中的Attention机制汇总(一):SE模块 Squeeze-and-Excitation Networks 论文链接:Squeeze-and-Excitation Networks 1. 摘要 ...

  4. 夯实Java基础系列11:深入理解Java中的回调机制

    本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...

  5. java多态 降低代码耦合性_深度分析:理解Java中的多态机制,一篇直接帮你掌握!...

    Java中的多态 1 多态是什么 多态(Polymorphism)按字面的意思就是"多种状态".在面向对象语言中,接口的多种不同的实现方式即为多态.用白话来说,就是多个对象调用同一 ...

  6. 深入理解Java中的反射机制和使用原理!详细解析invoke方法的执行和使用

    反射的概念 反射:Refelection,反射是Java的特征之一,允许运行中的Java程序获取自身信息,并可以操作类或者对象的内部属性 通过反射,可以在运行时获得程序或者程序中的每一个类型的成员活成 ...

  7. 【Java】如何理解Java中的异常机制?

    1 异常的概念 程序在执行过程中出现非正常线性,导致JVM非正常停止 异常不是语法错误 2 异常的分类 Throwable是所有错误或异常的超类 ··········Exception是编译期间异常( ...

  8. Java 并发编程解析 | 如何正确理解Java领域中的锁机制,我们一般需要掌握哪些理论知识?

    苍穹之边,浩瀚之挚,眰恦之美: 悟心悟性,善始善终,惟善惟道! -- 朝槿<朝槿兮年说> 写在开头 提起Java领域中的锁,是否有种"道不尽红尘奢恋,诉不完人间恩怨"的 ...

  9. android classloader异常,Android中ClassLoader类加载机制

    Android中apk的构建过程 构建apk 如图 所示,典型 Android 应用模块的构建流程通常依循下列步骤: 编译器将您的源代码转换成 DEX(Dalvik Executable) 文件(其中 ...

  10. 理解LSTM/RNN中的Attention机制

    转自:http://www.jeyzhang.com/understand-attention-in-rnn.html,感谢分享! 导读 目前采用编码器-解码器 (Encode-Decode) 结构的 ...

最新文章

  1. iOS开发几年了,你清楚OC中的这些东西么!!!?
  2. 关于地图中轨迹的平滑移动的实现
  3. 锤子科技犯过的构图错误你一定也犯过
  4. 双目深度估计中的自监督学习概览
  5. python windows程序管理器_获取使用python运行的windows应用程序的列表
  6. Apache ActiveMQ中的消息级别授权
  7. 技术动态 | 知识图谱上的实体链接
  8. linux.命令格式,【Linux基础知识】Linux命令格式介绍
  9. c语言编写简单的成绩管理系统,用c语言编写学生成绩管理系统
  10. python如何创建工程预设_如何在sublime3项目设置中设置python模块的搜索路径?ImportError: No module named *的解决办法...
  11. windows server2012 domain user权限配置
  12. FishC《零基础学习python》笔记--第010讲、11讲、12讲:列表:一个打了激素的数组1、2、3
  13. 谈谈教学视频加密、防录屏的方法
  14. kaldi中文语音识别
  15. 西安交大计算机专业考研复试,西交大的计算机考研初试+复试经历
  16. 每秒订单数25倍提升,蘑菇街怎样跨过海量服务架构的技术藩篱?
  17. 如何恢复删除好友的微信聊天记录?iPhone手机高效操作方法
  18. H5游戏-面试问题知识点总结
  19. Sequence operation HDU - 3397
  20. in和exists的区别

热门文章

  1. 什么是生成器 — 一篇文章让你看懂
  2. 华硕fl5600l笔记本拆机,在光驱位加装固态硬盘
  3. 华东师范大学计算机考研资料汇总
  4. Python 北京房价预测实验报告 深度学习 tensorflow keras
  5. php中法兰克福的时区,法兰克福时差与中国差多少
  6. ASEMI肖特基二极管SBT40100VDC正向压降温度系数
  7. IE主页遭篡改解决方法
  8. 软件工程——团队作业4
  9. Power Query之二 可视化数据处理
  10. mysql jdbc dao_MYSQL 之 JDBC(九):增删改查(七)DAO的补充和重构