深入浅出ZooKeeper
概述
Zookeeper
是分布式调度服务。在分布式系统当中,当集群中的部分节点出现故障,那么需要通知其他节点。Zookeeper
主要就是用来解决这个问题
实例
以上将编写Zookeeper
客户端程序,来展示其是如何管理集群中的节点
组和成员
Zookeeper
可以看成是一个高可用的文件系统,但它没有文件和文件夹的概念,其是一个树状结构。它的基本单元是znode
,其主要用来存储数据。
创建组
1 |
|
创建一个CreateGroup
对象,然后调用connect
方法,通过ZooKeeper
的api
与ZooKeeper
服务器连接。指定三个参数,服务端主机名以及端口,连接超时时间,Watcher
接口的实例,Watcher
主要是负责接收ZooKeeper
数据变化时产生的事件回调。
创建之后使用CountDownLatch
,让当前线程等待,直到ZooKeeper
准备就绪。当ZooKeeper
准备就绪时,Watcher
接口的方法process
会回调连接成功事件,然后CountDownLatch
释放。
连接成功之后创建znode
组,可以指定path
,内容
和ACL
。znode
的分为ephemeral
和persistent
两种,使用ephemeral
时,创建znode
的客户端的会话结束或者客户端与服务端断开时,该znode
会被自动删除。而persistent
的znode
,除非是客户端主动删除,否则永远存在
加入组
1 | class JoinGroup : ConnectionWatcher() { |
加入组与创建组类似,都需要先连接Zookeeper
服务器,然后创建EPHEMERAL
类型的znode
。这样当客户端断开时会自动删除该znode
成员列表
1 | class ListGroup : ConnectionWatcher() { |
调用Zookeeper
的getChildren
方法来获取某个path
下的节点,如果不存在节点,那么会抛出NoNodeException
Zookeeper服务模型
数据模型
Zookeeper
包含了一个树形的数据模型,那就是znode
。一个znode
中包含了存储的数据和ACL
。Zookeeper
的数据模型适合存储少量的数据,单个znode
不能存储超过1M
的数据
数据的访问具有原子性。意味着从znode
中获取的数据永远都是完整的。不可能出现部分数据的情况。znode
的路径必须是全路径,是简单的字符串,默认有个叫zookeeper
的根节点,用来存储一些管理数据
ephemeral(临时)znodes
ephemeral
节点,当客户端的session
结束了,该节点会被自动删除。而persistent
节点创建之后就跟客户端没有关系了,需要客户端主动删除。ephemeral
节点不会有子节点,其对所有客户端可见,但只绑定创建者客户端
序号
创建znode
时,会在名字后面增加一个数字。这就是每个节点的序号。例如
创建一个znode
指定命名为/a/b-
,那么最后创建完的名字是/a/b-3
,再创建一个/a/b-
的节点,那么最后的名字是/a/b-5
。调用create
方法的返回值是znode
的真实名字。这些序号主要是用来排序的
观察模式 Watches
观察模式可以使客户端在某个节点发生变化时得到通知。例如,客户端对某个znode
进行了exist
操作,同时在znode
上开启了观察模式,如果znode
不存在,这个exist
操作将返回false
。之后如果客户端创建了这个znode
,观察者模式将触发,并通知开启了观察者模式的客户端。观察者模式只能被触发一次,如果要持续对某个节点进行观察,那么需要持续对该节点开启观察者模式
操作
操作 | 说明 |
---|---|
create | 创建znode |
delete | 删除znode |
exists | 判断znode是否存在 |
getACL, setACL | 获取znode访问控制列表 |
getChildren | 获取znode的子节点 |
getData, setData | 设置或获取znode关联节点的数据 |
sync | 同步客户端和服务端znode的状态 |
在调用delete
和setData
时,必须指定znode
数据的版本号。Zookeeper
支持将多个操作组合成一个操作单元
Zookeeper
支持同步和异步操作。
读操作会在znode
上开启观察模式,并且写操作会触发观察模式。而写操作不会启动观察者模式
exists
启动的观察模式,由create
,setData
,delete
来触发getData
启动的观察模式, 由delete
,setData
来触发,create
不会触发getChildren
启动的观察者模式,由子节点创建和删除,或者本节点被删除才会触发
znode
被创建时,赋予了ACL
。通过如下几种方式来鉴权
- digest: 用户名和密码
- sast: 使用
Kerberos
鉴权 - ip: 使用客户端
ip
鉴权
1 | ACL(Perms.READ, Id("ip", "10.0.0.1")) |
exist
不受ACL
控制,所有的客户端均可操作
实现
Zookeeper
服务可以在两种模式下运行。standalone
模式下,可以运行一个单独的Zookeeper
服务器。在生产环境中,会采用replicated
模式安装在多台服务器上,组建ensemble
集群。Zookeeper
和它的副本组成高可用的集群,只要ensemble
能够选举出主服务器,那么Zookeeper
就不会中断。例如在一个5节点的ensemble
中,能够容忍两个节点脱离集群,服务还是可用的。因为剩下的三个节点投票,可以产生超过集群半数的投票,来推选一台服务器。而6个节点的服务器,最多也只能容忍2个节点脱离集群,因为剩下的三个节点无法产生超过集群半数的投票。所以一般ensemble
中节点的数量都是奇数。
Zookeeper
做的是保证每一次对znode
树的修改,能够复制到ensemble
集群中的大多数节点中。如果非主服务器脱离集群,那么剩下的服务器也能很快更新到最新的状态
Zookeeper
使用Zab
协议。这个协议包括两个阶段
- 领导选举:
ensumble
集群选举出一个leader
节点。其他节点为follower
。当大多数follower
与leader
状态完成同步,那么这个阶段完成 - 原子广播:
所有的写入请求都会发送给leader
,leader
在广播给follower
。当大多数follower
完成了数据同步,leader
才会更新提交。
如果之前的leader
回到集群,那么会被当做是一个follower
。leader
选举很快,大概200ms就能够产生结果,所以不会影响效率
ensemble
的所有节点都会在更新内存中的znode
树的副本之前,先将更新数据写入到硬盘上
数据一致性
ensemble
集群中follower
的update
操作会滞后于leader
操作。Zookeeper
客户端的最佳实践是全部链接到follower
上。然而客户端是有可能链接到leader
上的,并且客户端控制不了这个选择,客户端也不知道连接到follower
还是leader
。一般的操作都是读直接从follower
读取,写需要写入到leader
。这跟数据库的读写分离类似
每一个对znode
树的更新操作,都会被赋予一个全局的唯一ID
,我们称为zxid
。更新操作的ID
按照发生的时间顺序升序排序
Zookeeper
在数据一致性上实现了以下几方面:
- 顺序一致性:
从客户端提交的更新操作都是按照先后循环排序的 - 原子性
更新操作只有成功和失败 - 系统视图唯一性
无论客户端连接到哪个服务器,都将看见唯一的系统视图。客户端连接的服务器状态永远都是最新的 - 持久性
一旦操作成功,数据将被持久化到服务器上,并且不能撤销,所以服务器宕机重启,也不会影响数据 - 时效性
系统视图的状态更新的延迟时间是有一个上限的,最多不过几十秒。如果服务器的状态落后于其他服务器太多,ZooKeeper
宁可关闭这个服务器上的服务,强制客户端去连接一个状态更新的服务器
为了保证数据的一致性,那么客户端在读取数据的时候,需要先调用sync
来同步状态
会话Session
Zookeeper
客户端中,配置了一个ensemble
服务器列表。当启动时,首先尝试连接其中一个服务器,如果连接失败,那么会尝试连接下一个,直到成功或者全部失败
一旦连接成功,服务器就会为客户端创建一个会话(session)。session的过期时间由创建会话的客户端应用来设定,如果在这个时间期间,服务器没有收到客户端的任何请求,那么session将被视为过期,并且这个session不能被重新创建,而创建的ephemeral znode将随着session过期被删除掉。在会话长期存在的情况下,session的过期事件是比较少见的,但是应用程序如何处理好这个事件是很重要的。
在长时间的空闲情况下,客户端会不断的发送ping请求来保持session。ping请求的间隔被设置成足够短,以便能够及时发现服务器失败(由读操作的超时时长来设置),并且能够及时的在session过期前连接到其他服务器上。
容错连接到其他服务器上,是由ZooKeeper客户端自动完成的。重要的是在连接到其他服务器上后,之前的session以及epemeral节点还保持可用状态。
在容错的过程中,应用将收到与服务断开连接和连接的通知。Watch模式的通知在断开链接时,是不会发送断开连接事件给客户端的,断开连接事件是在重新连接成功后发送给客户端的。如果在重新连接到其他节点时,应用尝试一个操作,这个操作是一定会失败的。对于这一点的处理,是一个ZooKeeper应用的重点。
time
在ZooKeeper中有一些时间的参数。tick是ZooKeeper的基础时间单位,用来定义ensemble中服务器上运行的程序的时间表。其他时间相关的配置都是以tick为单位的,或者以tick的值为最大值或者最小值。例如,session的过期时间在2 ticks到20 ticks之间,那么你再设置时选择的session过期时间必须在2和20之间的一个数。
通常情况1 tick等于2秒。那么就是说session的过期时间的设置范围在4秒到40秒之间。在session过期时间的设置上有一些考虑。过期时间太短会造成加快物理失败的监测频率。在组成员关系的例子中,session的过期时间与从组中移除失败的成员花费的时间相等。如果设置过低的session过期时间,那么网络延迟就有可能造成非预期的session过期。这种情况下,就会出现在短时间内一台机器不断的离开组,然后又从新加入组中。
如果应用需要创建比较复杂的临时状态,那么就需要较长的session过期时间,因为重构花费的时间比较长。有一些情况下,需要在session的生命周期内重启,而且要保证重启完后session不过期(例如,应用维护和升级的情况)。服务器会给每一个session一个ID和密码,如果在连接创建时,ZooKeeper验证通过,那么session将被恢复使用(只要session没过期就行)。所以应用程序可以实现一个优雅的关机动作,在重启之前,将session的ID和密码存储在一个稳定的地方。重启之后,通过ID和密码恢复session。
在一些特殊的情况下,我们需要使用这个特性来使用比较长的session过期时间。大多数情况下,我们还是要考虑当出现非预期的异常失败时,如何处理session过期,或者仅需要优雅的关闭应用,在session过期前不用重启应用。
通常情况也越大规模的ensemble,就需要越长的session过期时间。Connetction Timeout、Read Timeout和Ping Periods都由一个以服务器数量为参数的函数计算得到,当ensemble的规模扩大,这些值需要逐渐减小。
状态
ZooKeeper
对象在他的生命周期内会有不同的状态,我们通过getState()
来获取当前的状态
1 | getState(): States |
新构建的ZooKeeper
对象在尝试连接ZooKeeper
服务时的状态是CONNECTING
,一旦建立成功,那么状态变成CONNECTED