资讯专栏INFORMATION COLUMN

zookeeper-选举源码分析

阿罗 / 1977人阅读

摘要:在集群中发生选举的场景有以下三种集群启动时节点重启时节点重启时本文主要针对集群启动时发生的选举实现进行分析。

zookeeper 集群中发生选举的场景有以下三种:

集群启动时

Leader 节点重启时

Follower 节点重启时

本文主要针对集群启动时发生的选举实现进行分析。

</>复制代码

  1. ZK 集群中节点在启动时会调用QuorumPeer.start方法

</>复制代码

  1. public synchronized void start() {
  2. /**
  3. * 加载数据文件,获取 lastProcessedZxid, currentEpoch,acceptedEpoch
  4. */
  5. loadDataBase();
  6. /**
  7. * 启动主线程 用于处理客户端连接请求
  8. */
  9. cnxnFactory.start();
  10. /**
  11. * 开始 leader 选举; 会相继创建选举算法的实现,创建当前节点与集群中其他节点选举通信的网络IO,并启动相应工作线程
  12. */
  13. startLeaderElection();
  14. /**
  15. * 启动 QuorumPeer 线程,监听当前节点服务状态
  16. */
  17. super.start();
  18. }
加载数据文件

loadDataBase 方法中,ZK 会通过加载数据文件获取 lastProcessedZxid , 并通过读取 currentEpoch , acceptedEpoch 文件来获取相对应的值;若上述两文件不存在,则以 lastProcessedZxid 的高 32 位作为 currentEpoch , acceptedEpoch 值并写入对应文件中。

初始选举环境

</>复制代码

  1. synchronized public void startLeaderElection() {
  2. try {
  3. // 创建投票
  4. currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
  5. } catch(IOException e) {
  6. }
  7. // 从集群中节点列表,查找当前节点与其他进行信息同步的地址
  8. for (QuorumServer p : getView().values()) {
  9. if (p.id == myid) {
  10. myQuorumAddr = p.addr;
  11. break;
  12. }
  13. }
  14. if (myQuorumAddr == null) {
  15. throw new RuntimeException("My id " + myid + " not in the peer list");
  16. }
  17. // electionType == 3
  18. this.electionAlg = createElectionAlgorithm(electionType);
  19. }

</>复制代码

  1. protected Election createElectionAlgorithm(int electionAlgorithm){
  2. Election le=null;
  3. //TODO: use a factory rather than a switch
  4. switch (electionAlgorithm) {
  5. // 忽略其他算法的实现
  6. case 3:
  7. /**
  8. * 创建 QuorumCnxManager 实例,并启动 QuorumCnxManager.Listener 线程用于与集群中其他节点进行选举通信;
  9. */
  10. qcm = createCnxnManager();
  11. QuorumCnxManager.Listener listener = qcm.listener;
  12. if(listener != null){
  13. listener.start();
  14. /**
  15. * 创建选举算法 FastLeaderElection 实例
  16. */
  17. le = new FastLeaderElection(this, qcm);
  18. } else {
  19. LOG.error("Null listener when initializing cnx manager");
  20. }
  21. break;
  22. default:
  23. assert false;
  24. }
  25. return le;
  26. }

初始节点的相关实例之后,执行 super.start() 方法,因 QuorumPeer 类继承 ZooKeeperThread 故会启动 QuorumPeer 线程

</>复制代码

  1. public void run() {
  2. // 代码省略
  3. try {
  4. /*
  5. * Main loop
  6. */
  7. while (running) {
  8. switch (getPeerState()) {
  9. case LOOKING:
  10. LOG.info("LOOKING");
  11. if (Boolean.getBoolean("readonlymode.enabled")) {
  12. // 只读模式下代码省略
  13. } else {
  14. try {
  15. setBCVote(null);
  16. setCurrentVote(makeLEStrategy().lookForLeader());
  17. } catch (Exception e) {
  18. LOG.warn("Unexpected exception", e);
  19. setPeerState(ServerState.LOOKING);
  20. }
  21. }
  22. break;
  23. // 忽略其他状态下的处理逻辑
  24. }
  25. }
  26. } finally {
  27. }
  28. }
选举

从上述代码可以看出 QuorumPeer 线程在运行过程中轮询监听当前节点的状态并进行相应的逻辑处理,集群启动时节点状态为 LOOKING (也就是选举 Leader 过程),此时会调用 FastLeaderElection.lookForLeader 方法 (也是投票选举算法的核心)简化后源码如下:

