ZooKeeper介绍
- Zookeeper 分布式服务框架是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。本文将从使用者角度详细介绍 Zookeeper 的安装和配置文件中各个配置项的意义,以及分析 Zookeeper 的典型的应用场景(配置文件的管理、集群管理、同步锁、Leader 选举、队列管理等)。
- 简单说下我的理解:
- ZooKeeper有三种工作模式:
Standalone
:单点模式,有单点故障问题。伪分布式
:在一台机器同时运行多个ZooKeeper实例,仍然有单点故障问题,当然,其中配置的端口号要错开的,适合实验环境模拟集群使用。完全分布式
:在多台机器上部署ZooKeeper集群,适合线上环境使用。
- ZooKeeper到底是什么:
三种端口号
:这里先说明三个ZooKeeper配置需要的端口号,因为后面的解释中会经常引用,就拉到前面讲啦
- 端口X:客户端连接ZooKeeper集群使用的监听端口号
- 端口Y:leader和follower之间数据同步使用的端口号
- 端口Z:leader选举专用的端口号
单点分析
:在每个ZooKeeper节点当中,ZooKeeper维护了一个类似linux的树状文件系统结构,可以把一些配置信息,数据等存放到ZooKeeper当中,也可以把ZooKeeper当中的一个目录节点当做一个锁的互斥文件来实现并发安全控制,你看到这就先把ZooKeeper理解为一个在操作系统上运行的一个虚拟文件系统,只不过他不是像HDFS那样真的用来存放文件的,他是利用文件系统的节点作为底层实现来提供分布式环境很常用的功能,这在后面的实战使用
会具体讲解.集群分析
:ZooKeeper中的每个节点都是上面的单点分析
那样工作的,但是集群多节点之间到底如何进行协商和通信呢?
- 首先,类似mysql读写分离那样,ZooKeeper的每个节点都存放相同的数据,因此访问ZooKeeper的时候会被分流道各个节点实现高并发,多节点也顺便实现了高可用。
- ZooKeeper的节点之间也有主次关系,集群启动完成之后,ZooKeeper会运行选举程序(端口Z)从集群中选择一个leader节点,而其他的节点就是follower节点,对于ZooKeeper的写操作,会被转发到leader节点,而follower节点和leader节点的数据同步(端口Y)也在后台自动实现,读操作则每个节点都能提供,负载均衡~
容灾机制
:follower节点挂了比较不要紧,有leader协调,整个集群还不至于发生致命影响。但是leader节点要是挂了,就群龙无首了,但是其实这个状态和ZooKeeper集群启动前期还没确定leader节点的时候是一样的状态,下面讲解这个状态如何进行leader的 选举。
由于每个节点配置文件中都维护了整个ZooKeeper集群的所有节点列表,因此在没确定leader节点或者leader节点挂掉的时候,每个节点向leader的通信必然是失败的,follower节点就是这么发现leader节点挂了的,这个时候他就能启动选举程序进行leader竞选,和其他的所有follower节点进行通信,根据某种算法方案确定leader的节点之后,被选中的节点就启动leader的程序,化身leader重新领导整个集群。
- 客户端访问高可用:
读操作
:每个节点都能响应写操作
:不管向哪个节点请求都会转发到leader节点执行,再把数据同步到各个follower节点。访问follower节点宕机
:客户端会保存一个地址列表,会自动使用另一个地址进行访问,实在不行还有leader节点分配地址再次访问呢,而且一旦客户端和服务器的连接被断开,客户端就会自动去连接另一个节点的地址进行请求。访问leader节点宕机
:也是使用这个地址列表,如果是读操作,则leader选举出来之前都能访问,但是如果是写操作,就要等leader选举完成之后才能进行操作。- 我之前有疑问的地方就是以为客户端只维护一个节点的地址,这样的话导致了这个节点宕机之后客户端就和ZooKeeper集群断开了且无法重新连接了,但是后来知道了客户端是保存了节点地址列表,那所有问题就很好理解啦。
- 好了,说了很多废话你可能有点晕了,但是总说一点,别忘了,现在先把他当成一个树形文件系统~
ZooKeeper环境部署和和简单测试
- ZooKeeper下载:http://apache.fayea.com/zookeeper/
- 官网文档:https://zookeeper.apache.org/doc/r3.4.9/
- 安装准备:
- JDK
- hosts
- 防火墙
- SELinux
- ssh免密码登录
- 等等这些基本配置前面的Hadoop教程说过了,不再赘述,这些问题我相信都是很容易解决的。
- 安装步骤总结:
(1) 创建缓存目录;
(2) 配置zookeeper配置文件zoo.cfg,设置缓存目录、监听客户端端口号、server列表配置等
(3) bin目录下的zkServer.sh启动即可,还能查看集群中的leader、follower之间的关系
- 单机模式:
- 创建缓存目录;
- 解压ZooKeeper安装包,在conf目录下的zoo_sample.cfg文件复制一个副本zoo.cfg,在里面进行ZooKeeper整体配置:
tickTime=2000
dataDir=/opt/zookeeper1
clientPort=2181
tickTime
:这个时间是作为 Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳。
dataDir
:顾名思义就是 Zookeeper 保存数据的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里。
clientPort
:这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
- bin目录下的zkServer.sh执行即可启动:
bin/zkServer.sh start
- 可以查看ZooKeeper的执行状态:
bin/zkServer.sh status
- 集群模式:
- 创建缓存目录;
- 解压ZooKeeper安装包,在conf目录下的zoo_sample.cfg文件复制一个副本zoo.cfg,在里面进行ZooKeeper整体配置:
tickTime=2000
dataDir=/opt/zookeeper1
clientPort=2181
initLimit=5
syncLimit=2
server.1=192.168.211.1:2888:3888
server.2=192.168.211.2:2888:3888
上面前三个的配置和单机模式一样,不多赘述了。
initLimit
:这个配置项是用来配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 10 个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 52000=10 秒
syncLimit
:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 22000=4 秒
server.A=B:C:D
:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口(上面的端口Y);D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口(上面的端口Z)。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。
- 在上面配置的dataDir目录下创建myid文件,填写这个节点上的id号,就是
server.A=B:C:D
配置的A那个号码
[root@VM_68_145_centos zookeeper]# cat /opt/zookeeper1/myid
1
[root@VM_68_145_centos zookeeper]# cat /opt/zookeeper2/myid
2
[root@VM_68_145_centos zookeeper]# cat /opt/zookeeper3/myid
3
[root@VM_68_145_centos zookeeper]# cat /opt/zookeeper4/myid
4
>* 将配置好的整个安装包复制到每个节点机器上,scp命令复制过去,我这里是采用伪分布式,直接使用cp命令即可。
>* 到每个节点上zookeeper/bin目录下的zkServer.sh执行即可启动:
```bash
bin/zkServer.sh start
其他脚本介绍:
zkServer.sh : ZooKeeper服务器的启动、停止和重启脚本;
zkCli.sh : ZooKeeper的简易客户端;
zkEnv.sh : 设置ZooKeeper的环境变量;
zkCleanup.sh : 清理ZooKeeper历史数据,包括事务日志文件和快照数据文件。
- 可以查看ZooKeeper的执行状态:
zookeeper/bin/zkServer.sh status
- 简单说明这样的配置ZooKeeper集群是怎么工作的:
- 首先,每个节点上zoo.cfg配置文件中都有整个集群的列表,所以每个节点之间的通信都是可行的。
- 然后是dataDir目录下的myid标记了这个机器上的这个节点是zoo.cfg上的集群列表的哪个记录,从这个就能知道当前的这个节点所处的位置,也能知道当前机器的节点是不是leader,以便于执行leader该执行的程序。
- 关于配置的三个端口号:
- 端口X:监听客户端连接的,没什么可说的
- 端口Y:follower和leader进行数据同步通信用的,这个是长连接随时同步数据,健康情况下正常运行,leader宕机就无法正常执行,此时触发选举程序选择新的leader。
- 端口Z:选举时各个follower节点之间两两可以相互通信的,以便于成功选择出leader。
踩坑记录
:我刚开始以为端口X和端口Y是同一个,导致了我就设置了同一个端口号,所以显然是无法启动成功的,后来知道这两个端口号完成的是不同的功能,所以改正完成就能启动成功了。
- 可以创建一个启动脚本一次性启动整个集群,比如我搭建的伪分布式的启动脚本:
#! /bin/sh
/usr/java/zookeeper/zookeeper1/bin/zkServer.sh start
/usr/java/zookeeper/zookeeper2/bin/zkServer.sh start
/usr/java/zookeeper/zookeeper3/bin/zkServer.sh start
/usr/java/zookeeper/zookeeper4/bin/zkServer.sh start
/usr/java/zookeeper/zookeeper1/bin/zkServer.sh status
/usr/java/zookeeper/zookeeper2/bin/zkServer.sh status
/usr/java/zookeeper/zookeeper3/bin/zkServer.sh status
/usr/java/zookeeper/zookeeper4/bin/zkServer.sh status
echo 'you can use zookeeper client connect to 119.29.153.56 on follow ports:'
echo '2181'
echo '2182'
echo '2183'
echo '2184'
各个节点的启动状态:
[root@VM_68_145_centos zookeeper]# /usr/java/zookeeper/zookeeper1/bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/java/zookeeper/zookeeper1/bin/../conf/zoo.cfg
Mode: follower
[root@VM_68_145_centos zookeeper]# /usr/java/zookeeper/zookeeper2/bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/java/zookeeper/zookeeper2/bin/../conf/zoo.cfg
Mode: follower
[root@VM_68_145_centos zookeeper]# /usr/java/zookeeper/zookeeper3/bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/java/zookeeper/zookeeper3/bin/../conf/zoo.cfg
Mode: leader
[root@VM_68_145_centos zookeeper]# /usr/java/zookeeper/zookeeper4/bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/java/zookeeper/zookeeper4/bin/../conf/zoo.cfg
Mode: follower
启动完成之后顺便查看了每个节点充当的是leader还是follower,并把集群的所有客户端连接端口号输出出来,从上面的执行结果可知,zookeeper3的这个实例是leader,其他实例是follower。
- ZooKeeper简单操作测试:
- 命令行方式操纵ZooKeeper:
进入本地的ZooKeeper:
/usr/java/zookeeper/zookeeper1/bin/zkCli.sh
进入远程的ZooKeeper:
/usr/java/zookeeper/zookeeper1/bin/zkCli.sh -server {ip}:{port}
> 这样就进入了ZooKeeper的命令行客户端,就能访问指定的ZooKeeper集群中的数据,支持以下的操作(在命令行下输入错误命令就能看到提示了。。。):
>```bash
stat path [watch]
set path data [version]
ls path [watch]
delquota [-n|-b] path
ls2 path [watch]
setAcl path acl
setquota -n|-b val path
history
redo cmdno
printwatches on|off
delete path [version]
sync path
listquota path
rmr path
get path [watch]
create [-s] [-e] path data acl
addauth scheme auth
quit
getAcl path
close
connect host:port
- 操作示例:
创建节点/testInput,节点中的数值为inputData
create /testInput "inputData"
查询刚才插入的节点
get /testInput
运行结果
inputData
cZxid = 0x300000006
ctime = Sun Dec 18 21:47:21 CST 2016
mZxid = 0x300000006
mtime = Sun Dec 18 21:47:21 CST 2016
pZxid = 0x300000006
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 9
numChildren = 0
> 所以整个操纵的过程就和平时操作linux文件的感觉差不多,下面的Java程序操纵其实也只是对这些命令行的封装,下面也给出一些示例。
9. Java客户端操纵ZooKeeper:
>* 这里也只简单示例一些基本操作:
>```java
public class ZooKeeperClient {
//同步互斥变量,用来阻塞等待ZooKeeper连接完成之后再进行ZooKeeper的操作命令
private static CountDownLatch connectedSemaphore = new CountDownLatch( 1 );
public static void main(String[] args) throws Exception {
// 创建一个与服务器的连接
ZooKeeper zk = new ZooKeeper("119.29.153.56:2181",3000, new Watcher() {//这个是服务器连接完成回调的监听器
// 监控所有被触发的事件
public void process(WatchedEvent event) {
System.out.println("已经触发了" + event.getType() + "事件!");
if ( Event.KeeperState.SyncConnected == event.getState() ) {
//连接完成的同步事件,互斥变量取消,下面的阻塞停止,程序继续执行
connectedSemaphore.countDown();
}
}
});
//如果和ZooKeeper服务器的TCP连接还没完全建立,就阻塞等待
connectedSemaphore.await();
// 创建一个目录节点
zk.create("/testRootPath", "testRootData".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
// 创建一个子目录节点
zk.create("/testRootPath/testChildPathOne", "testChildDataOne".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
System.out.println(new String(zk.getData("/testRootPath",false,null)));
// 取出子目录节点列表
System.out.println(zk.getChildren("/testRootPath",true));
// 修改子目录节点数据
zk.setData("/testRootPath/testChildPathOne","modifyChildDataOne".getBytes(),-1);
System.out.println("目录节点状态:["+zk.exists("/testRootPath",true)+"]");
// 创建另外一个子目录节点
zk.create("/testRootPath/testChildPathTwo", "testChildDataTwo".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
System.out.println(new String(zk.getData("/testRootPath/testChildPathTwo",true,null)));
// 删除子目录节点
zk.delete("/testRootPath/testChildPathTwo",-1);
zk.delete("/testRootPath/testChildPathOne",-1);
// 删除父目录节点
zk.delete("/testRootPath",-1);
// 关闭连接
zk.close();
}
}
输出结果:
已经触发了None事件!
testRootData
[testChildPathOne]
目录节点状态:[17179869200,17179869200,1482114759242,1482114759242,0,1,0,0,12,1,17179869201
]
已经触发了NodeChildrenChanged事件!
testChildDataTwo
已经触发了NodeDeleted事件!
已经触发了NodeDeleted事件!
解释
:
第一个添加节点/testRootPath,触发None事件,之后再获取这个节点数据、其子节点列表、节点状态
接下来创建第二个子节点/testRootPath/testChildPathTwo,触发NodeChildrenChanged事件
然后删除两个子节点,触发两次NodeDeleted事件
- 常用接口介绍:
IBM教程
这边已经有部分接口的说明,可以拿来当做初步认识,在比较下面,往下翻哦~。- 几个可能的疑问:
ACL
:这个是ZooKeeper本身提供的简单的权限控制模型,有一些简单的权限控制策略,可以稍做了解。
OP
:面向对象的思想嘛,一个ZooKeeper的操作命令也抽象为一个操作对象,不过它只是个抽象类,具体的实现有Delete、Check等子类才是实际的具体操作对象。
CreateMode
:创建节点的模式枚举,四个成员分别是:PERSISTENT(持久化)、PERSISTENT_SEQUENTIAL(持久化并序号自增)、EPHEMERAL(临时,当前session有效)、EPHEMERAL_SEQUENTIAL(临时,当前session有效,序号自增)
ZooDefs.Ids
:提供了上面ACL
的常用的权限策略常量列表。
Event.KeeperState
:ZooKeeper的事件类型状态常量列表。- 有了上面的就基本对ZooKeeper的API有了初步的认识,剩下的问题看官方文档就很好理解了:http://people.apache.org/~larsgeorge/zookeeper-1215258/build/docs/dev-api/
特别注意:ZooKeeper是支持
Watcher监听
的,你可以用它监听某个节点的值是否存在、是否改变、是否被删除等之类的动作,当他触发了相关的动作就会进行回调
的,有点是异步编程,也是特别棒的东西。到这里入门就告一段落了,接下来就是实际应用场景的分析啦~
实战使用
-
统一命名服务(Name Service)
:在ZooKeeper的树形结构下你可以创建一个统一的不重复的命名,比如create创建一个节点即可,再创建一个相同名称的节点是不允许的。 -
配置管理(Configuration Management)
:意思就是分布式应用的配置可以交给ZooKeeper来管理,不然一旦修改配置,就得每台机器上的配置都做相应的修改,如果交给ZooKeeper管理的话,只需要修改ZooKeeper上的节点值即可。 -
集群管理(Group Membership)
:
- Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能 Leader Election。
- 它们的实现方式都是在 Zookeeper上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用 getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。
- Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。
-
共享锁(Locks)
:共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用 exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。 -
队列管理
:
同步队列
:
当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。
队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。
同步队列用 Zookeeper 实现的实现思路如下:
创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。FIFO队列
:
实现的思路也非常简单,就是在特定的目录下创建 SEQUENTIAL 类型的子目录 /queue_i,这样就能保证所有成员加入队列时都是有编号的,出队列时通过 getChildren( ) 方法可以返回当前所有的队列中的元素,然后消费其中最小的一个,这样就能保证 FIFO。
-
可见ZooKeeper本身的树形目录结构以及其提供的对目录节点的
监控Watcher
,提供了实时数据同步以及可客户端的及时通知。并且利用节点之间的变化触发的事件类型
可以很方便地设计很多实际应用场景的算法需求。具体的很多说明和算法示例代码在IBM教程中有详细的讲解。
常用分布式服务框架和ZooKeeper的依赖讲解
- 之前的
Hadoop 2.x
高可用配置使用了ZooKeeper,但是我根本不知道这个外部添加的ZooKeeper工具是怎么被Hadoop调用的,当时只是知道按照教程一步步下来就能成功运行,不过现在好好把ZooKeeper研究完了,就该来好好回顾一下这个问题了。- 这个是之前的Hadoop 2.x的高可用安装配置教程:http://www.coselding.cn/article/2016-05-31/hadoop2-high-available.html
- 具体的过程就不在这里重复说明了,只挑要点讲解:
- Hadoop 2.x启动前要求先把ZooKeeper配置好并启动起来,这个时候ZooKeeper还是独立运行的。
- Hadoop 2.x配置完成之后在启动步骤中有一步:
hdfs zkfc -formatZK
> 这步干嘛的?这个时候Hadoop中的多个NameNode节点都已经配置好了,这个步骤就是把NameNode的信息注册到ZooKeeper当中,包括哪个是Active,有哪些Standby,都在ZooKeeper当中进行注册记录,并且有一个`ZKFC`进程负责观察NameNode的状态,如果有NameNode宕机了,就马上通知ZooKeeper进行相应的记录修改,也就是说,ZooKeeper当中实时存放着NameNode的节点列表以及哪个是Active。(这部分的实时记录和更新代码存在ZKFC当中,是Hadoop本身就已经实现的代码,不需要我们自己编写,配置好就行)。
>3. Hadoop怎么知道ZooKeeper的存在?`hdfs-site.xml`中不是配置了`dfs.nameservices`这个属性吗,这个属性就是告诉Hadoop ZooKeeper的地址,Hadoop通过这个地址连接到ZooKeeper注册NameNode的命名信息,这个动作由`hdfs zkfc -formatZK`这个命令触发初始化执行。
>4. 到这步就已经知道了,Hadoop中的NameNode的信息ZooKeeper都知道了,那我们是怎么访问HDFS的呢?教程中很清楚说明了,我们访问的是`hdfs-site.xml`中配置的`dfs.nameservices`来访问HDFS的,这个地址刚才说了,就是ZooKeeper的地址,所以在访问HDFS的时候就是先访问了ZooKeeper得到当前的Active NameNode,然后再用得到的Active NameNode地址再去访问HDFS。
>5. Hadoop的Active NameNode宕机呢?`ZKFC`时刻检测着NameNode,当NameNode宕机的时候,通知ZooKeeper,ZooKeeper保存了所有的NameNode的地址列表,他去通知所有的Standby NameNode进行`抢锁竞选`,谁抢到不重要,结果是会有一个Standby NameNode抢到锁并切换为Active NameNode,并通知ZooKeeper,这个时候ZooKeeper中的数据依然是实时最新的,很完美~
>6. 这不实现了高可用和主备自动切换吗~ 简单粗暴~
### 参考文档
>* IBM教程:[https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/](https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/)
>* 官网文档:[https://zookeeper.apache.org/doc/r3.4.9/](https://zookeeper.apache.org/doc/r3.4.9/)
>* 参考博客:[http://luchunli.blog.51cto.com/2368057/1681841](http://luchunli.blog.51cto.com/2368057/1681841)
网友评论