zookeeper核心原理全面解析

  下述各zookeeper机制的java客户端实践参考zookeeper java客户端之curator详解html

  官方文档http://zookeeper.apache.org/doc/current/zookeeperOver.html、http://zookeeper.apache.org/doc/current/zookeeperInternals.html描述了部分关于zk的内部工做机制,可是并不够友好和详细。java

zookeeper简介

  据官网介绍,ZooKeeper是一个用于提供配置信息、命名服务、分布式协调以及分组服务的中心化服务,它们是分布式应用所必需的。从实际应用来看,zookeeper是最普遍使用的分布式协调服务,包括dubbo、kafka、hadoop、es-job等都依赖于zookeeper提供的分布式协调和注册服务。其余用于提供注册服务的中间件还包括consul以及etcd、eureka,但都不及zookeeper普遍。其余适用场景可见https://xie.infoq.cn/article/1dcd3f8b100645e0da782a279node

  官网:https://zookeeper.apache.org/,https://zookeeper.apache.org/doc/r3.4.14/mysql

  zookeeper配置文件详解:https://zookeeper.apache.org/doc/r3.4.14/zookeeperAdmin.html#sc_configuration,也能够参考http://www.javashuo.com/article/p-seepotrk-z.htmlgit

核心机制

zookeeper节点角色

  在zookeeper中,节点分为下列几种角色:github

  • 领导者(leader),负责进行投票的发起和决议,更新系统状态,在Zookeeper集群中,只有一个Leader节点。
  • 学习者(learner),包括跟随者(follower)和观察者(observer)。
  • follower用于接受客户端请求并想客户端返回结果,在选主过程当中参与投票,在Zookeeper集群中,follower能够为多个。
  • Observer能够接受客户端链接,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提升读取速度(不参与投票下降选举的复杂度)

在一个zookeeper集群,各节点之间的交互以下所示:web

 注:几乎全部现代基于分布式架构的中间件都是采用相似作法,例如kafka、es等。redis

 从上可知,全部请求均由客户端发起,它多是本地zkCli或java客户端。 各角色详细职责以下。算法

Leader

  leader的职责包括:sql

    • 恢复数据;
    • 维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型;
      Leader的工做流程简图以下所示,在实际实现中,启动了三个线程来实现功能。

 

Follower

  follower的主要职责为:

  • 向Leader发送请求;
  • 接收Leader的消息并进行处理;
  • 接收Zookeeper Client的请求,若是为写清求,转发给Leader进行处理

  Follower的工做流程简图以下所示,在实际实现中,Follower是经过5个线程来实现功能的。

  各类消息的含义以下: 

PING:心跳消息。
PROPOSAL:Leader发起的提案,要求Follower投票。
COMMIT:服务器端最新一次提案的信息。
UPTODATE:代表同步完成。
REVALIDATE:根据Leader的REVALIDATE结果,关闭待revalidate的session仍是容许其接受消息。
SYNC:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制获得最新的更新。

zookeeper数据存储机制

  虽然zookeeper采用的是文件系统存储机制,可是全部数据数据都存储于内存中。其对外提供的视图相似于Unix文件系统。树的根Znode节点至关于Unix文件系统的根路径。

  

节点类型

  zk中的节点称之为znode(也叫data register,也就是存储数据的文件夹),按其生命周期的长短能够分为持久结点(PERSISTENT)和临时结点(EPHEMERAL);在建立时还可选择是否由Zookeeper服务端在其路径后添加一串序号用来区分同一个父结点下多个结点建立的前后顺序。
  通过组合就有如下4种Znode结点类型:

  • persistent:永久性znode。
  • ephemeral: 随着建立的客户端关闭而自动删除,不过它们仍然对全部客户端可见,ephemeral节点不容许有子节点。是实现分布式协调的核心机制。
  • sequential:附属于上述两类节点,是一种特性。在建立时,zookeeper会在其名字上分配一个序列号。能够做为全局分布式队列使用。以下:

         

zookeeper的一致性保证

  zookeeper经过下列机制实现一致性保证:

  » 全部更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行
  » 数据更新原子性,一次数据更新要么成功,要么失败
  » 全局惟一数据视图,client不管链接到哪一个server,数据视图都是一致的,基于全部写请求所有由leader完成,而后同步实现
  » 实时性,在必定事件范围内,client能读到最新数据

读写机制

» Zookeeper是一个由多个server组成的集群
  » 一个leader,多个follower
  » 每一个server保存一份数据副本
  » 全局数据一致
  » 分布式读写
  » 更新请求所有转发由leader完成,并在成功后同步给follower
