全网最祥,万字长文全面剖析ZooKeeper
一. ZooKeeper是什么
ZooKeeper由雅虎研究院开发,是Google Chubby的开源实现,后来托管到Apache,于2010年11月正式成为Apache的顶级项目。
ZooKeeper是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。
分布式应用程序可以基于ZooKeeper实现数据发布与订阅、负载均衡、命名服务、分布式协调与通知、集群管理、Leader选举、分布式锁、分布式队列等功能。
二. ZooKeeper目标
ZooKeeper致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务
2.1 高性能
ZooKeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,尤其适用于以读为主的应用场景
2.2 高可用
ZooKeeper一般以集群的方式对外提供服务,一般3 ~ 5台机器就可以组成一个可用的Zookeeper集群了,每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都相互保持着通信。只要集群中超过一般的机器都能够正常工作,那么整个集群就能够正常对外服务
2.3 严格顺序访问
对于来自客户端的每个更新请求,ZooKeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序
三. ZooKeeper五大特性
ZooKeeper一般以集群的方式对外提供服务,一个集群包含多个节点,每个节点对应一台ZooKeeper服务器,所有的节点共同对外提供服务,整个集群环境对分布式数据一致性提供了全面的支持,具体包括以下五大特性:
3.1 顺序一致性
从同一个客户端发起的请求,最终将会严格按照其发送顺序进入ZooKeeper中
3.2 原子性
所有请求的响应结果在整个分布式集群环境中具备原子性,即要么整个集群中所有机器都成功的处理了某个请求,要么就都没有处理,绝对不会出现集群中一部分机器处理了某一个请求,而另一部分机器却没有处理的情况
3.3 单一性
无论客户端连接到ZooKeeper集群中哪个服务器,每个客户端所看到的服务端模型都是一致的,不可能出现两种不同的数据状态,因为ZooKeeper集群中每台服务器之间会进行数据同步
3.4 可靠性
一旦服务端数据的状态发送了变化,就会立即存储起来,除非此时有另一个请求对其进行了变更,否则数据一定是可靠的
3.5 实时性
当某个请求被成功处理后,ZooKeeper仅仅保证在一定的时间段内,客户端最终一定能从服务端上读取到最新的数据状态,即ZooKeeper保证数据的最终一致性
四. ZooKeeper集群角色
在分布式系统中,集群中每台机器都有自己的角色,ZooKeeper没有沿用传统的Master/Slave模式(主备模式),而是引入了Leader、Follower和Observer三种角色
4.1 Leader
集群通过一个Leader选举过程从所有的机器中选举一台机器作为”Leader”,Leader能为客户端提供读和写服务
Leader服务器是整个集群工作机制的核心,主要工作:
事务请求的唯一调度者和处理者,保证集群事务处理的顺序性
集群内部各服务器的调度者
4.2 Follower
顾名思义,Follower是追随者,主要工作:
参与Leader选举投票
处理客户端非事务请求 - 即读服务
转发事务请求给Leader服务器
参与事务请求Proposal的投票
4.3 Observer
Observer是ZooKeeper自3.3.0版本开始引入的一个全新的服务器角色,充当一个观察者角色,工作原理和Follower基本是一致的,和Follower唯一的区别是Observer不参与任何形式的投票
处理客户端非事务请求 - 即读服务
转发事务请求给Leader服务器
不参与Leader选举投票
参与事务请求Proposal的投票
所以Observer可以在不影响写性能的情况下提升集群的读性能
五. 原子广播协议 - Zab
ZooKeeper并非采用经典的分布式一致性协议 - Paxos,而是参考了Paxos设计了一种更加轻量级的支持崩溃可恢复的原子广播协议-Zab(ZooKeeper Atomic Broadcast)。
ZAB协议分为两个阶段 - Leader Election(领导选举)和Atomic Broadcast(原子广播)
5.1 领导选举 - Leader Election
当集群启动时,会选举一台节点为Leader,而其他节点为Follower,当Leader节点出现网络中断、崩溃退出与重启等异常情况,ZAB会进入恢复模式并选举产生新的Leader服务器,当集群中已有过半机器与该Leader服务器完成数据状态同步,退出恢复模式
5.2 原子广播 - Atomic Broadcast
当领导选举完成后,就进入原子广播阶段。此时集群中已存在一个Leader服务器在进行消息广播,当一台同样遵循ZAB协议的服务器启动后加入到集群中,新加的服务器会自动进入数据恢复阶段
六. 事务请求
在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的请求,一般指创建节点、更新数据、删除节点以及创建会话操作
6.1 事务转发
为了保证事务请求被顺序执行,从而确保ZooKeeper集群的数据一致性,所有的事务请求必须由Leader服务器处理,ZooKeeper实现了非常特别的事务请求转发机制:
所有非Leader服务器如果接收到来自客户端的事务请求,必须将其转发给Leader服务器来处理
6.2 事务ID - ZXID
在分布式系统中,事务请求可能存在依赖关系,如变更C需要依赖变更A和变更B,这样就要求ZAB协议能够保证如果一个状态变更成功被处理了,那么其所有依赖的状态变更都应该已经提前被处理掉了。
在ZooKeeper中对每一个事务请求,都会为其分配一个全局唯一的事务ID,使用ZXID表示,通常是一个64位的数字。每一个ZXID对应一次事务,从这些ZXID可以间接识别出ZooKeeper处理这些事务请求的全局顺序
七. 数据节点 - ZNode
ZooKeeper内部拥有一个树状的内存模型,类似文件系统,只是在ZooKeeper中将这些目录与文件系统统称为ZNode,ZNode是ZooKeeper中数据的最小单元,每个ZNode上可以保存数据,还可以挂载子节点,因此构成了一个层次化的命名空间
7.1 节点路径
ZooKeeper中使用斜杠(/)分割的路径表示ZNode路径,斜杠(/)表示根节点
7.2 节点特性
在ZooKeeper中,每个数据节点ZNode都是有生命周期的,其生命周期的长短取决于ZNode的节点类型
7.3 权限控制 - ACL
为了有效保障ZooKeeper中数据的安全,避免因误操作而带来数据随意变更导致分布式系统异常,ZooKeeper提供了一套完善的ACL(Access Contro List)权限控制机制来保障数据的安全。
可以从三个方面理解ACL机制,分别是:权限模式(Scheme)、授权对象(ID)和权限(Permission),通常使用”scheme:id:permission”来标识一个有效的ACL信息
7.4 节点状态信息
每个数据节点ZNode除了存储数据内容外,还存储了数据节点本身的一些状态信息
7.5 节点版本
ZooKeeper为数据节点引入版本的概念,对个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化
在分布式系统中,在运行过程中往往需要保证数据访问的排他性。Java并发中是实现了对CAS的指令支持,即对于值V,每次更新前都会比对其值是否是预期值A,只有符合预期,才会将V原子化的更新到新值B
而ZooKeeper每个节点都有数据版本的概念,在调用更新操作的时候,先从请求中获取当前请求的版本version,同时获取服务器上该数据最新版本currentVersion,如果无法匹配,就无法更新成功,这样可以有效避免一些分布式更新的并发问题
八. Watcher - 数据变更的通知
在ZooKeeper中,引入Watcher机制来实现分布式数据的发布/订阅功能。ZooKeeper允许客户端向服务器注册一个Watcher监听,当服务器的一些指定事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能
Watcher机制为以下三个过程:
8.1 客户端注册Watcher
在创建一个ZooKeeper客户端对象实例时,可以向构造方法中传入一个Watcher,这个Watcher将作为整个ZooKeeper会话期间的默认Watcher,一致保存在客户端,并向ZooKeeper服务器注册Watcher
客户端并不会把真实的Watcher对象传递到服务器,仅仅只是在客户端请求中使用boolean类型属性进行标记,降低网络开销和服务器内存开销
8.2 服务端处理Watcher
服务端执行数据变更,当Watcher监听的对应数据节点的数据内容发生变更,如果找到对应的Watcher,会将其提取出来,同时从管理中将其删除(说明Watcher在服务端是一次性的,即触发一次就失效了),触发Watcher,向客户端发送通知
8.3 客户端回调Watcher
客户端获取通知,识别出事件类型,从相应的Watcher存储中去除对应的Watcher(说明客户端也是一次性的,即一旦触发就会失效)
8.4 总结
一致性:无论是客户端还是服务器,一旦一个Watcher被处罚,ZooKeeper都会将其从相应的存储中移除,因此开发人员在Watcher使用上要反复注册,这样可以有效减轻服务器压力
客户端串行执行:客户端Watcher回调的过程是一个串行同步的过程,这保证了顺序
轻量:客户端并不会把真实的Watcher对象传递到服务器,仅仅只是在客户端请求中使用boolean类型属性进行标记,降低网络开销和服务器内存开销
九. Session - 会话
Session是指客户端连接 - 客户端和服务器之间的一个TCP长连接
9.1 会话状态
会话在整个生命周期中,会在不同的会话转态之间进行切换
9.2 Session属性
Session是ZooKeeper中的会话实体,代表了一个客户端会话,其包含4个属性:
9.3 心跳检测
为了保证客户端会话的有效性,客户端会在会话超时时间范围内向服务器发送PING请求来保持会话的有效性,即心跳检测。
服务器接收到客户端的这个心跳检测,就会重新激活对应的客户端会话
9.4 会话清理
服务器的超级检查线程会在指定时间点进行检查,整理出一些已经过期的会话后,就要开始进行会话清理了:
关闭会话
清理相关的临时节点
9.5 重连
当客户端和服务器之间网络连接断开,客户端会自动进行反复的重连,直到最终成功连接上ZooKeeper集群中的一台机器
在会话超时时间内重新连接上,被视为重连成功
在会话超时时间外重新连接上,此时服务器已经进行了会话清理,但客户端不知道会话已经失效,重新连接服务器会告诉客户端会话已失效,被视为非法会话
在此阶段,我们已经充分了解了什么是Zookeeper了,那么我们接下来用它能做什么,单机模式的安装,以及它的使用来为大家做详细介绍。
Zookeeper能做什么
zookeeper功能非常强大,可以实现诸如分布式应用配置管理、统一命名服务、状态同步服务、集群管理等功能,我们这里拿比较简单的分布式应用配置管理为例来说明。
假设我们的程序是分布式部署在多台机器上,如果我们要改变程序的配置文件,需要逐台机器去修改,非常麻烦,现在把这些配置全部放到zookeeper上去,保存在 zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 zookeeper 的通知,然后从 zookeeper 获取新的配置信息应用到系统中。
如上,你大致应该了解zookeeper是个什么东西,大概能做些什么了,我们马上来学习下zookeeper的安装及使用,并开发一个小程序来实现zookeeper这个分布式配置管理的功能。
Zookeeper单机模式安装
Step1:配置JAVA环境,检验环境:java -version
Step2:下载并解压zookeeper
# cd /usr/local# wget http://mirror.bit.edu.cn/apache/zookeeper/stable/zookeeper-3.4.12.tar.gz# tar -zxvf zookeeper-3.4.12.tar.gz# cd zookeeper-3.4.12
Step3:重命名配置文件zoo_sample.cfg
# cp conf/zoo_sample.cfg conf/zoo.cfg
Step4:启动zookeeper
# bin/zkServer.sh start
Step5:检测是否成功启动,用zookeeper客户端连接下服务端
# bin/zkCli.sh
Zookeeper使用
使用客户端命令操作zookeeper
1、使用 ls 命令来查看当前 ZooKeeper 中所包含的内容
2、创建一个新的 znode ,使用 create /zkPro myData
3、再次使用 ls 命令来查看现在 zookeeper 中所包含的内容:
4、下面我们运行 get 命令来确认第二步中所创建的 znode 是否包含我们所创建的字符串:
5、下面我们通过 set 命令来对 zk 所关联的字符串进行设置:
6、下面我们将刚才创建的 znode 删除
使用Java API操作zookeeper
使用Java API操作zookeeper需要引用下面的包
下面我们来实现上面说的分布式配置中心:
1、在zookeeper里增加一个目录节点,并且把配置信息存储在里面
2、启动两个zookeeper客户端程序,代码如下所示
import java.util.concurrent.CountDownLatch;import org.apache.zookeeper.WatchedEvent;import org.apache.zookeeper.Watcher;import org.apache.zookeeper.Watcher.Event.EventType;import org.apache.zookeeper.Watcher.Event.KeeperState;import org.apache.zookeeper.ZooKeeper;import org.apache.zookeeper.data.Stat; /** * 分布式配置中心demo * @author * */public class ZooKeeperProSync implements Watcher { private static CountDownLatch connectedSemaphore = new CountDownLatch(1); private static ZooKeeper zk = null; private static Stat stat = new Stat(); public static void main(String[] args) throws Exception { //zookeeper配置数据存放路径 String path = "/username"; //连接zookeeper并且注册一个默认的监听器 zk = new ZooKeeper("192.168.31.100:2181", 5000, // new ZooKeeperProSync()); //等待zk连接成功的通知 connectedSemaphore.await(); //获取path目录节点的配置数据,并注册默认的监听器 System.out.println(new String(zk.getData(path, true, stat))); Thread.sleep(Integer.MAX_VALUE); } public void process(WatchedEvent event) { if (KeeperState.SyncConnected == event.getState()) { //zk连接成功通知事件 if (EventType.None == event.getType() && null == event.getPath()) { connectedSemaphore.countDown(); } else if (event.getType() == EventType.NodeDataChanged) { //zk目录节点数据变化通知事件 try { System.out.println("配置已修改,新值为:" + new String(zk.getData(event.getPath(), true, stat))); } catch (Exception e) { } } } }}
两个程序启动后都正确的读取到了zookeeper的/username目录节点下的数据'qingfeng'
3、我们在zookeeper里修改下目录节点/username下的数据
修改完成后,我们看见两个程序后台都及时收到了他们监听的目录节点数据变更后的值,如下所示
Zookeeper集群模式安装
本例搭建的是伪集群模式,即一台机器上启动三个zookeeper实例组成集群,真正的集群模式无非就是实例IP地址不同,搭建方法没有区别
Step1:配置JAVA环境,检验环境:java -version
Step2:下载并解压zookeeper
# cd /usr/local# wget http://mirror.bit.edu.cn/apache/zookeeper/stable/zookeeper-3.4.12.tar.gz# tar -zxvf zookeeper-3.4.12.tar.gz# cd zookeeper-3.4.12
Step3:重命名 zoo_sample.cfg文件
# cp conf/zoo_sample.cfg conf/zoo-1.cfg
Step4:修改配置文件zoo-1.cfg,原配置文件里有的,修改成下面的值,没有的则加上
# vim conf/zoo-1.cfgdataDir=/tmp/zookeeper-1clientPort=2181server.1=127.0.0.1:2888:3888server.2=127.0.0.1:2889:3889server.3=127.0.0.1:2890:3890
配置说明
tickTime:这个时间是作为 Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳。
initLimit:这个配置项是用来配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 10个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 10*2000=20 秒
syncLimit:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 5*2000=10秒
dataDir:顾名思义就是 Zookeeper 保存数据的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里。
clientPort:这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
server.A=B:C:D:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。
Step4:再从zoo-1.cfg复制两个配置文件zoo-2.cfg和zoo-3.cfg,只需修改dataDir和clientPort不同即可
# cp conf/zoo-1.cfg conf/zoo-2.cfg# cp conf/zoo-1.cfg conf/zoo-3.cfg# vim conf/zoo-2.cfgdataDir=/tmp/zookeeper-2clientPort=2182# vim conf/zoo-2.cfgdataDir=/tmp/zookeeper-3clientPort=2183
Step5:标识Server ID
创建三个文件夹/tmp/zookeeper-1,/tmp/zookeeper-2,/tmp/zookeeper-2,在每个目录中创建文件myid 文件,写入当前实例的server id,即1.2.3
# cd /tmp/zookeeper-1# vim myid1# cd /tmp/zookeeper-2# vim myid2# cd /tmp/zookeeper-3# vim myid3
Step6:启动三个zookeeper实例
# bin/zkServer.sh start conf/zoo-1.cfg# bin/zkServer.sh start conf/zoo-2.cfg# bin/zkServer.sh start conf/zoo-3.cfg
Step7:检测集群状态,也可以直接用命令“zkCli.sh -server IP:PORT”连接zookeeper服务端检测
至此,我们对zookeeper就算有了一个入门的了解,当然zookeeper远比我们这里描述的功能多,比如用zookeeper实现集群管理,分布式锁,分布式队列,zookeeper集群leader选举等等。
想必大家对zookeeper有了一个了解吧!如果有更深层次的理解,欢迎在评论区和我讨论讨论!