zookeeper知识结构

之前写过关于zookeeper的一篇文章《zookeeper-paxos》,paxos太难理解了,当时理解了,但现在又忘记了,机械学习果然是不行的

虽然曾经有一篇文章讲阿里不使用zk做服务发现,但大多数公司的分布式架构中基本都能看到zk的身影,而且他躲在里面,你可能看不到他,感受不到他的存在

对于架构体系中的这样一位选手,了解,学习,研究是相当有必要的

ZK是什么

ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby 一个开源的实现

ZooKeeper 是集群的管理者,监视着集群中各节点的状态,根据节点提交的反馈进行下 一步合理的操作。最终,将简单易用的接口和功能稳定,性能高效的系统提供给用户

zooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services

这大概描述了Zookeeper的作用,配置管理,名字服务,提供分布式同步以及集群管理

为什么需要ZK

知道了zk的定义,其实跟不知道差不多,还是要追根溯源,看看zk今世因缘,存在的意义

zk的历史很多地方都有介绍,这儿就不赘述了

相对历史,更想知道为什么需要zk?

以前经历的系统,都是使用redis做为服务中心了,不管使用single redis,还是redis cluster,能胜任架构需求,全局命名服务、订阅发布监听服务列表、分布式锁足矣

一度怀疑zk的价值,也阻碍了进一步学习的热情,但存在即合理

设计 ZooKeeper 的目的是为了减轻分布式应用程序所承担的协调任务

还是从ZK的定义追溯它的作用

配置管理

在我们的应用中除了代码外,还有一些就是各种配置。比如数据库连接等

一般我们都是使用配置文件的方式,在代码中引入这些配置文件。但是当我们只有一种配置,只有一台服务器,并且不经常修改的时候,使用配置文件是一个很好的做法,但是如果我们配置非常多,有很多服务器都需要这个配置,而且还可能是动态的话使用配置文件就不是个好主意了。

这个时候往往需要寻找一种集中管理配置的方法,我们在这个集中的地方修改了配置,所有对这个配置感兴趣的都可以获得变更。比如我们可以把配置放在数据库里,然后所有需要配置的服务都去这个数据库读取配置

但是,因为很多服务的正常运行都非常依赖这个配置,所以需要这个集中提供配置服务的服务具备很高的可靠性。

一般我们可以用一个集群来提供这个配置服务,但是用集群提升可靠性,那如何保证配置在集群中的一致性呢? 这个时候就需要使用一种实现了一致性协议的服务了。Zookeeper就是这种服务,它使用Zab这种一致性协议来提供一致性

名字服务

比如为了通过网络访问一个系统,我们得知道对方的IP地址,但是IP地址对人非常不友好,这个时候我们就需要使用域名来访问。

但是计算机是不能是别域名的。

怎么办呢?

如果我们每台机器里都备有一份域名到IP地址的映射,这个倒是能解决一部分问题,但是如果域名对应的IP发生变化了又该怎么办呢?于是我们有了DNS这个东西。

我们只需要访问一个大家熟知的(known)的点,它就会告诉你这个域名对应的IP是什么。

在我们的应用中也会存在很多这类问题,特别是在我们的服务特别多的时候,如果我们在本地保存服务的地址的时候将非常不方便,但是如果我们只需要访问一个大家都熟知的访问点,这里提供统一的入口,那么维护起来将方便得多了

分布式锁

比如在一个分布式环境中,为了提高可靠性,我们的集群的每台服务器上都部署着同样的服务

但是,一件事情如果集群中的每个服务器都进行的话,那相互之间就要协调,编程起来将非常复杂。而如果我们只让一个服务进行操作,那又存在单点。通常还有一种做法就是使用分布式锁,在某个时刻只让一个服务去干活,当这台服务出问题的时候锁释放,立即fail over到另外的服务

这在很多分布式系统中都是这么做,这种设计有一个更好听的名字叫Leader Election(leader选举)。比如HBase的Master就是采用这种机制。但要注意的是分布式锁跟同一个进程的锁还是有区别的,所以使用的时候要比同一个进程里的锁更谨慎的使用

这儿其实说了两个作用

  1. 传统意义的锁,如《剖析分布式锁》,保护对共享资料操作
  2. master选举,像JOB,为了高可用,会有多台服务器部署同一套JOB程序,但在运行时,只有一台服务器真正执行业务,此时,需要选择一台服务器,如果这台机器挂了,别的机器需要顶替上来

集群管理

