ZooKeeper
设计目的
zk的设计是为了解决分布式服务领域的问题。
- 最终一致性:client不论连接到哪个Server,展示给它的都是同一个视图。
- 可靠性:具有简单、健壮、良好的性能、如果消息被到一台服务器接收,那么消息将被所有服务器接收。
- 实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息
- 原子性:更新只能成功或者失败,没有中间状态。
- 顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息a在消息b前被同一个发送者发布,a必将排在b前面。
应用场景
- 配置管理
- DNS服务
- 组成员管理
- 各种分布式锁
不适合的场景:
- 大数据量存储
ZooKeeper架构

- 每个server的数据都是一样的,Client的读请求可以请求任意一个Server。
- ZooKeeper启动时,将从实例中选举一个leader(Paxos协议)。
- Leader负责处理数据更新等操作(ZAB协议)。(ZooKeeper Atomic Broadcast protocol)
- 一个更新操作成功,当且仅当大多数Server在内存中成功修改 。
在zookeeper的集群中,各个节点共有下面3种角色和4种状态:
角色:leader、follower、observer
状态:
- LOOKING:当前Server不知道leader是谁,正在搜寻。
- LEADING:当前Server即为选举出来的leader。
- FOLLOWING:leader已经选举出来,当前Server与之同步。
- OBSERVING:observer的行为在大多数情况下与follower完全一致,但是他们不参加选举和投票,而仅仅接受(observing)选举和投票的结果。
session
session是zk中非常重要的一个概念。zk客户端和zk集群的某个节点直接建立一个session。客户端可以主动关闭session。如果在一定的timeout时间内,客户端没有想zk集群发送消息,zk集群可以主动断开连接。
zk客户端发现可一个zk集群节点连接失败后,会自动同其他节点建立连接。
数据一致性
zk集群中,只有leader节点可以处理写请求。follower节点接收到写请求,会转发给leader处理。
先到达leader的写请求先被处理
zk处理写请求时序图。
节点2是leader

Observer
zk集群中,除了leader和follew之外,还有一种observer角色。
Observer不参加ZooKeeper的事务提交和选举。只是被动的接收leader的通知。所以通过observer节点来提高整个集群的性能。

数据存储

zxid:每一个对zk datatree的修改都会作为一个事务执行。每个事务都有一个id,就是zxid。zxid是递增的。
zxid是一个8Byte的整数,即java的long型。8个字节分两部分。高四个字节保存的是epoch。低4个字节保存的是递增计数。
epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。
epoch文件只有在集群模式下才会产生。accepedEpoch和currentEpoch
epoch标识了当前Leader周期,集群机器相互通信时,会带上这个epoch以确保彼此在同一个Leader周期中
事务日志
zk将所有的变更存到日志文件里。zk在DataDir下创建version-2目录,下面会存放log文件和snapshot文件。
zk提供的一个查看log的工具:zkTxnLogToolkit.sh
数据快照
数据快照是Zookeeper数据存储中非常核心的运行机制,数据快照用来记录Zookeeper服务器上某一时刻的全量内存数据内容,并将其写入指定的磁盘文件中。
zk生成快照文件的时机:
重启
使用snapCount参数来配置每次数据快照之间的事务操作次数,即ZooKeeper会在snapCount次事务日志记录后进行一个数据快照。
但实际上,数据快照对于ZooKeeper所在机器的整体性能的影响,需要尽量避免ZooKeeper集群中的所有机器在同一时刻进行数据快照。因此ZooKeeper在具体的实现中,并不是严格的按照这个策略执行的,而是采取“过半随机”策略。
一个单独的异步线程来进行数据快照。
zk查看你快照文件,可以通过一个zk的类:org.apache.zookeeper.server.SnapshotFormatter
数据模型
zk的数据模型是树结构的层次模型。层次模型和KV模型是两种主流的数据模型。层次模型常见于文件系统。Zk选择使用文件系统模型。
- 文件系统的树形结构,方便表达数据之间的层次关系
- 文件系统的树形结构可以位不通应用分配独立的命名空间
zk的树形结构成为data tree。data tree的每个节点称为znode。和文件系统不一样,每个znode都是可以存储数据的。每个znode都有一个版本version,version从0开始。znode中的数据可以有多个版本,比如某一个znode下存有多个数据版本,那么查询这个路径下的数据需带上版本信息。