客户端写请求的过程以下:

  其过程为:

  • 1.全部的事务请求都交由集群的Leader服务器来处理,Leader服务器会将一个事务请求转换成一个Proposal(提议),并为其生成一个全局递增的惟一ID,这个ID就是事务ID,即ZXID,Leader服务器对Proposal是按其ZXID的前后顺序来进行排序和处理的。
  • 2.以后Leader服务器会将Proposal放入每一个Follower对应的队列中(Leader会为每一个Follower分配一个单独的队列),并以FIFO的方式发送给Follower服务器。
  • 3.Follower服务器接收到事务Proposal后,首先以事务日志的方式写入本地磁盘,而且在成功后返回Leader服务器一个ACK响应。
  • 4.Leader服务器只要收到过半Follower的ACK响应,就会广播一个Commit消息给Follower以通知其进行Proposal的提交,同时Leader自身也会完成Proposal的提交。

  因为每次的请求都须要转发给leader并进行投票处理,因此zookeeper并不适合于写密集型的场景,例如序列产生器、分布式锁,不一样节点数量、不一样读写比例下zk的tps以下:

 

   来源于官方测试。上述测试基于3.2,2Ghz Xeon, 2块SATA 15K RPM硬盘。日志(WAL)在单独的硬盘,快照(zk内存数据快照)写在OS系统盘,读写分别为1K大小,且客户端不直连leader。且从上可知,节点越多、写越慢、读越快,因此通常节点不会不少,可是为了作扩展性和异地,会使用observer节点。 

dataDir=/data

dataLogDir=/datalog

dataLogDir若是没提供的话使用的则是dataDir。zookeeper的持久化都存储在这两个目录里。dataLogDir里是放到的顺序日志(WAL)。而dataDir里放的是内存数据结构的snapshot,便于快速恢复(目前基本上全部带持久化特性的中间件如redis 4.x(kafka采用磁盘append,是个另类)都是借鉴数据库(oracle/mysql也支持buffer_pool/sga的dump)的作法,按期快照+WAL重放,而不是重启后清空来尽量提高性能)。为了达到性能最大化,通常建议把dataDir和dataLogDir分到不一样的磁盘上,这样就能够充分利用磁盘顺序写的特性。以下:

 

 zookeeper快照文件的命名规则为snapshot.**,其中**表示zookeeper触发快照的那个瞬间,提交的最后一个事务的ID。其默认不会清理,从3.4.0开始,zookeeper提供了自动清理snapshot和事务日志的功能,经过配置 autopurge.snapRetainCount 和 autopurge.purgeInterval 这两个参数可以实现定时清理了。这两个参数都是在zoo.cfg中配置的:autopurge.purgeInterval 这个参数指定了清理频率,单位是小时,须要填写一个1或更大的整数,默认是0,表示不开启本身清理功能。autopurge.snapRetainCount 这个参数和上面的参数搭配使用,这个参数指定了须要保留的文件数目。默认是保留3个。

zkid

  znode节点的状态信息中包含czxid, 那么什么是zxid呢? 在zk中,状态的每一次改变, 都对应着一个递增的Transaction id, 该id称为zxid. 因为zxid的递增性质, 若是zxid1小于zxid2, 那么zxid1确定先于zxid2发生.    建立任意节点, 或者更新任意节点的数据, 或者删除任意节点, 都会致使Zookeeper状态发生改变, 从而致使zxid的值增长.

znode节点中的信息

  Znode结构主要由存储于其中的数据信息和状态信息两部分构成,经过get 命令获取一个Znode结点的信息以下:

  第一行存储的是ZNode的数据信息,从cZxid开始就是Znode的状态信息。Znode的状态信息比较多,几个主要的为:

    • czxid:
      即Created ZXID,表示建立该Znode结点的事务ID

    • mzxid:
      即Modified ZXID,表示最后一次更新该结点的事务ID

    • version
      该Znode结点的版本号。每一个Znode结点被建立时版本号都为0,每更新一次都会致使版本号加1,即便更新先后Znode存储的值没有变化版本号也会加1。version值能够形象的理解为Znode结点被更新的次数。Znode状态信息中的版本号信息,使得服务端能够对多个客户端对同一个Znode的更新操做作并发控制。整个过程和java中的CAS有点像,是一种乐观锁的并发控制策略,而version值起到了冲突检测的功能。客户端拿到Znode的version信息,并在更新时附上这个version信息,服务端在更新Znode时必须必须比较客户端的version和Znode的实际version,只有这两个version一致时才会进行修改。

  ZooKeeper默认状况下对数据字段的传输限制为1MB(全部分布式应用几乎默认都这个大小,如kafka、dubbo),该限制为任何节点数据字段的最大可存储字节数,同时也限制了任何父节点能够拥有的子节点数。