在分布式的集群中,经常会由于各种原因,比如硬件故障,软件故障,网络问题,有些节点会进进出出。有新的节点加入进来,也有老的节点退出集群。这个时候,集群中其他机器需要感知到这种变化,然后根据这种变化做出对应的决策

比如我们是一个分布式存储系统,有一个中央控制节点负责存储的分配,当有新的存储进来的时候我们要根据现在集群目前的状态来分配存储节点

这个时候我们就需要动态感知到集群目前的状态,这也就是注册中心

CAP

分布式系统在设计时,都会考虑一下CAP,在现有理论下,CAP是不能同时满足的,所以需要根据业务场景选择合适的设计要求

CAP定义在《zookeeper-paxos》中有详细说明

ZooKeeper是个CP(一致性+分区容错性)的,即任何时刻对ZooKeeper的访问请求能得到一致的数据结果,同时系统对网络分割具备容错性;但是它不能保证每次服务请求的可用性。也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果。

ZooKeeper是分布式协调服务,它的职责是保证数据在其管辖下的所有服务之间保持同步、一致;所以就不难理解为什么ZooKeeper被设计成CP而不是AP特性的了

而且, 作为ZooKeeper的核心实现算法Zab,就是解决了分布式系统下数据如何在多个服务之间保持同步问题的

特点

  • 顺序一致性:从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
  • 原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
  • 单一系统映像:无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
  • 可靠性:一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。

znode

在谈到分布式的时候,我们通常说的“节点”是指组成集群的每一台机器
然而,在 ZooKeeper 中,“节点”分为两类:

  1. 第一类同样是指构成集群的机器,我们称之为机器节点。
  2. 第二类则是指数据模型中的数据单元,我们称之为数据节点一ZNode

与Linux文件系统不同的是,Linux文件系统有目录和文件的区别,而Zookeeper的数据节点称为ZNode,ZNode是Zookeeper中数据的最小单元,每个ZNode都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,称为树

  • 每一个znode默认能够存储1MB的数据(对于记录状态性质的数据来说,够了)

  • 可以使用zkCli命令,登录到zookeeper上,并通过ls、create、delete、sync等命令操作这些znode节点

znode除了名称、数据以外,还有一套属性:zxid

ZooKeeper状态的每一次改变, 都对应着一个递增的Transaction id, 该id称为zxid. 由于zxid的递增性质, 如果zxid1小于zxid2, 那么zxid1肯定先于zxid2发生

创建任意节点, 或者更新任意节点的数据, 或者删除任意节点, 都会导致Zookeeper状态发生改变, 从而导致zxid的值增加

此外,znode还有操作权限。如果我们把以上几类属性细化,又可以得到以下属性的细节:

  • czxid:创建节点的事务的zxid
  • mzxid:对znode最近修改的zxid
  • ctime:以距离时间原点(epoch)的毫秒数表示的znode创建时间
  • mtime:以距离时间原点(epoch)的毫秒数表示的znode最近修改时间
  • version:znode数据的修改次数
  • cversion:znode子节点修改次数
  • aversion:znode的ACL修改次数
  • ephemeralOwner:如果znode是临时节点,则指示节点所有者的会话ID;如果不是临时节点,则为零。
  • dataLength:znode数据长度。
  • numChildren:znode子节点个数。

znode是由客户端创建的,它和创建它的客户端的内在联系,决定了它的存在性:

  1. PERSISTENT-持久化节点:创建这个节点的客户端在与zookeeper服务的连接断开后,这个节点也不会被删除(除非您使用API强制删除)。
  2. PERSISTENT_SEQUENTIAL-持久化顺序编号节点:当客户端请求创建这个节点A后,zookeeper会根据parent-znode的zxid状态,为这个A节点编写一个全目录唯一的编号(这个编号只会一直增长)。当客户端与zookeeper服务的连接断开后,这个节点也不会被删除。
  3. EPHEMERAL-临时目录节点:创建这个节点的客户端在与zookeeper服务的连接断开后,这个节点(还有涉及到的子节点)就会被删除。
  4. EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点:当客户端请求创建这个节点A后,zookeeper会根据parent-znode的zxid状态,为这个A节点编写一个全目录唯一的编号(这个编号只会一直增长)。当创建这个节点的客户端与zookeeper服务的连接断开后,这个节点被删除。

另外,无论是EPHEMERAL还是EPHEMERAL_SEQUENTIAL节点类型,在zookeeper的client异常终止后,节点也会被删除。

服务的四种状态

