在 ZK 中我们最常提到的就是“大多数”,一搜一大堆:
那么这个“大多数”到底是什么意思呢?理科生本不该纠结文字游戏,但是这个“大多数”是本文要探讨的关键。
先抛出几个问题,让大伙知道我在纠结啥。
问题
以一篇博客中的描述为例:
广播模式:leader写入数据时会发起提议,当大多数follower都同意之后,leader就会更新数据并广播给其他follower。
我对这段描述的理解是,写数据需要大多数 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可用,整个系统保持可用。
很明显这里的 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 running
at 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;//ms
static 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 之后就已经算通过了“大多数”的限制:
换句话说,三节点 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 集群基数台是比较划算的,一是可容忍宕机的数量一致,且通信效率更快。