</>复制代码

  1. public Vote lookForLeader() throws InterruptedException {
  2. // 忽略
  3. try {
  4. HashMap recvset = new HashMap();
  5. HashMap outofelection = new HashMap();
  6. int notTimeout = finalizeWait;
  7. synchronized(this){
  8. // logicalclock 逻辑时钟加一
  9. logicalclock.incrementAndGet();
  10. /**
  11. * 更新提案信息,用于后续投票;集群启动节点默认选举自身为 Leader
  12. */
  13. updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
  14. }
  15. /**
  16. * 发送选举投票提案
  17. */
  18. sendNotifications();
  19. /*
  20. * Loop in which we exchange notifications until we find a leader
  21. */
  22. while ((self.getPeerState() == ServerState.LOOKING) &&
  23. (!stop)){
  24. /*
  25. * Remove next notification from queue, times out after 2 times
  26. * the termination time
  27. */
  28. /**
  29. * 从 recvqueue 队列中获取外部节点的选举投票信息
  30. */
  31. Notification n = recvqueue.poll(notTimeout,
  32. TimeUnit.MILLISECONDS);
  33. /*
  34. * Sends more notifications if haven"t received enough.
  35. * Otherwise processes new notification.
  36. */
  37. if(n == null){
  38. /**
  39. * 检查上一次发送的选举投票信息是否全部发送;
  40. * 若已发送则重新在发送一遍,反之说明当前节点与集群中其他节点未连接,则执行 connectAll() 建立连接
  41. */
  42. if(manager.haveDelivered()){
  43. sendNotifications();
  44. } else {
  45. manager.connectAll();
  46. }
  47. /*
  48. * Exponential backoff
  49. */
  50. int tmpTimeOut = notTimeout*2;
  51. notTimeout = (tmpTimeOut < maxNotificationInterval?
  52. tmpTimeOut : maxNotificationInterval);
  53. LOG.info("Notification time out: " + notTimeout);
  54. }
  55. else if(self.getVotingView().containsKey(n.sid)) {
  56. /**
  57. * 只处理同一集群中节点的投票请求
  58. */
  59. switch (n.state) {
  60. case LOOKING:
  61. // If notification > current, replace and send messages out
  62. if (n.electionEpoch > logicalclock.get()) {
  63. /**
  64. * 外部投票选举周期大于当前节点选举周期
  65. *
  66. * step1 : 更新选举周期值
  67. * step2 : 清空已收到的选举投票数据
  68. * step3 : 选举投票 PK,选举规则参见 totalOrderPredicate 方法
  69. * step4 : 变更选举投票并发送
  70. */
  71. logicalclock.set(n.electionEpoch);
  72. recvset.clear();
  73. if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
  74. getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
  75. updateProposal(n.leader, n.zxid, n.peerEpoch);
  76. } else {
  77. updateProposal(getInitId(),
  78. getInitLastLoggedZxid(),
  79. getPeerEpoch());
  80. }
  81. sendNotifications();
  82. } else if (n.electionEpoch < logicalclock.get()) {
  83. // 丢弃小于当前选举周期的投票
  84. break;
  85. } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
  86. proposedLeader, proposedZxid, proposedEpoch)) {
  87. /**
  88. * 同一选举周期
  89. *
  90. * step1 : 选举投票 PK,选举规则参见 totalOrderPredicate 方法
  91. * step2 : 变更选举投票并发送
  92. */
  93. updateProposal(n.leader, n.zxid, n.peerEpoch);
  94. sendNotifications();
  95. }
  96. /**
  97. * 记录外部选举投票信息
  98. */
  99. recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
  100. /**
  101. * 统计选举投票结果,判断是否可以结束此轮选举
  102. */
  103. if (termPredicate(recvset,
  104. new Vote(proposedLeader, proposedZxid,
  105. logicalclock.get(), proposedEpoch))) {
  106. // ......
  107. if (n == null) {
  108. /**
  109. * 选举结束判断当前节点状态; 若提案的 leader == myid 则 state = LEADING, 反之为 FOLLOWING
  110. */
  111. self.setPeerState((proposedLeader == self.getId()) ?
  112. ServerState.LEADING: learningState());
  113. // 变更当前投票信息
  114. Vote endVote = new Vote(proposedLeader,
  115. proposedZxid,
  116. logicalclock.get(),
  117. proposedEpoch);
  118. leaveInstance(endVote);
  119. return endVote;
  120. }
  121. }
  122. break;
  123. case OBSERVING:
  124. LOG.debug("Notification from observer: " + n.sid);
  125. break;
  126. case FOLLOWING:
  127. case LEADING:
  128. // ......
  129. break;
  130. default:
  131. LOG.warn("Notification state unrecognized: {} (n.state), {} (n.sid)",
  132. n.state, n.sid);
  133. break;
  134. }
  135. } else {
  136. LOG.warn("Ignoring notification from non-cluster member " + n.sid);
  137. }
  138. }
  139. return null;
  140. } finally {
  141. // ......
  142. }
  143. }