zk是一个内存数据库。在内存里保存了整棵树的内容,Zookeeper会定时将这个数据存储到磁盘上。
zk是是使用CurrentHashMap保存这颗树,map的key就是路径
ZNode的类型
znode可以分为持久性(persistent)的和临时性(ephemeral)的。
- 持久性的。这种znode在创建之后,即使zk集群宕机或者client宕机都不会丢失
- 临时性的。这种znode在client宕机或者client在指定的timeout时间内没有想zk发送消息,这种node就会消失。
znode还可以设置位顺序性和非顺序性的。顺序性的znode会关联一个唯一的单调递增整数。这个整数会作为znode名字的后缀。
以上两两组合就可以出来4中znode
Zookeeper原生API
watch机制
watch是zk中非常好用的机制。客户端在读取一个数据时,可以同时给这个数据设置一个watch。当这个数据有变化时,客户端就会收到一个事件通知。这样就避免了客户端不断轮询来查询最新的数据。
zk的watch采用了一种推拉结合的模式。一旦服务端感知数据变了,那么只会发送一个事件类型和节点信息给关注的客户端,而不会包括具体的变更内容,所以事件本身是轻量级的,这就是所谓的”推”部分,然后收到变更通知的客户端需要自己去拉变更的数据,这就是”拉”部分。
注意,watch是一次性的。
条件更新
对数据的更新操作,可以设置version,就行有条件更新。即只有客户端请求的version和实际数据的version相同时,才进行更新操作。否则不能操作。
如果客户端的请求中将version设置为-1,表示无条件更新。
multi操作
zk提供了一种multi api。可以把多个对znode的操作作为一个事务进行提交,要么全部成功,要么全部失败。
实现分布式队列
使用持久有序节点
实现分布式锁
使用临时有序节点
注意解决羊群效应:watch前一个锁请求,而不是watch锁本身
这样是一个公平锁
性能比较低,代码里取出所有节点,排序,查看前面是否有锁请求,有则watch,否则获取锁。这整个过程是加锁进行的,所以效率比较慢。
实现分布式选举
使用临时顺序znode
设计上和分布式锁很像。
Apache Curator
简化zookeeper代码的开发。
有很多写好的分布式服务可以使用
服务发现
curator有一个扩展curator-x-discovery,基于zk实现了服务发现。
基本设计:一个服务注册的根目录,服务目录,服务实例(临时节点)
container 节点
container节点是zk的一种特殊节点。引入他的目的是为了下挂子节点。当子节点都被删除时,这个container节点会被zk自动删除。
服务注册的根目录和服务目录都是container类型节点。
Zookeeper的运维
最重要的三个配置项
- clientPort
- dataDir。zk保存的是数据的一个个快照文件
- dataLogDir。保存事务日志文件的目录。zk在提交一个事务之前,必须保证事务日志的落盘。
硬件要求,zk应分配一个独占的服务器
- 内存,zk需要在内存中保存data tree。一般data tree也不会特别大。8G以上。
- CPU。zk不是计算密集型,2核以上
- 存储。存储设备的写延迟严重影响事务提交的效率。所以建议给dataLogDir分配一个独占的SSD盘
ZooKeeper常用命令
Zookeeper服务端命令
启动ZK服务: sh bin/zkServer.sh start
查看ZK服务状态: sh bin/zkServer.sh status
停止ZK服务: sh bin/zkServer.sh stop
重启ZK服务: sh bin/zkServer.sh restart
Zookeeper客户端命令
客户端登录Zookeeper: sh bin/zkCli.sh -server 127.0.0.1:2181
数据清理
手动清理
可以使用zk提供的bin/zkCleanup.sh脚本进行快照文件的清理
1 | bin/zkCleanup.sh -n 5 |
表示保留最近的5个快照
自动清理
在conf文件中可以配置
1 | autopurge.snapRetainCount=10 |
autopurge.purgeInterval: 这个参数指定了清理频率,单位是小时,需要填写一个1或更大的整数,默认是0,表示不开启自动清理功能。
autopurge.snapRetainCount: 这个参数和上面的参数搭配使用,这个参数指定了需要保留的快照文件数目,默认是保留3个。
zk的监控
4字监控命令。
通过telnet或者ncat向zk发出命令。
如
1 | echo ruok | ncat *.*.*.* 2181 |
jmx
zk很好的支持了jmx,大量的监控和管理工作可以通过jmx来做。可以把zk的JMX数据集成到prometheus,使用prometheus来做zk的监控管理。
1 | jconsole 连接jmx |
默认jmx只能本地连接。要配置远程可访问。需要先开放JMX端口
1 | export JMXPORT=8081 |
然后再启动zk
跨数据中心部署
利用Observer节点

比如业务需要部署北京和香港两地都使用的 ZooKeeper服务。要求北京和香港的客户端请求的延迟都低。因此,需要再北京和香港都部署zk节点。
假设leader节点在北京。如果香港的节点也是follower,那么每个来自香港的写请求要需要在北京的leader和每个香港的follower节点之间进行propose、ack、和commit跨广域网的消息确认。
解决方案就是把香港的节点都设成observer。这些propose、ack和commit消息都变成同步一个leader的inform消息。
指定节点为observer
只需要在conf文件中,节点后面加上
1 | server.3=x.x.x.x:2222:2223:observer |
集群节点调整
手动调整
可以采取更改配置文件的方式调整。
- 停掉集群
- 修改conf文件的server.n
- 启动节点
缺点:
1. 服务中断
2. 可能导致已经提交的数据被覆盖
动态配置
3.5.0的新特性
在配置文件中开启动态配置
配置digest
使用命令进行节点的动态修改
Chubby vs Zookeeper
Chubby是一个分布式锁系统,非开源,广泛应用在Google的基础架构中,比如GFS和Bigtable中都是用chubby做协同服务
zookeeper借鉴了很多chubby的设计思想。所以他们之间有很多相似之处。