zookeeper
zookeeper简介
ZooKeeper是一个高可用的分布式数据管理与系统协调框架。官方文档上这么解释zookeeper,它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
ZAB协议
Zookeeper 使用 Zookeeper Atomic Broadcast (ZAB) 协议来保障分布式数据一致性。
ZAB是一种支持崩溃恢复的消息广播协议,采用类似2PC(两阶段提交)的广播模式保证正常运行时性能,并使用基于 Paxos 的策略保证崩溃恢复时的一致性。
ZAB协议中节点存在四种状态:
状态 | 说明 |
---|---|
Leading | 当前节点为 Follower 在 Leader 协调下执行事务 |
Following | 当前节点为 Follower 在 Leader 协调下执行事务 |
Looking | 集群没有正在运行的 Leader, 正处于选举过程 |
Observing | 节点跟随 Leader 保存系统最新的状态提供读服务,但不参与选举和事务投票 |
广播模式: 当集群正常运行过程中,Leader 使用广播模式保证各 Follower 节点的一致性
恢复模式: 集群启动或 Leader 崩溃时系统进入恢复模式,选举 Leader 并将集群中各节点的数据同步到最新状态
zookeeper要点总结
znode数据结构
节点类型 | 简介 |
---|---|
持久节点 | 持久节点是一种非常有用的节点,可以通过持久类型的znode为应用永久保存一些数据。持久节点的删除只能通过调用delete来进行删除,用来保存系统级的配置信息。 |
临时节点 | 临时节点传达了应用某些方面的信息,当会话有效时这些信息必须有效的保存,一个临时的节点在以下两种情况下将被删除:1.当创建者的客户端的会话因为超时或者主动关闭而终止。2.当某个客户端(不一定是创建者)主动删除该节点。 因为临时节点在其创建者会话超时时会被删除,所以临时节点不能拥有子节点。 |
有序节点 | 一个znode还可以设置为有序的节点(持久和临时的都可以),一个有序的znode节点被分配唯一一个单调递增的整数。当创建有序节点时,一个序号会被追加到路径之后。例如,如果一个客户端创建了一个有序znode节点,其路径为/tasks/task-,那么zk将会分配一个序号,如1,并将这个数字追加到路径之后,最后该节点的名称/tasks/task-1。有序znode通过提供了创建具有唯一znode名称的方式,同时也通过这种方式可以直观的查看znode的创建顺序 |
数据节点除了存储了数据内容之外,还存储了数据节点本身的一些状态信息,zookeeper是通过Stat类来记录这些信息的
具体的状态属性说明如下表:
zookeeper节点个数建议为奇数
在zookeeper集群中只要有过半的机器是正常工作的,那么整个集群对外就是可用的,这里的过半是指大于集群中节点总数的一半,而不是等于。可以从脑裂和节省资源的角度来分析:
简单说一下什么是脑裂,集群的脑裂通常是发生在节点之间通信不可达的情况下,集群会分裂成不同的小集群,小集群各自选出自己的master节点,导致原有的集群出现多个master节点的情况。
脑裂角度
当zookeeper集群有5个节点的时候,发生了脑裂,出现两个小集群
第一种情况
集群A | 集群B |
---|---|
1一个节点 | 4个节点 |
第二种情况
集群A | 集群B |
---|---|
2一个节点 | 3个节点 |
假如出现上面两种情况,A、B中总会有一个小集群满足 可用节点数量 > 总节点数量/2 。所以zookeeper集群仍然能够选举出leader , 仍然能对外提供服务,只不过是有一部分节点失效了而已。
当zookeeper集群有4个节点的时候,发生了脑裂,出现两个小集群
第一种情况
集群A | 集群B |
---|---|
1一个节点 | 3个节点 |
第二种情况
集群A | 集群B |
---|---|
2一个节点 | 2个节点 |
当出现脑裂成1个和3个节点的时候,小集群B可以满足选举条件,此时只有一个集群可用,zookeeper集群对外是可用的,但是当出现小集群都是2个节点的时候,都不满足可用节点数量 > 总节点数量/2 的选举条件, 所以此时zookeeper集群就彻底不能提供服务。
在节点数量是奇数个的情况下,zookeeper集群总能对外提供服务(即使损失了一部分节点);如果节点数量是偶数个,会存在zookeeper集群不能用的可能性(脑裂成两个均等的子集群的时候)。
容错能力角度
假设有两个zookeeper集群,集群1是3个节点,集群2是4个节点。zookeeper集群想要正常对外提供服务(即leader选举成功),至少需要过半个节点是正常的。换句话说,就是不可用的节点不能过半,也就是集群1只能挂掉1个几点,集群2也只能挂掉1个节点,因此在容错能力相同的情况下,选择奇数台的节点数更为节省资源。
客户端命令
下面介绍一下一些简单常用的客户端命令
命令 | 作用 |
---|---|
ls path [watch] | ls 命令可以列出 ZooKeeper 指定节点下的所有子节点,但是只能查看指定节点下的第一级子节点。 path 用于指定节点路径 |
create [-s] [-e] path data acl 1 | create 命令可以创建一个 znode,-s 用于指定节点是否是顺序的, -e 用于指定节点是否是临时的, -s 和 -e 是可选的, 默认创建持久节点。path 用于指定节点路径, data 表示节点数据, acl 用于权限控制, 默认情况下不做权限控制 |
get path [watch] | 使用 get 命令可以获取 ZooKeeper 指定节点的数据内容和相关信息 |
set path data [version] | 使用 set 命令可以更新节点的数据,data 是更新后的数据,在 ZooKeeper 中, znode 中的数据是有版本概念的, version 就是用来指定更新操作是基于 znode 的哪一个数据版本 |
delete path [version] | 使用 delete 命令可以删除指定的节点,其中 path 指定节点路径, version 参数和 set 命令中的 version 参数一样 |
stat path [watch] | 使用 stat 命令可以输出节点的统计信息 |
示例:
创建节点,名称为/tce 节点存储数据为123
获取/tce节点的信息,分别有数据内容(123)、创建该节点的事务ID(cZxid)、最后一次更新该节点的事务ID(mZxid)和最后一次更新该节点的时间(mtime)等属性信息
更新/tce节点数据内容为456,然后可以看到mZxid、dataVersion等进行相应的变化
先在/tce下创建两个子节点/tce1和/tce2,然后通过ls /tce进行查看子节点
还可以通过ls2 /tce查看更详细的节点信息
watcher机制
通过zookeeper的watcher机制可以实现数据发布/订阅等许多应用场景,下面简单介绍一下watcher机制:
zookeeper客户端会话连接之后,zookeeper允许客户端向服务端注册一个Watcher监听,同时会将Watcher对象存储在客户端的WatcherManager中,当zookeeper服务端触发Watcher事件后,会向客户端发送通知,客户端现场从WatcherManager中取出对应的Watcher对象来执行回调逻辑。
watcher机制源码分析
ZooKeeper 的 Watcher 机制,总的来说可以分为三个过程:客户端注册 Watcher、服务器处理 Watcher 和客户端回调 Watcher。接下来我们将结合源码以及实际操作进行这三个过程的分析:
客户端注册Watcher
通过java客户端API创建zookeeper客户端实例的时候,可以在构造方法中传入一个默认的Watcher
这个默认的Watcher将作为整个zookeeper会话期间默认的watcher,会一直保存在客户端的ZkWatcherManager的default中。
另外zookeeper客户端也可以通过getData、getChrgetChildren和exist三个接口来向zookeeper服务端注册watcher,三者注册watcher的原理大致是一样的,这里以getData这个接口来进行分析,getData用于获取指定节点的数据内容,主要有两个方法:
在向getData接口注册一个watcher后,客户端会把请求标记为“使用watcher监听”,并创建一个DataWatchRegistration对象用于保存watcher与节点path的对应关系。
下面将添加了System.out输出的代码重新编译部署之后来验证一下
可以看到在控制台输出了刚才打印的注释信息,同时我们也可以发现SendThread线程向客户端发送的信息里面将是否使用监听标记为“T”。通过Debug输出我们也可以看到一个Packet对象,在zookeeper中Packet可以看做是一个最小的通信协议单元,用于客户端与服务端之间的网络传输,任何需要传输的对象都会被包装成一个Packet对象。因此上面提到的DataWatchRegistration对象即用于保存watcher与节点path的对应关系的对象会被封装到Packet里面,然后放入发送队列等待客户端发送:
随后客户端就会向服务端发送这个请求,同时等待请求的返回,完成请求发送后,客户端SendThread线程的readResponse方法会负责接收来自服务端的响应
在上面的过程中可以看到watcher暂时封装在了DataWatchRegistration对象中,而实际上这个watcher被会再次提取出来,交给ZkWatchManager保存在dataWatches,dataWatches是一个Map<String,Set>类型的数据结构,保存数据结点和Watcher对象的一一映射关系,便于被监听的事件发生变化之后的watcher事件回调。
那么现在有一个问题就是Watcher对象会不会发送到服务端呢?从前面的分析我们会发现好像watcher对象被封装在DataWatchRegistration里面,然后放在Packet对象里面随着请求发送给客户端,但其实这只是障眼法,真正在网络传输序列化的过程中并没有将DataWatchRegistration完全序列化到底层字节数组中去,Packet对象的createBB只是将requestHeader和request两个属性序列化,因此watcher并没有被传输到服务端去。zookeeper开发者这样设计的原因应该是怕当所有的客户端watcher都传输到服务端的话,服务端肯定的会出现内存紧张或者其他性能之类的问题。
客户端注册Watcher的流程大概就是如下几步:
服务端处理Watcher
在完成了客户端注册watcher之后,第二步就是服务端对watcher的操作了,大概分为两点,一是服务端完成客户端的watcher注册,将watcher存储起来,二是当watcher事件发生的时候,服务端对watcher的处理
在接收到客户端的请求之后,服务端中FinalRequestProcessor的processRequest方法会对请求进行处理,判断当前请求是否需要注册watcher,然后就会将ServerCnxn对象(zookeeper客户端和服务端之间的连接接口,在3.4版本中是基于netty实现的NettyServerCnxn,实现了Watcher的process接口,可以把ServerCnxn看成是一个Watcher对象)传入gerData方法中。
当用get /tce watch 注册关于数据内容变化的watcher时间时,服务端中processRequest 方法会case到getData方法,然后进行Watcher的注册
和客户端类似,服务端也有一个WatchManager,是服务端watcher的管理者,服务端会把watcher对象(指ServerCnxn)存储到WatchManager的watchTable和watch2Paths中。watchTable和watch2Paths分别从两个维度进行watcher的存储,watchTable是从数据结点路径的粒度来管理watcher,watch2Paths是从watcher的粒度来控制事件需要出发的数据结点。WatchManager还负责Watcher事件的触发,并移除已经被触发的watcher,在服务端有两者WatchManager,一种是这里的dataWatches对应的WatchManager,还有一种是childWatches对应的WatchManager,这两类WatchManager由DataTree负责托管。下面是WatcherManager的类图:
当注册好watcher时候,剩下服务端对watcher的操作就是watcher的触发,接下来看一下服务端是如何触发watcher事件的,首先客户端发起setData请求,然后服务端会调用setData方法,在setData方法里面会触发已经注册好的watcher,下面是服务端的setData方法,在触发watcher之前添加一句System.out语句然后重新编译部署测试一下
执行set /tce 456操作
setData方法触发了NodeDataChanged事件
watcher事件触发之后会调用triggerWatch方法,然后可以看到zookeeper会将watchTable和watch2Paths里的watcher进行remove,所以可以肯定的是watcher在服务端是一次性的,触发之后就失效了
无论是dataWatches还是childWatches,Watcher触发的逻辑大致是一样的基本步骤如下:
1.将通知状态(KeeperState)、事件类型(EventType)以及节点路径(Path)封装成一个WatchEvent对象
2.根据节点路径在watchTable中取出dui对应的watcher,如果没有找到则说明没有注册过watcher,直接退出,如果找到了会将其get出来,同时删除watchTable和watch2Paths中的wawatcher删除
3.调用process方法来触发Watcher,向客户端发送通知
客户端回调Watcher
客户端会通过使用ServerCxnServerCnxn对应的TCP连接来向客户端发送一个WatcherEvent事件,然后客户端中的SendThread就会接收并处理这个事件通知
zookeeper客户端接收到请求后会先将字节流反序列化成WatcherEvent对象,然后服务端传过来的完整界定路径进行chrootPath处理,生成客户端的一个相对路径,之后再把WatcherEvent对象转换成WatchedEvent,最后的回调Watcher是将WatchedEvent对象交给EventThread线程,在下一个轮询周期中进行Watcher回调。
EventThread线程是zookeeper客户端用来专门处理服务通知事件的线程,EventThread中的queueEvent方法会根据该通知事件,从中ZKWatchManager中取出所有有关watcher(同时删除对应的watcher,说明了客户端watcher也是一次性的),在获取到之后放到待处理watcher的WaitingEvent队例中,EventThread线程的run方法会不断的对该队列进行处理。
EventThread的queueEvent方法:
EventThread线程的run方法不断轮询然后回调对应watcher的process方法
下面我们来通过实际操作验证一下:
先在一个客户端get /tce watch注册监听
随后用另一个客户端set /tce 123修改节点的数据内容触发watcher事件
然后我们可以在原来的客户端看到刚才添加的注释信息,证明客户端的回调是交给EventThread进行处理的
zookeeper服务器类型
Leader:整个zookeeper集群工作机制中的核心,主要工作有
- 事物请求的唯一调度和请求者,保证集群事物处理的顺序性
- 集群内部服务器的调度者
Follower:zookeeper集群状态的跟随者
- 处理客户端非事务请求,转发事务请求给Leader服务器
- 参与事务请求Proposal的投票
- 参数Leader选举投票
Observer:在集群中充当观察者的角色,工作原理基本上和Follower服务器一样,唯一的区别就是Observer不参与任何形式的投票。
Leader选举
服务器启动时的Leader选举
1.每个server会发出一个投票,刚开始每一个服务器会把主机当做Leader服务器来进行投票,也就是选自己给自己投票,投票的最基本信息至少包括所推举的服务器的myid和ZXID,可以采用(myid,ZXID)来表示,然后把给自己的这个投票发给集群中的其他机器。
2.接收来自各个服务器的投票,在接收之后会判断该投票的有效性,包括检查是否是本轮投票,是否来自Looking状态的服务器。
3.处理投票,将自己的投票与自己的投票进行对比,先对比ZXID,ZXID比较大的优先作为Leader,如果ZXID系统,则通过对比myid的大小,myid大的作为Leader。对比之后更新自己的投票为信息,如果本来自己的投票信息就是对比的Leader结果则不需要更新。
4.统计投票,每次投票之后服务器都会统计所有的投票,判断是否有过半的机器接收到了相同的投票信息,如果有则过半投票中myid对应的服务器为Leader。
5.改变服务器状态,一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower则状态为following,如果是Leader则为leading。
服务器运行时的Leader选举
当集群中的Leader服务器挂掉之后,集群无法对外提供服务,就会进入新一轮的Leader选举。服务器运行时的Leader选举和启动时的Leader选举差不多,不同的是在上面服务器启动时Leader选举的第一步之前所有非Observer服务器会变更自己的服务器状态为Looking,然后进入和服务器启动时的Leader选举一样的流程。
zookeeper典型应用场景
数据发布/订阅(配置中心)
数据发布/订阅即配置中心,可以同消息队列中的生产者消费者模式进行类比。数据的发布者将数据发布到zookeeper节点上,数据的订阅者可以进行动态地订阅,实现配置信息的集中式管理和动态更新。数据的发布/订阅一般有两种模式:推(push)模式和拉(pull)模式,在zookeeper中采用两种模式结合实现,客户端向服务端注册自己需要关注的节点,一旦该节点发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主要到服务端或区域最新的数据。
命名服务
在传统的关系型数据库中是利用主键来唯一的作为一条记录的ID,但是在分布式场景中出现分库分表的时候就无法利用主键来进行唯一的标识。zookeeper中可以创建顺序节点,在创建时返回带顺序的自增节点的名称,使用这个特性可以帮助是吸纳分布式场景下的命名服务。
分布式协调/通知
ZooKeeper中特有watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。使用方法通常是不同系统都对zookeeper上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理。其中可以实现的典型场景有心跳检测机制、调度系统、任务分发系统等,总之,使用zookeeper来进行分布式通知和协调能够大大降低系统之间的耦合。
集群管理
集群管理中包括集群监控和集群控制两大块。zookeeper上的临时节点会随着客户端和服务端的会话失效自动被清除,通过创建临时节点,加上watcher事件,当临时节点被清除时就意味着机器的不再存活,利用这两个特性我们可以实现集群监控,从而实现监控集群中机器的存活性。除此之外还可以进行在自动化运维中还可以监控机器的上/下线。
Master选举
Master是在分布式系统中非常常见的一个场景。在一些场景Master负责处理一些负责的逻辑,另一些场景中负责处理客户端的写请求。利用zookeeper的强一致性可以实现Master选举。所谓zookeeper的强一致性就是zookeeper保证在高并发情况下,如果有多个客户端同时请求创建同一个节点,那么最终一定只有一个客户端请求创建成功。
分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式,从而保证分布式资源的一致性。zookeeper可以实现排他锁和共享锁。排他锁的原理是利用同一时刻多个客户端创建同一个临时节点时只能有一个客户端创建成功的方式来实现获取锁,释放锁的话则是通过删除临时节点或者等会话失效时自动清除临时节点。共享锁的原理则是创建多个顺序临时节点,当是读请求的时候创建顺序节点时判断该顺序临时节点的前一个顺序临时节点是否是读请求,如果是则获得共享锁,如果不是则等待。当时写请求时同样判断前一个顺序临时节点,不管前一个顺序临时节点是写请求还是读请求,只有当前一个顺序临时节点被清除(即释放锁)才能获取到共享锁。
分布式队列
通过创建顺序临时节点可以实现FIFO队列,当处理完一个请求时即自动删除一个临时节点,然后处理下一个请求。同样可以首先在某个特定节点存储Barrier值,然后创建顺序节点,利用watcher监听节点个数的变化,同Barrier值进行对比,来实现分布式屏障。