lookForLeader 方法的实现可以看出,选举流程如下:

发送内部投票

</>复制代码

  1. 内部投票发送逻辑参考后续小节

接收外部投票

</>复制代码

  1. 接收外部投票逻辑参考后续小节

选举投票 PK

</>复制代码

  1. 当接收到外部节点投票信息后会与内部投票信息进行 PK 已确定投票优先权;PK 规则参见 totalOrderPredicate 方法如下

</>复制代码

  1. protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
  2. if(self.getQuorumVerifier().getWeight(newId) == 0){
  3. return false;
  4. }
  5. /*
  6. * We return true if one of the following three cases hold:
  7. * 1- New epoch is higher
  8. * 2- New epoch is the same as current epoch, but new zxid is higher
  9. * 3- New epoch is the same as current epoch, new zxid is the same
  10. * as current zxid, but server id is higher.
  11. */
  12. return ((newEpoch > curEpoch) ||
  13. ((newEpoch == curEpoch) &&
  14. ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
  15. }

从其实现可以看出选举投票 PK 规则如下:

</>复制代码

  1. * 比较外部投票与内部投票的选举周期值,选举周期大的值优先
  2. * 若选举周期值一致,则比较事务 ID; 事务 ID 最新的优先
  3. * 若选举周期值一致且事务 ID 值相同,则比较投票节点的 server id; server id 最大的优先

统计选举投票

</>复制代码

  1. 当接收到外部投票之后,都会统计下此轮选举的投票情况并判断是否可结束选举; 参考 termPredicate 方法

</>复制代码

  1. protected boolean termPredicate(
  2. HashMap votes,
  3. Vote vote) {
  4. HashSet set = new HashSet();
  5. /**
  6. * 统计接收的投票中与当前节点所推举 leader 投票一致的个数
  7. */
  8. for (Map.Entry entry : votes.entrySet()) {
  9. if (vote.equals(entry.getValue())){
  10. set.add(entry.getKey());
  11. }
  12. }
  13. /**
  14. * 如果超过一半的投票一致 则说明可以终止本次选举
  15. */
  16. return self.getQuorumVerifier().containsQuorum(set);
  17. }

确认节点角色

</>复制代码

  1. 当此轮选举结束之后,通过判断所推举的 leader server id 是否与当前节点 server id 相等; 若相等则说明当前节点为 leader, 反之为 follower。

发送接收投票

</>复制代码

  1. 上文中主要聊了下 ZK 选举算法的核心部分,下面接着看下集群节点在选举过程中是如何发送自己的投票和接收外部的投票及相关处理逻辑。

首先通过 FastLeaderElection.sendNotifications 方法看下发送投票逻辑:

</>复制代码

  1. private void sendNotifications() {
  2. for (QuorumServer server : self.getVotingView().values()) {
  3. long sid = server.id;
  4. /**
  5. * 发送投票通知信息
  6. *
  7. * leader : 被推举的服务器 myid
  8. * zxid : 被推举的服务器 zxid
  9. * electionEpoch : 当前节点选举周期
  10. * ServerState state : 当前节点状态
  11. * sid : 消息接收方 myid
  12. * peerEpoch : 被推举的服务器 epoch
  13. */
  14. ToSend notmsg = new ToSend(ToSend.mType.notification,
  15. proposedLeader,
  16. proposedZxid,
  17. logicalclock.get(),
  18. QuorumPeer.ServerState.LOOKING,
  19. sid,
  20. proposedEpoch);
  21. /**
  22. * 将消息添加到队列 sendqueue 中;
  23. *
  24. * @see Messenger.WorkerSender sendqueue 队列会被 WorkerSender 消费
  25. */
  26. sendqueue.offer(notmsg);
  27. }
  28. }

从实现可以看出节点在启动阶段会将自身信息封装为 ToSend 实例(也就是选举自身为 leader)并添加到队列 FastLeaderElection.sendqueue 中;那么此时我们会问到 FastLeaderElection.sendqueue 队列中的消息被谁消费处理呢 ? 让我们回过头看下节点在启动初始化选举环境时创建 QuorumCnxManager, FastLeaderElection 实例的过程。

</>复制代码

  1. PS : FastLeaderElection.sendqueue 队列中消息被谁消费 ?
QuorumCnxManager

</>复制代码

  1. public QuorumCnxManager(final long mySid,
  2. Map view,
  3. QuorumAuthServer authServer,
  4. QuorumAuthLearner authLearner,
  5. int socketTimeout,
  6. boolean listenOnAllIPs,
  7. int quorumCnxnThreadsSize,
  8. boolean quorumSaslAuthEnabled,
  9. ConcurrentHashMap senderWorkerMap) {
  10. this.senderWorkerMap = senderWorkerMap;
  11. this.recvQueue = new ArrayBlockingQueue(RECV_CAPACITY);
  12. this.queueSendMap = new ConcurrentHashMap>();
  13. this.lastMessageSent = new ConcurrentHashMap();
  14. String cnxToValue = System.getProperty("zookeeper.cnxTimeout");
  15. if(cnxToValue != null){
  16. this.cnxTO = Integer.parseInt(cnxToValue);
  17. }
  18. this.mySid = mySid;
  19. this.socketTimeout = socketTimeout;
  20. this.view = view;
  21. this.listenOnAllIPs = listenOnAllIPs;
  22. initializeAuth(mySid, authServer, authLearner, quorumCnxnThreadsSize,
  23. quorumSaslAuthEnabled);
  24. listener = new Listener();
  25. }

QuorumCnxManager 实例化后,会启动一个 QuorumCnxManager.Listener 线程;同时在 QuorumCnxManager 实例中存在三个重要的集合容器变量:

senderWorkerMap : 发送器集合,Map 类型按 server id 分组;为集群中的每个节点分配一个 SendWorker 负责消息的发送

recvQueue : 消息接收队列,用于存放从外部节点接收到的投票消息

queueSendMap : 消息发送队列,Map 类型按 server id 分组;为集群中的每个节点分配一个阻塞队列存放待发送的消息,从而保证各个节点之间的消息发送互不影响

下面我们再看下 QuorumCnxManager.Listener 线程启动后,主要做了什么:

</>复制代码

  1. public void run() {
  2. int numRetries = 0;
  3. InetSocketAddress addr;
  4. while((!shutdown) && (numRetries < 3)){
  5. try {
  6. ss = new ServerSocket();
  7. ss.setReuseAddress(true);
  8. /**
  9. * 获取当前节点的选举地址并 bind 监听等待外部节点连接
  10. */
  11. addr = view.get(QuorumCnxManager.this.mySid).electionAddr;
  12. ss.bind(addr);
  13. while (!shutdown) {
  14. /**
  15. * 接收外部节点连接并处理
  16. */
  17. Socket client = ss.accept();
  18. setSockOpts(client);
  19. receiveConnection(client);
  20. numRetries = 0;
  21. }
  22. } catch (IOException e) {
  23. LOG.error("Exception while listening", e);
  24. numRetries++;
  25. ss.close();
  26. Thread.sleep(1000);
  27. }
  28. }
  29. }

跟踪代码发现 receiveConnection 方法最终会调用方法 handleConnection 如下

</>复制代码

  1. private void handleConnection(Socket sock, DataInputStream din)
  2. throws IOException {
  3. /**
  4. * 读取外部节点的 server id
  5. * ps : 此时的 server id 是什么时候发送的呢 ?
  6. */
  7. Long sid = din.readLong();
  8. if (sid < this.mySid) {
  9. /**
  10. * 若外部节点的 server id 小于当前节点的 server id,则关闭此连接,改为由当前节点发起连接
  11. * ps : 该限制说明选举过程中,zk 只允许 server id 较大的一方去主动发起连接避免重复连接
  12. */
  13. SendWorker sw = senderWorkerMap.get(sid);
  14. if (sw != null) {
  15. sw.finish();
  16. }
  17. closeSocket(sock);
  18. connectOne(sid);
  19. } else {
  20. SendWorker sw = new SendWorker(sock, sid);
  21. RecvWorker rw = new RecvWorker(sock, din, sid, sw);
  22. sw.setRecv(rw);
  23. SendWorker vsw = senderWorkerMap.get(sid);
  24. if(vsw != null)
  25. vsw.finish();
  26. /**
  27. * 按 server id 分组,为外部节点分配 SendWorker, RecvWorker 和一个消息发送队列
  28. */
  29. senderWorkerMap.put(sid, sw);
  30. queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue(SEND_CAPACITY));
  31. /**
  32. * 启动外部节点对应的 SendWorker, RecvWorker 线程
  33. */
  34. sw.start();
  35. rw.start();
  36. return;
  37. }
  38. }

