基础
数据模型
ZooKeeper数据模型的每个节点称之为ZNode,节点可以保存数据,也可以挂载子节点,形成一个树形结构。
节点类型
持久节点:被创建后一直存在,需要通过delete删除
临时节点:生命周期与客户端会话绑定,当客户端会话失效,节点自动清除,也可以通过delete主动删除,只能作为叶子节点。
顺序节点:一个顺序节点被分配唯一的单调递增的整数。当创建有顺序点,一个序号会被追加到路径之后。
持久/临时&顺序可以组合,所以节点类型一共有四种:持久、临时、持久顺序和临时顺序。
节点版本
每个znode节点都有一个版本号,它随着每次数据变更而自增。
setData和delete调用的时候可以传入版本号参数,只有传入的版本号与服务器上的版本号一直时,调用才会成功。
节点相关API
创建节点:create /path data
删除节点:delete /path
节点是否存在:exist /path
设置节点数据:setData /path data
获取节点数据:getData /path
获取子节点:getChildren /path
事务
ZooKeeper的事务是用multiop实现,multiop里的所有操作,要么都成功,要么都失败。
使用事务有两个好处:
- 简化复杂性:不用考虑多个操作中,部分成功部分失败的解决方案。
- 修改前检查znode节点版本:例如存在节点A、B 节点B修改之前,需要查询节点A的数据。 节点A的数据没有发生变化的情况下,才能修改节点B的数据。 multiop包含两个内容 1、check 节点A的版本 2、setData 节点B的内容。
监视与通知
客户端向ZooKeeper注册需要接收通知的znode,通过对znode设置监视点来接收通知。监视点是一个单次触发的操作。
术语
- 事件:表示一个znode节点执行了更新操作
- 监视点:标识一个与之关联的zonde节点和事件类型组成的单次触发器
- 通知:当一个监视点被一个事件触发时,就会产生一个通知。通知是注册了监视点的应用客户端收到事件报告的消息。
事件类型
- NodeCreated,节点创建事件,可以用exists调用设置监视点
- NodeDeleted,节点删除事件,可以用exists或getData调用设置监视点
- NodeDataChanged,节点数据变更事件,可以用exists或getData调用设置监视点
- NodeChildrenChanged,子节点变更事件,可以用用getChildren调用设置监视点
重要知识
- 监视通知是单次触发的,会丢失事件——接收到通知,还未重新设置监视点的时候,数据又发生了变更。(可以通过从ZooKeeper服务器重新读取数据并设置新的监视点来避免这种问题,例如getData或getChildren)
- 监视点一旦创建就无法删除,只有2种情况会被移除:监视点被触发、创建监视点的会话过期或关闭。
- 利用监视点可以实现缓存,避免每次从ZooKeeper服务器读取数据——curator已经实现节点缓存。
- ZooKeeper保障客户端以全局的顺序来观察ZooKeeper的状态,即通知的顺序是按照数据变更的顺序。
- 设置一个监视点,会在ZooKeeper服务器上增加250-300字节的内存消耗,过多的监视点会加大服务器的内存压力。
- 羊群效应,客户端在1个节点上设置了10000个监视点,当节点发生变化,就需要触发10000个通知。
会话
状态
- NOT_CONNECTED:一个会话,从NOT_CONNECTED状态开始
- CONNECTING:客户端初始化后切换到CONNECTING
- CONNECTED:成功与ZooKeeper服务器建立连接后,切换到CONNECTED状态
- CLOSED:当客户端与ZooKeeper服务器断开连接或无法收到服务器响应时,会切换回CONNECTING状态。并尝试发现其他服务器,如果能发现另外一个服务器或者重连到原来的服务器,会话又会切换回CONNECTED。否则会声明会话过期,然后切换到CLOSED状态。应用也可以显式地关闭会话,那么会话的状态也会变为CLOSED。
属性
- sessionID:会话ID,唯一标识一个会话,每次客户端创建新的会话时,Zookeeper都会为其分配一个全局唯一的sessionID。
- TimeOut:会话超时时间,客户端在构造Zookeeper实例时,会配置sessionTimeout参数用于指定会话的超时时间,Zookeeper客户端向服务端发送这个超时时间后,服务端会根据自己的超时时间限制最终确定会话的超时时间。
- TickTime:下次会话超时时间点,为了便于Zookeeper对会话实行"分桶策略"管理,同时为了高效低耗地实现会话的超时检查与清理,Zookeeper会为每个会话标记一个下次会话超时时间点,其值大致等于当前时间加上TimeOut。
- isClosing:标记一个会话是否已经被关闭,当服务端检测到会话已经超时失效时,会将该会话的isClosing标记为"已关闭",这样就能确保不再处理来自该会话的新请求了。
服务器
角色
- Leader,事务请求的唯一调度和处理者,保证集群事务处理的顺序性;集群内部各服务器的调度者。
- Follower,处理客户端非事务请求,转发事务请求给Leader服务器;参与事务请求提议的投票;参与Leader选举。
- Observer,处理客户端非事务请求,转发事务请求给Leader服务器。
状态
- looking,集群中没有leader,选举中
- following,服务器角色是Follower
- leading,服务器角色是Leader
- observing,服务器角色是observer
运行模式
- 独立模式:一个单独的服务器
- 仲裁模式——集群:有一组ZooKeeper服务器,他们之前可以进行状态的复制,并同时服务于客户端的请求。
选举
服务器获得法定数量的服务器认可,才可以成为Leader,法定人数一般的集群机器数过半,例如集群机器数是3,法定人数就是2(不是一定的,利用分组功能,可以实现法定人数不用过半数)。过半数的好处有2个:
- 避免发生脑裂,避免出现多个Leader
- 半数支持者中,一定有一个提交了最新的事务——事务的同步,就是要半数服务器接受了Leader的提案。
流程
容错
节点发现自己被选为leader后,会等到200ms,没有变化才真正成为leader。防止因为网络原因,导致部分选票没有到达,选出了错误的leader。
请求处理
读请求
Leader/Follower/Observer都可直接处理读请求,从本地内存中读取数据并返回给客户端。
写请求
只有Leader能处理写请求,Follower和Observer接收到写请求后,都需要转发给Leader处理。
事务
每一个写操作,对应着一个事务,事务由两部分组成:
- 事务id—zxid,共64位,事务id由两部分组成:
时间戳(epoch):32位,更换leader的时候,就会更换
计数器(counter):32位,同一个epoch周期内的递增数字 - 数据
流程—ZooKeeper原子广播协议ZAB
假设接收到请求的是Follower
- Follower将写请求转发给Leader
- Leader向所有Follower发送一个PROPOSAL消息
- 当Follower接收到消息p后,会相应Leader一个ACK消息,通知leader已经接收PROPOSAL
- 当收到仲裁数量的Follower的ACK消息,leader就发送消息通知Follower提交操作
- Follower提交之后,就通知Client写操作执行完毕
保障
- 如果leader按顺序广播的事务T1和T2,那么每个服务器在提交事务T2之前保证事务T1已经提交完成
- 如果某个服务器按照T1、T2的顺序表提交事务,所有其他服务器也必然会在提交事务T2前提交事务T1
- 一个被选举的leader在提交完所有之前的epoch内的事务后,才会开始广播自己epoch内的事务
- leader的数据一定是所有机器里最新的,不需要从跟随者同步数据(选举的时候,有半数以上机器的支持者)
存储
日志
在接受一个提议时,一个服务器就会将提议的事务持久化到事务日志中,该事务日志保存在服务器的本地磁盘中,而事务将会按照顺序追加其后。服务器会是不是地滚动日志,即关闭当前文件并打开一个新的文件。
为了保证数据不丢失,ZooKeeper执行完事务之后,会让操作系统刷盘,把页缓存里的脏页刷到磁盘。
事务日志的性能优化:
- 组提交。一次性写入多个事务到磁盘,减少磁道寻址开销。
- 补白。预分配磁盘存储块。对于涉及存储块分配的文件系统元数据的更新,不会明显得影响文件的顺序写入操作。
快照
快照是ZooKeeper数据树的拷贝副本,每一个服务器会经常以序列化整个数据树的方式来提取快照。
故障恢复
可恢复的故障
活动死节点
旧主节点不知道自己被ZooKeeper服务器判定为会话超时,依然行驶主节点的职责。直到重连到ZooKeeper服务器后,才知道自己不是主节点了。
新选举出的主节点也在履行主节点的职责,两者可能造成冲突。
解决方式,当遇到Disconnected事件市,主节点应该停止所有主节点操作。
已存在的监视点与Disconnected事件
遇到Disconnected事件,客户端连接到新服务器后,会重建所有已经存在的监视点。
客户端会发送监视点列表和最后已知的zxid给服务器,服务器检查这些监视点并检查znode节点的修改时间戳和这些监视点是否对应,如果任何已经监视的znode的修改时间戳晚于最后已知的zxid,服务器会触发这个监视点。
对于exists设置的监视点,可能会出现ABA(创建了,后续又删除了)问题导致事件丢失。但是这种,似乎不严重,可以忽略?
不可恢复的故障
- 会话过期
- 已认证的会话无法再次与ZooKeeper完成认证
注意事项
1、恢复会话:
建议客户端不要使用任何之前从ZooKeeper获取的缓存状态,而是从服务器获取最新信息。
客户端崩溃时,可能有部分操作没有收到确认消息,无法知道是否完成。恢复后,应该进行相应的清理操作,以便完成某些未完成的任务。
2、当znode节点重新创建时,版本号会被重置。
3、顺序性保障
- 连接丢失,操作会报CONNECTIONLOSS事件,如果客户端实现重试,就可能导致后提交的操作,比先提交的执行。
- 多个线程并发调用同步API,ZooKeeper会按顺序返回多个线程的操作结果,但操作结果可能因为各种原因导致后提交的操作而先被执行。需要客户端自己注意这些操作的提交顺序。
- 同步和一部混合调用的顺序性无法保障。例子,异步操作提交了两个请求,Aop1和Aop2。在Aop1的回调函数中,进行一个同步调用,Sop1,该同步调用阻塞了ZooKeeper客户端的分发线程,这样就会导致客户端应用程序接收Sop1的结果之后才能接收Aop2的操作结果。
4、ZooKeeper对节点的数据大小和子节点数量有限制,例如,节点最多存储1MB的数据。
应用实例
黄色—持久节点,蓝色—临时节点,绿色—临时顺序节点
非公平锁
结构
描述
并发创建/lock/latch临时节点,谁创建成功,谁就获取到锁。
创建失败,则设置监听,收到/lock/latch节点被删除的通知后,再继续尝试创建/lock临时节点。
不断重复这一过程,直到创建节点成功。
公平锁
结构
描述
首先创建临时顺序节点,规定序号最小的节点获得锁,获取锁失败的客户端监听比比自己序号小的数据节点的删除事件,收到通知后,判断自己是否是最小节点来判断是否获取锁,当获取锁的客户端的会话失效后,节点自动删除,表示锁被释放。
主-从模式
结构
描述
角色:
- 主节点master——主节点服务监视新的从节点和任务,分配任务给可用的从节点。
- 从节点worker——通过系统注册自己,以确保主节点看到它们可以执行任务task,然后开始监视新任务。
- 客户端——创建新任务并等待系统的响应。
/root/master节点非公平锁,谁获取到了,谁就是主节点。成为master的节点,需要监听workers和tasks目录的变化。为有效的worker分配task。
/root/workers目录作为存活的从节点的容器。
每个存活的从节点(例如work-123),用自己的实例标识在/root/workers目录下创建临时子节点,代表自己活着;同时在/assign目录下用自己的实例标识创建一个持久节点,用于存储分配给自己的任务。
/root/tasks目录作为客户端提交的任务的容器,当客户端提交任务,就在该目录下创建一个子节点。
主节点监听到/root/tasks新增了自节点,就会从/root/works目录下的子节点中选择一个节点(假设选中的子节点为/work-123),再到/assign/work-123目录下创建对应的任务。
从节点work-123需要监听/assign/work-123的子节点变化,当新增子节点的时候,就知道主节点给它分配了任务,可以执行对应节点代表的任务。
从节点work-123执行完任务,就可以把/assign/work-123/task-1节点删除,并为/tasks/task-1节点创建一个子节点status,数据为done。
客户端监听到/tasks/task-1新增了子节点status,就可以根据status节点的数据内容得到当前任务的进度。
注册中心
结构
描述
服务提供者:在接口节点的providers节点下创建临时节点保存服务器ip及端口 服务消费者:订阅接口的providers的所有子节点,当有新的提供者加入或有节点退出,都能及时收到通知,并且在consumers节点下创建临时节点保存服务器地址。
分布式锁和外部资源
像java的客户端,在java进行垃圾回收的时候,会出现stop the world,如果这个时间很长,就会导致zookeeper服务器认为客户端会话超时。
这种情况,利用zk作为分布式锁的服务,就可能会出现两个实例在并行工作,导致问题——例如以前的Apache HBase。
可以使用一种名为隔离(fencing)的技巧。例如以创建znode时候的zxid—czxid来作为隔离符号。
当我们请求外部资源时,也提供这个隔离符号。如果外部资源已经收到更高版本的隔离符号的请求,我们的请求就会被拒绝。