zookeeper是分布式协调系统,用来协调、同步多服务器之间的状态,容错能力强

一个应用要保证HA,往往需要N个服务器(N>1)提供服务,其中有M台master,N-M台slave。这样一台挂了,另外N-1台也能提供服务。所以,数据也会备份成N份散布在这些服务器上。现在的问题变成了,如何管理这N台服务器?如何在master节点失败的时候重新选择master?如何保证所有服务器存储的备份数据一致?

如果你碰到上面那些棘手的问题,zookeeper刚好可以帮到你:

  • 存储公共配置信息

  • 跟踪所有进程运行状态

  • 集群中各节点的管理

  • master选主

  • 提供分布式同步支持(分布式锁等功能)

上面列出的是官方提供的功能支持,还有很多其他功能等待挖掘。

当你的系统依赖zookeeper保证HA和一致性的时候,你肯定也好奇,zookeeper本身是如何保证这两个特性的。幕后功臣往往容易被忽视,其实它就是Zookeeper原子广播协议(Zab协议)。如果你了解过二阶段提交、paxos算法、raft算法等大名鼎鼎的分布式一致性算法,Zab肯定也不会陌生,因为他们要达到的目的相同,就是保证应用程序的HA和一致性。

为了读懂后面的内容,有些术语需要了解:

Zab的理论和实现并不完全一致。

实现是在理论的基础之上做了一些优化手段,这些优化是在zookeeper不断发展的过程中给加上的。我接下来的讲解,也是参照这篇论文,以zookeeper 3.3.3的版本的实现为准,并用自己的语言总结。

理论中的协议
随着系统启动或者恢复,会经历Zab协议中描述的如下四个阶段

  • 阶段0:Leader选举。每个peer从Quorum peer中选出自己心中的预备leader

  • 阶段1:发现。预备leader从Quorum Follower中发现最新的数据,并覆盖自己的过期数据

  • 阶段2:同步。预备leader采用二阶段提交的方式将自己的最新数据同步给Quorum Follower。完成这个步骤,预备leader就转为正式leader

  • 阶段3:广播。Leader接受写请求,并通过二阶段提交的方式广播给Quorum Follower

之前我只是简述了一下理论中的协议,然而理想很骨感,有很多需要改进或者妥协的地方。下面我会一一阐明:

  • 阶段0的选举leader实际上很简陋,只是“看对眼”了就选为预备leader,所以预备leader的数据可能并非最新

  • 预备leader数据过期,就需要用阶段1来弥补,通过互相传输数据,来发现最新的数据,并完成预备leader的数据升级

  • 更多的网络间数据传输代表了更大的网络开销

协议实现
了解了理想的骨感之后,我们回归现实。

真正apache zookeeper在实现上提出设想:优化leader选举,直接选出最新的Peer作为预备Leader,这样就能将阶段0和阶段1合并,减少网络上的开销和多流程的复杂性

由图可知,代码实现上,协议被简化为三个阶段

  • 快速选举Leader阶段:从Quorum Peer中选出数据最新的peer作为leader

  • 恢复阶段:Leader将数据同步给Quorum Follower

  • 广播阶段:Leader接受写请求,并广播给Quorum Follower

Talk is cheap.Show me the Code

这时Linus说过的一句话,无论语言多么有力,只有代码才能真正展现作者的思想。之前我只是对是协议实现的三个阶段做了一番简述,只有代码才能真正拨开Zab协议外面那层迷雾。(为了不浪费篇幅,这里采用伪代码)

快速选举Leader阶段(FLE)

首先初始化一些数据

  • ReceivedVotes:投票箱,存储投票节点以及当前投票信息

  • OutOfElection:存储已经成为Leader、Follower,不参与投票的节点信息,以及它的历史投票信息

  • 在选票中写上自己的大名

  • send notification:发起选票通知,该节点会携带选票,进入目标节点的队列中,相当于给自己投了一票,并毛遂自荐给其他人

如果当前节点处于选举状态,则它也会接到选票通知。它会从队列中不断轮询节点,以便获取选票信息(如果超时,则不断放松超时时间,直至上限)。根据轮询出来的发送节点的状态,来做相应的处理。

  • election:如果发送节点的轮次(round)大于自己,说明自己的选举信息过时,则更新自己的选举轮次,清空投票箱,更新自己的选票内容,并将新的选票通知给其他节点;如果发送节点的轮次等于自己,并且投票内容比自己的更新,则只需要更新自己的选票,并通知给其他节点就行;如果发送节点的轮次小于自己,说明投票内容过期,没有参考意义,直接忽略。所有未被忽略的选票,都会进入投票箱。最终根据选票箱中的结果,判断当前节点的选票是不是占大多数,如果是就根据当前节点的选票选出Leader

  • leading或following:发送节点轮次等于自己,说明发送节点还参与投票,如果发送节点是Leading或者它的选票在选票箱中占大多数,则直接完成选举;如果发送节点已经完成选举(轮次不同)或者它收集的选票较少,那么它的信息都会存放在OutOfElection中。当节点不断完成选举,OutOfElection中数量逐渐变成Quorum时,就把OutOfElection当做投票箱,从中检查发送节点的选票是否占多数,如果是就直接选出Leader