至此会发现 QuorumCnxManager.Listener 线程处理逻辑如下:

监听当前节点的 election address 等待接收外部节点连接

读取外部节点的 server id 并与当前节点的 server id 比较;若前者小则关闭连接,改由当前节点发起连接

反之为外部节点分配 SendWorker,RecvWorker 线程及消息发送队列

</>复制代码

  1. PS : 此处我们会有个疑问外部节点的 server id 是什么时候发送过来的呢 ?

下面我们在看下为每个外部节点开启了 SendWorkerRecvWorker 线程后做了什么:

SendWorker

</>复制代码

  1. public void run() {
  2. // 省略
  3. try {
  4. while (running && !shutdown && sock != null) {
  5. ByteBuffer b = null;
  6. try {
  7. /**
  8. * 通过 server id 获取待发送给集群中节点的消息队列
  9. */
  10. ArrayBlockingQueue bq = queueSendMap
  11. .get(sid);
  12. if (bq != null) {
  13. /**
  14. * 从队列中获取待发送的消息
  15. */
  16. b = pollSendQueue(bq, 1000, TimeUnit.MILLISECONDS);
  17. } else {
  18. LOG.error("No queue of incoming messages for " +
  19. "server " + sid);
  20. break;
  21. }
  22. if(b != null){
  23. lastMessageSent.put(sid, b);
  24. /**
  25. * 写入 socket 的输出流完成消息的发送
  26. */
  27. send(b);
  28. }
  29. } catch (InterruptedException e) {
  30. }
  31. }
  32. } catch (Exception e) {
  33. }
  34. }
  35. synchronized void send(ByteBuffer b) throws IOException {
  36. byte[] msgBytes = new byte[b.capacity()];
  37. try {
  38. b.position(0);
  39. b.get(msgBytes);
  40. } catch (BufferUnderflowException be) {
  41. LOG.error("BufferUnderflowException ", be);
  42. return;
  43. }
  44. /**
  45. * 发送的报文包括:消息体正文长度和消息体正文
  46. */
  47. dout.writeInt(b.capacity());
  48. dout.write(b.array());
  49. dout.flush();
  50. }

