zookeeper核心原理全面解析
下述各zookeeper机制的java客户端实践参考zookeeper java客户端之curator详解。
官方文档http://zookeeper.apache.org/doc/current/zookeeperOver.html、http://zookeeper.apache.org/doc/current/zookeeperInternals.html描述了部分关于zk的内部工作机制,但是并不够友好和详细。
zookeeper简介
据官网介绍,ZooKeeper是一个用于提供配置信息、命名服务、分布式协调以及分组服务的中心化服务,它们是分布式应用所必需的。从实际应用来看,zookeeper是最广泛使用的分布式协调服务,包括dubbo、kafka、hadoop、es-job等都依赖于zookeeper提供的分布式协调和注册服务。其他用于提供注册服务的中间件还包括consul以及etcd(云原生标配)、eureka,但都不及zookeeper(java分布式微服务标配)广泛。其他适用场景可见https://xie.infoq.cn/article/1dcd3f8b100645e0da782a279。
官网:https://zookeeper.apache.org/,https://zookeeper.apache.org/doc/r3.4.14/。
zookeeper配置文件详解:https://zookeeper.apache.org/doc/r3.4.14/zookeeperAdmin.html#sc_configuration,也可以参考https://www.cnblogs.com/smail-bao/p/7009633.html
核心机制
zookeeper节点角色
在zookeeper中,节点分为下列几种角色:
- 领导者(leader),负责进行投票的发起和决议,更新系统状态,在Zookeeper集群中,只有一个Leader节点。
- 学习者(learner),包括跟随者(follower)和观察者(observer)。
- follower用于接受客户端请求并想客户端返回结果,在选主过程中参与投票,在Zookeeper集群中,follower可以为多个。
- Observer可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度(不参与投票降低选举的复杂度)
在一个zookeeper集群,各节点之间的交互如下所示:
注:几乎所有现代基于分布式架构的中间件都是采用类似做法,例如kafka、es等。
从上可知,所有请求均由客户端发起,它可能是本地zkCli或java客户端。 各角色详细职责如下。
Leader
leader的职责包括:
- 恢复数据;
- 维持与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节点。
对于读请求,则是直接在连接到的节点进行查询,所以可能会查询到过期的数据(ETCD则是follower会转发到leader处理,所以不会有过期数据)。
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个。
snapCount 快照的数量及生成机制
(Java system property: zookeeper.snapCount)
ZooKeeper logs transactions to a transaction log. After snapCount transactions are written to a log file a snapshot is started and a new transaction log file is created. The default snapCount is 100,000. 如果要加快生成快照的频率,或者定时生成快照,只要给zk发足够数量的写即可。
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(事务id是一个64位的数字,高32位为epoch(代表一次leader选举的变化,选举过程会保证只有一个leader得到了epoch),低32位为计数器counter) -
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),该限制为任何节点数据字段的最大可存储字节数,同时也限制了任何父节点可以拥有的子节点数。
zk事务日志和快照内容的分析
zk提供了两个Formatter供可视化内容(其中会话信息也会进入快照):参见https://www.cnblogs.com/softlin/p/4819756.html。
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/
https://www.cnblogs.com/yinzhengjie/p/10753869.html
https://github.com/soabase/exhibitor/wiki/Running-Exhibitor
https://segmentfault.com/a/1190000017809150
redis、zk etcd consul性能测试对比:https://coreos.com/blog/performance-of-etcd.html、https://juejin.cn/post/6992916118058827813、https://blog.crazytaxii.com/posts/performance_optimization_of_etcd/