恢复阶段

经过FLE,已经选出了日志中具有最新已提交的事务的节点作为预备Leader。下面就分Leader和Follower两个视角来介绍具体实现。

Leader视角
首先,更新lastZxid,将纪元+1,计数清零,宣布改朝换代啦。然后在每次接收到Follower的数据同步请求时,都会将自己lastZxid反馈回去,表示所有Follower以自己的lastZxid为准。接下来,根据具体情况来判断该如何将数据同步给Follower

  • 如果Leader的历史提交事务比Follower的最新事务要新,说明Follower的数据有待更新。更新方式取决于,Leader最早事务有没有比Follower最新事务要新:如果前者更新,说明在Leader看来Follower所有记录的事务都太过陈旧,没有保留价值,这时只需要将Leader所有history发给Follower就行(响应SNAP);如果后者更新,说明在Leader看来,Follower从自己的lastZxid开始到Leader日志的最新事务,都需要同步,于是将这一部分截取并发送给Follower(响应DIFF)

  • 如果Leader的历史提交事务没有Follower的最新事务新,说明Follower存在没有提交的事务,这些事务都应该被丢弃(响应TRUNC)

当Follower完成同步时,会发送同步ack,当Leader收到Quorum ack时,表示数据同步阶段大功告成,进入最后的广播阶段。

Follower视角
通知Leader,表示自己希望能同步Leader中的数据。

  • 当收到Leader的拒绝响应时,说明Leader不承认自己作为Follower,有可能该Leader并不可靠,于是开始重新开始FLE

  • 当收到SNAP或DIFF响应时,Follower会同步Leader发送过来的事务

  • 当收到TRUNC响应,Follower会丢弃所有未完成的数据

当每个Follower完成上述的同步过程时,会发送ack给Leader,并进入广播阶段。

广播阶段

进入到这个阶段,说明所有数据完成同步,Leader已经转正。开始zookeper最常见的工作流程:广播。

广播阶段是真正接受事务请求(写请求)的阶段,也代表了zookeeper正常工作阶段。所有节点都能接受客户端的写请求,但是Follower会转发给Leader,只有Leader才能将这些请求转化成事务,广播出去。这个节点一样有两个角色,下面还是按照这两个角色来讲解。

Leader视角:

  • Leader必须经过ready,才能接受写请求。完成ready的Leader不断接受写请求,转化成事务请求,广播给Quorum Follower。

  • 当Leader接收到ack时,说明Follower完成相应处理,Leader广播提交请求,Follower完成提交。

  • 当发现新Peer请求作为Follower加入时,将自己的纪元、事务日志发送给该Peer,以便它完成上述恢复阶段的过程。收到该Peer的同步完成的ack时,Leader会发送提交请求,以便Peer提交所有同步完成的事务。这时,该Peer转正为Follower,被Leader纳入Quorum Follower中。

Follower视角:

*Follower被发现是Leading状态,则执行ready过程,用来接受写请求。

  • 当接受到Leader广播过来的事务请求时,Follower会将事务记录在history,并响应ack。

  • 当接收到Leader广播过来的提交请求时,Follower会检查history中有没有尚未提交的事务,如果有,需要等待之前的事务按照FIFO顺序提交之后,才能提交本事务。

这篇文章没有介绍Zookeeper的使用,而是着重讲解它的核心协议Zab的实现。正如文中提及,Zab最早的设想和现在的实现并不相同,今日的实现是在Zookeeper不断发展壮大的过程中不断优化、改进而来的,也许早期的实现就是yahoo论文中构想的那样。罗马不是一日建成,任何人都不能指望一口吃个大胖子。如果Zookeeper刚开始就想着如何优化到极致,那反而会严重影响到这个项目本身的价值,因为它很可能还没面试就被淘汰。

We should forget about small e ciencies, say about 97% of the time: premature optimization is the root of all evil.

通过本文可以看出,过早优化是万恶之源。但是同时,一个好的程序员也不会忘记需要优化的那部分,他会定位相应的代码,然后针对性的修改。这也是zookeeper的开发者所做的。

©著作权归作者所有:来自51CTO博客作者mob604756f06ed8的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. java集合【8】——— List源码详细分析
  2. MongoDB全自动容灾集群节点角色解析
  3. 干货丨DolphinDB集群如何扩展节点和存储
  4. 基于数组或链表实现Map
  5. 【DB宝44】Oracle rac集群中的IP类型简介
  6. 2021-03-21:给定一棵二叉树的头节点head,求以head为头的树中,最小深
  7. 浅析ElasticSearch原理
  8. MySQL 8.0 常用语句、主从及MHA配置文件调整
  9. 容器技术|Docker三剑客之docker-swarm

随机推荐

  1. [原]Android应用程序进程启动过程的源代
  2. Android快速开发,十个最常用的框架
  3. 在android的eclipse开发环境中配置git环
  4. [置顶] Mc小冰总结的Android开发工程师面
  5. Android 基础——第一行代码读书笔记(1)
  6. Ubuntu Android开发环境配置
  7. android开发工具合集(Zipalign)
  8. Android Sqlite 数据库—基础篇
  9. android平台的三个编译命令----make,mm,m
  10. Android微件(续)