通过代码实现我们知道 SendWorker 的职责就是从 queueSendMap 队列中获取待发送给远程节点的消息并执行发送。

</>复制代码

  1. PS : 此处我们会有个疑问 QuorumCnxManager.queueSendMap 中节点对应队列中待发送的消息是谁生产的呢 ?

RecvWorker

</>复制代码

  1. public void run() {
  2. threadCnt.incrementAndGet();
  3. try {
  4. while (running && !shutdown && sock != null) {
  5. /**
  6. * 读取外部节点发送的消息
  7. * 由 SendWorker 可知前 4 字节为消息载体有效长度
  8. */
  9. int length = din.readInt();
  10. if (length <= 0 || length > PACKETMAXSIZE) {
  11. throw new IOException(
  12. "Received packet with invalid packet: "
  13. + length);
  14. }
  15. /**
  16. * 读取消息体正文
  17. */
  18. byte[] msgArray = new byte[length];
  19. din.readFully(msgArray, 0, length);
  20. ByteBuffer message = ByteBuffer.wrap(msgArray);
  21. /**
  22. * 将读取的消息包装为 Message 对象添加到队列 recvQueue 中
  23. */
  24. addToRecvQueue(new Message(message.duplicate(), sid));
  25. }
  26. } catch (Exception e) {
  27. LOG.warn("Connection broken for id " + sid + ", my id = "
  28. + QuorumCnxManager.this.mySid + ", error = " , e);
  29. } finally {
  30. LOG.warn("Interrupting SendWorker");
  31. sw.finish();
  32. if (sock != null) {
  33. closeSocket(sock);
  34. }
  35. }
  36. }
  37. public void addToRecvQueue(Message msg) {
  38. synchronized(recvQLock) {
  39. // 省略
  40. try {
  41. recvQueue.add(msg);
  42. } catch (IllegalStateException ie) {
  43. // This should never happen
  44. LOG.error("Unable to insert element in the recvQueue " + ie);
  45. }
  46. }
  47. }