服务器具有四种状态,分别是LOOKING,FOLLOWING,LEADING,OBSERVING

  • LOOKING
    寻找leader状态
    当前服务器处于该状态时,它会认为当前集群中没有leader,因此需要进入leader选举状态
  • FOLLOWING
    跟随者状态
    表示当前服务器的角色是Follower角色
  • LEADING
    领导者状态
    表示当前服务器是Leader
  • OBSERVING
    观察者状态
    表示当前服务器角色是Observer


角色

最典型集群模式:Master/Slave 模式(主备模式)

在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务

zookeeper都是集群形式部署的,而zk服务又分为不同角色来执行不同的任务,ZooKeeper中没有选择传统的 Master/Slave 概念

而是引入了Leader、Follower 和 Observer 三种角色

在区分zk服务器角色之前,需要解释几个概念:

  • 事务请求
    在zk中,那些会改变服务器状态的请求称为事务请求(创建节点、更新数据、删除节点、创建会话等等)
  • 非事务请求
    从zk读取数据但是不对状态进行任何修改的请求称为非事务请求

领导者Leader

  1. 事务请求的唯一调度和处理者,保证集群事务处理的顺序性;
  2. 集群内部各服务器的调度者
  3. 只有一个

跟随者(Follower)

  1. 处理客户端非事务请求,转发事务请求给Leader服务器
  2. 参与事务请求Proposal的投票
  3. 参与Leader选举的投票

观察者(Observer):

  1. Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程
  2. 也不参与写操作的“过半写成功”策略,因此 Observer
  3. 机器可以在不影响写性能的情况下提升集群的读性能

数据流

  1. 在Client向Follwer发出一个写的请求
  2. Follwer把请求发送给Leader
  3. Leader接收到以后开始发起投票并通知Follwer进行投票
  4. Follwer把投票结果发送给Leader
  5. Leader将结果汇总后如果需要写入,则开始写入同时把写入操作通知给Leader,然后commit;
  6. Follwer把请求结果返回给Client

leader选举

选举(election)是分布式系统实践中常见的问题,通过打破节点间的对等关系,选得的leader(或叫master、coordinator)有助于实现事务原子性、提升决议效率

为什么需要选举

集群本身有很多种类,如tomcat集群,集群里面每一台机器是对等的,所以其自身不存在leader之说

另外一类,如fastDfs,其依赖于独特的HASH算法,建立文件名和路径之间的映射关系,写操作都是通过namenode分发到各台datanode之上,算法保证了文件名的独一无二,也不存在leader的说法

还有memcache集群,集群里面的机器之间彼此无心跳,通过一致性hash尽可能将key值的存储分散化,降低单一memcahe服务器down机的影响。

还有一类是主从复制,主节点负责写,从节点负责读,提高读的性能。从节点定期通过心跳与主节点沟通,一旦主节点挂掉了,从节点马上接手主节点的任务

对于分布式应用,难以避免出现网络的抖动。比如,
主节点暂时失去响应,如瞬时负载过高,网络拥塞或者其他原因导致主节点暂时失去响应,超过响应超时时间,这个时候从节点启动,承担起leader的职责,但是原先的主节点又恢复了服务。这个时候,如果没有选举机制(不能仅仅自己宣告自己是leader,还要广而告之,让其他服务器或者客户端知道自己是leader),有可能会存在两个leader节点,导致集群发生混乱

上图示例一下此类场景

主节点出现问题,那就是单点故障

单点故障

传统方式是采用一个备用节点,这个备用节点定期给当前主节点发送ping包,主节点收到ping包以后向备用节点发送回复Ack,当备用节点收到回复的时候就会认为当前主节点还活着,让他继续提供服务

当主节点挂了,这时候备用节点收不到回复了,然后他就认为主节点挂了接替他成为主节点如下图

但是这种方式就是有一个隐患,就是网络问题,来看一网络问题会造成什么后果,如下图

也就是说我们的主节点的并没有挂,只是在回复的时候网络发生故障,这样我们的备用节点同样收不到回复,就会认为主节点挂了,然后备用节点将他的Master实例启动起来,

这样我们的分布式系统当中就有了两个主节点也就是—双Master

出现Master以后我们的从节点就会将它所做的事一部分汇报给了主节点,一部分汇报给了从节点,这样服务就全乱了

选举

场景

在哪些场景下需要进行leader选举

  1. 服务器初始化启动
  2. 服务器运行期间无法和Leader保持连接

初始化时

若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下

  • 1.每个Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。
  • 2.接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器
  • 3.处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下
    • 3.1.优先检查ZXID。ZXID比较大的服务器优先作为Leader
    • 3.2.如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器