zookeeper的其余核心机制

  • Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫作Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步之后,恢复模式就结束了。状态同步保证了leader和Server具备相同的系统状态。
  • 为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。全部的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
  • 每一个Server在工做过程当中有三种状态:
    LOOKING:当前Server不知道leader是谁,正在搜寻
    LEADING:当前Server即为选举出来的leader
    FOLLOWING:leader已经选举出来,当前Server与之同步

分布式系统的一致性实现算法之paxos

  paxos算法基于这样的原理:

  • 在一个分布式数据库系统中,若是各节点的初始状态一致,每一个节点都执行相同的操做序列,那么他们最后能获得一个一致的状态。
  • Paxos算法解决的什么问题呢,解决的就是保证每一个节点执行相同的操做序列。好吧,这还不简单,master维护一个
     全局写队列,全部写操做都必须 放入这个队列编号,那么不管咱们写多少个节点,只要写操做是按编号来的,就能保证一
   致性。没错,就是这样,但是若是master挂了呢。
  • Paxos算法经过投票来对写操做进行全局编号,同一时刻,只有一个写操做被批准,同时并发的写操做要去争取选票,
   只有得到过半数选票的写操做才会被 批准(因此永远只会有一个写操做获得批准),其余的写操做竞争失败只好再发起一
   轮投票,就这样,在日复一日年复一年的投票中,全部写操做都被严格编号排 序。编号严格递增,当一个节点接受了一个
   编号为100的写操做,以后又接受到编号为99的写操做(由于网络延迟等不少不可预见缘由),它立刻能意识到本身 数据
   不一致了,自动中止对外服务并重启同步过程。任何一个节点挂掉都不会影响整个集群的数据一致性(总2n+1台,除非挂掉大于n台)
所以在生产中,要求zookeeper部署3(单机房)或5(单机房或多机房)或7(跨机房)个节点的集群。

zookeeper java官方客户端核心package简介

  • org.apache.zookeeper 包含ZooKeeper客户端主要类,ZooKeeper watch和各类回调接口的定义。
  • org.apache.zookeeper.data 定义了和data register相关的特性
  • org.apache.zookeeper.server, org.apache.zookeeper.server.quorum, org.apache.zookeeper.server.upgrade是服务器实现的核心接口
  • org.apache.zookeeper.client定义了Four Letter Word的主要类

  因为zookeeper的java官方客户端太不友好,所以实际中通常使用三方客户端Curator。故不对zookeeper客户端进行详细分析,参见本文首部对curator的详解。

watch机制

  watch是zookeeper针对节点的一次性观察者机制(即一次触发后就失效,须要手工从新建立watch),行为上相似于数据库的触发器。

  当watch监视的数据发生时,通知设置了该watch的client,客户端即watcher。watcher的机制是监听数据发生了某些变化,因此必定会有对应的事件类型和状态类型,一个客户端能够监控多个节点,在代码中体如今new了几个就产生几个watcher,只要节点变化都会执行一遍process。其示意图以下:

  在zookeeper中,watch是采用推送机制实现的,而不是客户端轮训(有些中间件采用拉的模式,例如kafka消费者,这主要取决于设计者认为的合理性,通常来讲流量很大的适合于拉的模式,这样更好作控制,不然客户端容易失控;反之推的模式)。watch有两种类型的事件可以监听:znode相关的及客户端实例相关的。分别为:

  • 事件类型:(znode节点相关的)【针对的是你所观察的一个节点而言的】
    • EventType.NodeCreated 【节点建立】
    • EventType.NodeDataChanged 【节点数据发生变化】
    • EventType.NodeChildrenChanged 【这个节点的子节点发生变化】
    • EventType.NodeDeleted 【删除当前节点】
  • 状态类型:(是跟客户端实例相关的)【ZooKeeper集群跟应用服务之间的状态的变动】
    • KeeperState.Disconnected 【没有链接上】
    • KeeperState.SyncConnected 【链接上】
    • KeeperState.AuthFailed 【认证失败】
    • KeeperState.Expired 【过时】

  总结起来,zk watch的特性为:

  • 一次性:对于ZooKeeper的watcher,你只须要记住一点,ZooKeeper有watch事件,是一次性触发的,当watch监视的数据发生变化时,通知设置该watch的client,即watcher,因为ZooKeeper的监控都是一次性的,因此每次必须设置监控。在这里,LZ不得不说一句,一次触发实际上是一个特性、并不是设计缺陷,且zk各api已经提供了开关是否继续开启,并无带来不方便,因此把它作缺点是说不过去的。
  • 客户端串行执行:客户端watcher回调的过程是一个串行同步的过程,这为咱们保证了顺序,同时须要开发人员注意一点,千万不要由于一个watcher的处理逻辑影响了整个客户端的watcher回调
  • 轻量:WatchedEvent是ZooKeeper整个Watcher通知机制的最小通知单元,整个结构只包含三个部分:通知状态、事件类型和节点路径。也就是说Watcher通知很是的简单,只会告知客户端发生了事件而不会告知其具体内容,须要客户端本身去进行获取,好比NodeDataChanged事件,ZooKeeper只会通知客户端指定节点的数据发生了变动,而不会直接提供具体的数据内容
  • 客户端设置的每一个监视点与会话关联,若是会话过时,等待中的监视点将会被删除。不过监视点能够跨越不一样服务端的链接而保持,例如,当一个ZooKeeper客户端与一个ZooKeeper服务端的链接断开后链接到集合中的另外一个服务端,客户端会发送未触发的监视点列表,在注册监视点时,服务端将要检查已监视的znode节点在以前注册监视点以后是否已经变化,若是znode节点已经发生变化,一个监视点的事件就会被发送给客户端,不然在新的服务端上注册监视点。这一机制使得咱们能够关心逻辑层会话,而非底层链接自己。