从上面可以看出 RecvWorker 线程在运行期间会接收 server id 对应的外部节点发送的消息,并将其放入 QuorumCnxManager.recvQueue 队列中。
到目前为止我们基本完成对 QuorumCnxManager 核心功能的分析,发现其功能主要是负责集群中当前节点与外部节点进行选举通讯的网络 IO 操作,譬如接收外部节点选举投票和向外部节点发送内部投票。

FastLeaderElection

下面我们在接着回头看下 FastLeaderElection 类实例的过程:

</>复制代码

  1. public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){
  2. this.stop = false;
  3. this.manager = manager;
  4. starter(self, manager);
  5. }
  6. private void starter(QuorumPeer self, QuorumCnxManager manager) {
  7. this.self = self;
  8. proposedLeader = -1;
  9. proposedZxid = -1;
  10. sendqueue = new LinkedBlockingQueue();
  11. recvqueue = new LinkedBlockingQueue();
  12. this.messenger = new Messenger(manager);
  13. }

</>复制代码

  1. Messenger(QuorumCnxManager manager) {
  2. /**
  3. * 启动 WorkerSender 线程用于发送消息
  4. */
  5. this.ws = new WorkerSender(manager);
  6. Thread t = new Thread(this.ws,
  7. "WorkerSender[myid=" + self.getId() + "]");
  8. t.setDaemon(true);
  9. t.start();
  10. /**
  11. * 启动 WorkerReceiver 线程用于接收消息
  12. */
  13. this.wr = new WorkerReceiver(manager);
  14. t = new Thread(this.wr,
  15. "WorkerReceiver[myid=" + self.getId() + "]");
  16. t.setDaemon(true);
  17. t.start();
  18. }

FastLeaderElection 实例化过程我们知道,其内部分别启动了线程 WorkerSenderWorkerReceiver ;那么接下来看下这两个线程具体做什么吧。

WorkerSender

</>复制代码

  1. public void run() {
  2. while (!stop) {
  3. try {
  4. /**
  5. * 从 sendqueue 队列中获取 ToSend 待发送的消息
  6. */
  7. ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
  8. if(m == null) continue;
  9. process(m);
  10. } catch (InterruptedException e) {
  11. break;
  12. }
  13. }
  14. LOG.info("WorkerSender is down");
  15. }
  16. void process(ToSend m) {
  17. // 将 ToSend 转换为 40字节 ByteBuffer
  18. ByteBuffer requestBuffer = buildMsg(m.state.ordinal(),
  19. m.leader,
  20. m.zxid,
  21. m.electionEpoch,
  22. m.peerEpoch);
  23. // 交由 QuorumCnxManager 执行发送
  24. manager.toSend(m.sid, requestBuffer);
  25. }

看了 WorkerSender 的实现是不是明白了什么? 还记得上文中 FastLeaderElection.sendNotifications 方法执行发送通知的时候的疑惑吗 ? FastLeaderElection.sendqueue 队列产生的消息就是被 WorkerSender 线程所消费处理, WorkerSender 会将消息转发至 QuorumCnxManager 处理

</>复制代码

  1. public void toSend(Long sid, ByteBuffer b) {
  2. /*
  3. * If sending message to myself, then simply enqueue it (loopback).
  4. * 如果是发给自己的投票,则将其添加到接收队列中等待处理
  5. */
  6. if (this.mySid == sid) {
  7. b.position(0);
  8. addToRecvQueue(new Message(b.duplicate(), sid));
  9. /*
  10. * Otherwise send to the corresponding thread to send.
  11. */
  12. } else {
  13. /*
  14. * Start a new connection if doesn"t have one already.
  15. */
  16. ArrayBlockingQueue bq = new ArrayBlockingQueue(SEND_CAPACITY);
  17. ArrayBlockingQueue bqExisting = queueSendMap.putIfAbsent(sid, bq);
  18. // 将发送的消息放入对应的队列中,若队列满了则将队列头部元素移除
  19. if (bqExisting != null) {
  20. addToSendQueue(bqExisting, b);
  21. } else {
  22. addToSendQueue(bq, b);
  23. }
  24. connectOne(sid);
  25. }
  26. }
  27. private void addToSendQueue(ArrayBlockingQueue queue,
  28. ByteBuffer buffer) {
  29. // 省略
  30. try {
  31. // 将消息插入节点对应的队列中
  32. queue.add(buffer);
  33. } catch (IllegalStateException ie) {
  34. }
  35. }