对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。

  • 4.统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了Leader。
  • 5.改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。

Leader挂掉

在Zookeeper运行期间,Leader与非Leader服务器各司其职,即便当有非Leader服务器宕机或新加入,此时也不会影响Leader,但是一旦Leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮Leader选举,其过程和启动时期的Leader选举过程基本一致。假设正在运行的有Server1、Server2、Server3三台服务器,当前Leader是Server2,若某一时刻Leader挂了,此时便开始Leader选举。选举过程如下

  • 1.变更状态。Leader挂后,余下的非Observer服务器都会讲自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程
  • 2.每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;在第一轮投票中,Server1和Server3都会投自己,产生投票(1, 123),(3, 122),然后各自将投票发送给集群中所有机器。
  • 3.接收来自各个服务器的投票。与启动时过程相同。
  • 4.处理投票。与启动时过程相同,此时,Server1将会成为Leader。
  • 5.统计投票。与启动时过程相同。
  • 6.改变服务器的状态。与启动时过程相同

过程

理想状态

  1. 在第一轮中,按照“我最牛逼,我怕谁”的原则,每个节点都推荐它自己为集群的leader节点
  2. 按照我们假设的理想条件,节点S1首先收到了S2发送来的推荐者“2”,节点S1发现“2”要比它之前推荐的“1”(也就是它自己)牛。根据谁牛推荐谁的原则,“S1”清空自己的票箱,重新选举“2”(注意,此时“S1”的新票箱中已经有两票选举“2”了,一票是它自己,另外一票是”S2”,并且所有节点都是Looking状态)
  3. 同样的事情发生在“S2”身上:”S2”收到了”S3”发过来的推荐信息,发现“3”这个被推举者比之前自己推举的“2”要牛,于是也清空自己的票箱,发起一轮新的投票,此时“S2”选举“3”。依次类推”S3”、”S4”
  4. 这里要注意S5这个节点,在第一轮接受到了来源于“S1”——“S4”的推举者(一定注意,每一次接受信息,都会广播一次“我坚持推举的人”),发现“还是推荐的5最牛”,于是“我继续推举S5吧”
  5. 以上这个过程在整个理想的网络环境上一直持续。到了第四轮,“S1”收到了“S2”发送来的推举者“5”,发现“5”要比当前“S1”推荐的“4”要牛。所以“S1”清空了自己的票箱,重新推举“5”(发送给其他所有节点)
  6. 关键的第五轮来了,我们再重复一下,经过之前的选举,现在“S2”——“S5”都已经推举“5”为Leader了,而且都处于第四轮。这时他们收到了”S1”发来的新的“第五轮”投票,于是都和之前一样,做相同的一件事:清空自己的票箱,重新向其他所有节点广播自己的第五轮投票“5”
  7. 于是,节点X,收到了大于N / 2 +1的选举“5”的投票,且都是第五轮投票。这样每个节点就都知道了自己的角色。,选举结束。所有将成为Follower状态的节点,向将要成为Leader的节点发起最后一次“工作是否正常”的询问。得到肯定的ack后,整个集群的工作状态就确认了

非理想状态

过程中出现了宕机、网络延迟、网络物理层断开等情况

在第三轮的选举过程后,“S1”,“S2”两个节点就断开了,他们的投票信息根本没有发送出去

  • “S3”收到了“S4”,“S5”发来的投票信息,这时“S3”的票箱处于第3轮,并且发现了占大多数的投票结果:大家选举“S5”为Leader节点
  • 同样的事情也发生在“S4”身上。这样“S3”,“S4”两个节点率先知道了投票结果,在最后一次询问Leader节点是否能正常工作,并得到了肯定的ACK之后,“S3”,“S4”两个节点变成了Follower状态
  • 之后,无论“S3”,“S4”两个节点收到了任何节点的投票信息,都直接向源节点反馈投票结果,不会再进行投票了。
  • 在投票完成后,“S1”,“S2”重新连入后,虽然他们发起了投票,但是不会再收到投票反馈了。直接根据“S3”或者“S4”发来的结果状态,变成Follower状态

总结

此篇zookeeper的基础内容基本都包含了

了解这些基本已经入门

下一篇学习其中最核心的zab协议,理解原子性广播概念,以及zab的实现过程

参考资料

面试问题,请说明zookeeper的选举机制

zookeeper选举机制

Zookeeper的Leader选举

公众号:码农戏码
欢迎关注微信公众号『码农戏码』