LEADER服务器的选举 

  两种状况下会发生Leader节点的选举,集群初始构建的时候;其次,不管如何,leader老是有可能发生宕机可能的。zookeeper中leader的选举过程为:

集群中的服务器会向其余全部的Follower服务器发送消息,这个消息能够形象化的称之为选票,选票主要由两个信息组成,所推举的Leader服务器的ID(即配置在myid文件中的数字),以及该服务器的事务ID,事务表示对服务器状态变动的操做,一个服务器的事务ID越大,则其数据越新。整个过程以下所述:

  • 1.Follower服务器投出选票(SID,ZXID),第一次每一个Follower都会推选本身为Leader服务器,也就是说每一个Follower第一次投出的选票是本身的服务器ID和事务ID。
  • 2.每一个Follower都会接收到来自于其余Follower的选票,它会基于以下规则从新生成一张选票:比较收到的选票和本身的ZXID的大小,选取其中最大的;若ZXID同样则选取SID即服务器ID最大的。最终每一个服务器都会从新生成一张选票,并将该选票投出去。

  这样通过多轮投票后,若是某一台服务器获得了超过半数的选票,则其将当前选为Leader。由以上分析可知,Zookeeper集群对Leader服务器的选择具备偏向性,偏向于那些ZXID更大,即数据更新的机器。

  整个过程以下图所示:

    因此这里实际上简化了,有一个最后达成一致的细节过程须要进一步阐述(后续补充)。

故障恢复

Zookeeper经过事务日志和数据快照来避免由于服务器故障致使的数据丢失。这一点上全部采用事务机制的存储实现都同样,采用WAL+重放机制实现。

  • 事务日志是指服务器在更新内存数据前先将事务操做以日志的方式写入磁盘,Leader和Follower服务器都会记录事务日志。
  • 数据快照是指周期性经过深度遍历的方式将内存中的树形结构数据转入外存快照中。但要注意这种快照是"模糊"的,由于可能在作快照时内存数据发生了变化。可是由于Zookeeper自己对事务操做进行了幂等性保证,故在将快照加载进内存后会经过执行事务日志的方式来说数据恢复到最新状态。

Zookeeper链接状态管理

  zookeeper的链接状态机以下:

   从上可知,共有5种主要状态。实际上还有NOT_CONNECTED、CONNECTEDREADONLY、ASSOCIATING、RECONNECTED。

  https://curator.apache.org/apidocs/org/apache/curator/framework/state/ConnectionState.html

事务

未完待续。。。

客户端缓存数据管理

未完待续。。。

权限体系

  关于zookeeper的acl认证机制,及相关集成,可参考zookeeper acl认证机制及dubbo、kafka集成、zooviewer/idea zk插件配置

分析zookeeper的事务日志 

  可参见http://www.pianshen.com/article/6006190069/。

web监控工具

https://blog.imaginea.com/monitoring-zookeeper-with-exhibitor/

http://www.javashuo.com/article/p-qenrfpus-nc.html

https://github.com/soabase/exhibitor/wiki/Running-Exhibitor

http://www.javashuo.com/article/p-wlhbcppf-gz.html

zk etcd consul性能测试对比:https://coreos.com/blog/performance-of-etcd.html