QuorumCnxManager 在收到 FastLeaderElection.WorkerSender 转发的消息时,会判断当前消息是否发给自己的投票,若是则将消息添加到接收队列中,反之会将消息添加到 queueSendMap 对应 server id 的队列中;看到这里的时候是不是就明白了在 QuorumCnxManager.SendWorker 分析时候的疑惑呢 。 这个时候投票消息未必能够发送出去,因为当前节点与外部节点的通道是否已建立还未知,所以继续执行 connectOne

</>复制代码

  1. synchronized public void connectOne(long sid){
  2. /**
  3. * 判断当前服务节点是否与 sid 外部服务节点建立连接;有可能对方先发起连接
  4. * 若已连接则等待后续处理,反之发起连接
  5. */
  6. if (!connectedToPeer(sid)){
  7. InetSocketAddress electionAddr;
  8. if (view.containsKey(sid)) {
  9. electionAddr = view.get(sid).electionAddr;
  10. } else {
  11. LOG.warn("Invalid server id: " + sid);
  12. return;
  13. }
  14. try {
  15. LOG.debug("Opening channel to server " + sid);
  16. Socket sock = new Socket();
  17. setSockOpts(sock);
  18. sock.connect(view.get(sid).electionAddr, cnxTO);
  19. LOG.debug("Connected to server " + sid);
  20. initiateConnection(sock, sid);
  21. } catch (UnresolvedAddressException e) {
  22. } catch (IOException e) {
  23. }
  24. } else {
  25. LOG.debug("There is a connection already for server " + sid);
  26. }
  27. }
  28. public boolean connectedToPeer(long peerSid) {
  29. return senderWorkerMap.get(peerSid) != null;
  30. }

</>复制代码

  1. private boolean startConnection(Socket sock, Long sid)
  2. throws IOException {
  3. DataOutputStream dout = null;
  4. DataInputStream din = null;
  5. try {
  6. /**
  7. * 发送当前节点的 server id,需告知对方我是哪台节点
  8. */
  9. dout = new DataOutputStream(sock.getOutputStream());
  10. dout.writeLong(this.mySid);
  11. dout.flush();
  12. din = new DataInputStream(
  13. new BufferedInputStream(sock.getInputStream()));
  14. } catch (IOException e) {
  15. LOG.warn("Ignoring exception reading or writing challenge: ", e);
  16. closeSocket(sock);
  17. return false;
  18. }
  19. // 只允许 sid 值大的服务器去主动和其他服务器连接,否则断开连接
  20. if (sid > this.mySid) {
  21. LOG.info("Have smaller server identifier, so dropping the " +
  22. "connection: (" + sid + ", " + this.mySid + ")");
  23. closeSocket(sock);
  24. // Otherwise proceed with the connection
  25. } else {
  26. SendWorker sw = new SendWorker(sock, sid);
  27. RecvWorker rw = new RecvWorker(sock, din, sid, sw);
  28. sw.setRecv(rw);
  29. SendWorker vsw = senderWorkerMap.get(sid);
  30. if(vsw != null)
  31. vsw.finish();
  32. senderWorkerMap.put(sid, sw);
  33. queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue(SEND_CAPACITY));
  34. sw.start();
  35. rw.start();
  36. return true;
  37. }
  38. return false;
  39. }

从上述代码可以看出节点在与外部节点连接后会先发送 myid 报文告知对方我是哪个节点(这也是为什么 QuorumCnxManager.Listener 线程在接收到一个连接请求时会先执行 getLong 获取 server id 了);同样在连接建立的时候也遵循一个原则(只允许 server id 较大的一方发起连接)。

WorkerReceiver

</>复制代码

  1. public void run() {
  2. Message response;
  3. while (!stop) {
  4. // Sleeps on receive
  5. try{
  6. /**
  7. * 从 QuorumCnxManager.recvQueue 队列中获取接收的外部投票
  8. */
  9. response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);
  10. if(response == null) continue;
  11. if(!self.getVotingView().containsKey(response.sid)){
  12. // 忽略对方是观察者的处理
  13. } else {
  14. // Instantiate Notification and set its attributes
  15. Notification n = new Notification();
  16. // 将 message 转成 notification 对象
  17. if(self.getPeerState() == QuorumPeer.ServerState.LOOKING){
  18. // 当前节点状态为 looking,则将外部节点投票添加到 recvqueue 队列中
  19. recvqueue.offer(n);
  20. if((ackstate == QuorumPeer.ServerState.LOOKING)
  21. && (n.electionEpoch < logicalclock.get())){
  22. // 若外部节点选举周期小于当前节点选举周期则发送内部投票
  23. Vote v = getVote();
  24. ToSend notmsg = new ToSend(ToSend.mType.notification,
  25. v.getId(),
  26. v.getZxid(),
  27. logicalclock.get(),
  28. self.getPeerState(),
  29. response.sid,
  30. v.getPeerEpoch());
  31. sendqueue.offer(notmsg);
  32. }
  33. } else {
  34. // 忽略其他状态时的处理
  35. }
  36. }
  37. } catch (InterruptedException e) {
  38. }
  39. }
  40. LOG.info("WorkerReceiver is down");
  41. }

此时我们明白 WorkerReceiver 线程在运行期间会一直从 QuorumCnxManager.recvQueue 的队列中拉取接收到的外部投票信息,若当前节点为 LOOKING 状态,则将外部投票信息添加到 FastLeaderElection.recvqueue 队列中,等待 FastLeaderElection.lookForLeader 选举算法处理投票信息。

</>复制代码

  1. 到此我们基本明白了 ZK 集群节点发送和接收投票的处理流程,但是这个时候您是不是又有一种懵的状态呢 笑哭,我们会发现选举过程中依赖了多个线程 WorkerSender, SendWorker, WorkerReceiver, RecvWorker ,多个阻塞队列 sendqueue, recvqueue,queueSendMap,recvQueue 而且名字起的很类似,更让人懵 ; 不过莫慌,我们来通过下面的图来缕下思路

小结

看了这么长时间的代码,也够累的;最后我们就来个小结吧 :

QuorumCnxManager 类主要职能是负责集群中节点与外部节点进行通信及投票信息的中转

FastLeaderElection 类是选举投票的核心实现

选举投票规则

比较外部投票与内部投票的选举周期值,选举周期大的值优先

若选举周期值一致,则比较事务 ID; 事务 ID 最新的优先

若选举周期值一致且事务 ID 值相同,则比较投票节点的 server id; server id 最大的优先

集群中节点通信时为了避免重复建立连接,遵守一个原则:连接总是由 server id 较大的一方发起

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77695.html

相关文章

  • zookeeper-数据同步源码分析

    摘要:只有数据同步完成之后集群才具备对外提供服务的能力。当节点在选举后角色确认为后将会进入状态,源码如下在节点状态变更为之后会创建实例,并触发过程。 在上一篇对 zookeeper 选举实现分析之后,我们知道 zookeeper 集群在选举结束之后,leader 节点将进入 LEADING 状态,follower 节点将进入 FOLLOWING 状态;此时集群中节点将进行数据同步操作,以保证...

    plus2047 评论0 收藏0
  • Elasticsearch分布式一致性原理剖析(一)-节点篇

    摘要:摘要目前是最流行的开源分布式搜索引擎系统,其使用作为单机存储引擎并提供强大的搜索查询能力。前言分布式一致性原理剖析系列将会对的分布式一致性原理进行详细的剖析,介绍其实现方式原理以及其存在的问题等基于版本。相当于一次正常情况的新节点加入。 摘要: ES目前是最流行的开源分布式搜索引擎系统,其使用Lucene作为单机存储引擎并提供强大的搜索查询能力。学习其搜索原理,则必须了解Lucene,...

    genedna 评论0 收藏0
  • Elasticsearch分布式一致性原理剖析(一)-节点篇

    摘要:摘要目前是最流行的开源分布式搜索引擎系统,其使用作为单机存储引擎并提供强大的搜索查询能力。前言分布式一致性原理剖析系列将会对的分布式一致性原理进行详细的剖析,介绍其实现方式原理以及其存在的问题等基于版本。相当于一次正常情况的新节点加入。 摘要: ES目前是最流行的开源分布式搜索引擎系统,其使用Lucene作为单机存储引擎并提供强大的搜索查询能力。学习其搜索原理,则必须了解Lucene,...

    lindroid 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<