ZooKeeper学习笔记
特点
一个leader
多个follower
组成的集群,只要半数服务器有效,则集群有效。
- 顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到
ZooKeeper
中去。 - 原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
- 全局数据一致: 无论客户端连到哪一个
ZooKeeper
服务器上,其看到的服务端数据模型都是一致的。 - 可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。
- 实时性:在一段时间内,客户端可以读取到最新的数据(服务器间同步很快)
体系结构
事实上ZooKeeper
底层只提供了两个功能:
- 管理(存储,读取)用户程序提交的数据
- 为用户程序提交数据节点监听服务
由若干服务器构成,每台服务器内存中维护相同的类似于文件系统的树形结构。其中一台通过ZAB
原子广播选举主控服务器,其他的作为从属服务器。
客户端可以通过TCP
协议连接任意一台服务器,使用这个TCP
连接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。但是:
- 每一台从服务器都可以直接响应读请求
- 如果是更新或者写数据操作,则必须由主控服务器进行协调更新(客户端如果连接的是从服务器,则请求会被转发至主控服务器上,由其完成)
因此,ZooKeeper
任意一台服务器都可以响应读操作,这是吞吐量高的主要原因(数据存储在内存中,因此它也是低延迟的)。并且,在读多于写的应用程序中性能更高。
但潜在的问题是,即使主控服务器已经更新了内存数据,但是ZAB
协议还未能将其广播到从属服务器时,客户端可能会读到过期的数据。所以在API
中提供了sync
操作:接收到sync
命令的从属服务器从主控服务器同步状态信息,保证两者完全一致。因此只要在读操作前调用sync
,可以保证客户端读到最新状态的数据。
服务器在响应请求时,会给客户端分配一个渐增的zxid
格式的时间戳(包括了节点创建时间cZxid
、节点修改时间mZxid
、子节点修改或者创建时间pZxid
),这个时间戳全局有序,实际上是64位的数字,高32位是epoch
,用来标识leader
是否发生改变,低32
位用来递增计数。客户端会在请求中携带该信息,如果发生了服务器切换,当服务器发现自身的zxid
比客户端发来的zxid
要低,表明服务器数据过期,需要同步。
ZooKeeper
的容错是通过重放日志(Replay log
)和模糊快照(Fuzzy Snapshot
)实现的:
- 重放日志:将更新操作体现在内存数据之前先写入外存日志中避免数据丢失
- 模糊快照:周期性对内存数据进行数据快照,并不对内存数据加锁,而是用深度遍历的方式将内存中的属性结构转入外存快照数据中。因而快照数据可能与内存中并不一致,是模糊的。
ZooKeeper
可以保证数据更新操作是幂等的,只要保证操作顺序一致,即使多次执行统一操作对最终结果没有影响。加载模糊快照,利用重放日志重新执行一遍,系统会恢复到最新状态。
数据结构
ACL
ZooKeeper
采用ACL
(Access Controll List
,访问控制)策略进行权限控制,共有五种权限:
create
:创建子节点read
:获取节点数据和子节点列表write
:更新节点数据delete
:删除子节点admin
:设置节点ACL
权限
Znode
类似于传统的文件系统模式,由树形的层级目录结构组成,节点称之为Znode
。每个ZNode
默认可以存储1MB
的数据。节点既可以是文件,也可以是目录。它有两种类型:持久节点和临时节点。
对应于每个Znode
,Zookeeper
都会为其维护一个叫作Stat
的数据结构,Stat
中记录了这个Znode
的三个数据版本,分别是dataVersion
(每次对节点进行set
操作,数据版本值加)、cVersion
(当子节点有变化时,值加1)和aclVersion
(当前Znode
的访问控制版本号)。
dataVersion
实际上是乐观锁。
持久节点不论客户端会话情况,一直存在,只有当客户端显式调用删除操作才会消失。临时节点会在会话结束后,自动由ZooKeeper
删除。
在
ZooKeeper
中,一个客户端连接是指客户端和服务器之间的一个TCP
长连接。通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper
服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch
事件通知。 会话的sessionTimeout
值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在sessionTimeout
规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。在为客户端创建会话之前,服务端首先会为每个客户端都分配一个
sessionID
。由于sessionID
是Zookeeper
会话的一个重要标识,许多与会话相关的运行机制都是基于这个sessionID
的,因此,无论是哪台服务器为客户端分配的sessionID
,都务必保证全局唯一。
API
getData
、exists
和getChildren
可以设置观察标识,如果观察标识watch
为true
,则当节点内容发生变化时,ZooKeeper
会主动通知客户端进程,但是并不会将变化内容数据推送过来。
应用场景
领导者选举
当主从结构中主节点发生故障,需要由某台备机成为新的主控机,这个过程被称为领导者选举。
ZooKeeper
将l
节点设置为领导专用节点,存储领导者的地址信息以及其他辅助信息。当进程p
尝试读取l
,并设置观察标识。
- 如果读取成功,说明已有领导者,并且从读取结果中获得领导者相关信息。
- 如果读取失败,说明无领导者。
p
尝试创建l
节点,如果创建成功,其他节点因为设置了观察标识,ZooKeeper
会通知其他节点领导者发生了故障。 - 如果
l
节点失效了,ZooKeeper
会通知其他非领导节点,其他节点再按此步骤进行选举竞争。
配置管理
配置文件存储在ZooKeeper
的某个节点c
中,分布式系统中的客户端进程启动时从该节点读取配置信息并设置观察标记。若配置文件内容被改变,客户端会接收到变化通知。
组成员管理
利用临时节点进行组成员管理,建立一个g
节点代表某个群组,某个组成员加入在g
下创建临时子节点。负责组成员管理的监控进程可以使用getChildren
获得组下所有成员信息并设置观察标识,当后续有新节点加入或者退出时会获得通知消息。
任务分配
监控进程创建任务队列管理节点tasks
,所有新进入系统的任务都可以在tasks
节点下创建子节点。当有新任务task-j
时,ZooKeeper
通知监控进程,监控进程将其分配给机器i
,然后在machines
目录下对应的m-i
节点创建子节点task-j
。m-i
节点对应的服务器会监听节点的变化,发现有新增节点时说明有新分配的任务,读出并执行后,将其删除,同时删除tasks
节点下的task-j
节点。
锁管理(实现分布式锁)
首先设立节点l
作为锁标记,每个进程在节点下创立一个临时自增节点,这样各个进程按照建立子节点的先后顺序在名字上进行编号,如果当前进程发现自己编号是l
节点下最小的编号节点,那么它就获得了锁。
由于创建的是临时节点,客户端如果发生宕机,过了一定时间
ZooKeeper
没有收到客户端的心跳包会判断会话失效,将临时节点删除从而释放锁。同时,
ZooKeeper
提供的API
中,读取子节点列表(判断自己是否是序号最小的节点)与设置监听器的操作是原子执行的,从而可以保证不会丢失事件。
写锁(排他锁):判断l
下的节点是否存在编号比自己小1
的子节点,并设置观察标识。如果不存在,可以直接获得锁,如果存在则进行等待。
读锁(并发锁):判断l
下的节点是否存在编号比自己小的子节点持有写锁,如果没有写锁可以直接获得锁;否则监听离自己最近的写锁并进行等待。
双向路障同步
路障同步是指多个并发进程都要到达某个同步点后再继续向后推进。
利用某个节点b
代表路障,每个节点通过在b
下创建子节点表明自己已经到达了同步点,离开时通过删除自己节点表明自己准备离开同步点。因此,在开始时,可以判断b
下的节点是否到达一定数量,从而判断同步条件是否满足,可以开始运行。如果需要对结束时进行同步,那么判断b
下的节点个数是否已经到达0
。
其他还包括命名服务,负载均衡等。
集群角色
ZooKeeper
中没有选择传统的Master/Slave
概念,而是引入了Leader
、Follower
和Observer
三种角色。
ZooKeeper
集群中的所有机器通过一个Leader
选举过程来选定一台称为“Leader”
的机器,Leader
既可以为客户端提供写服务又能提供读服务。除了 Leader
外,Follower
和Observer
都只能提供读服务。Follower
和Observer
唯一的区别在于Observer
机器不参与Leader
的选举过程,也不参与写操作的“过半写成功”策略,因此Observer
机器可以在不影响写性能的情况下提升集群的读性能。
半数机制
我们知道在Zookeeper
中Leader
选举算法采用了Zab
协议。Zab
核心思想是当多数Server
写成功,则任务数据写成功。
- 如果有3个
Server
,则最多允许1个Server
挂掉。 - 如果有4个
Server
,则同样最多允许1个Server
挂掉
因此选奇数个Server
即可。
ZAB协议
ZooKeeper
基于paxos
算法,但是也拥有自己的核心算法ZAB
(原子消息广播协议),包括消息广播和崩溃恢复两个方面。
消息广播:
- 在广播事务之前
Leader
服务器会先给这个事务分配一个全局单调递增的唯一ID
,也就是zxid
,每一个事务必须按照zxid
的先后顺序进行处理。 Leader
通过先进先出队列将带有 zxid 的消息作为一个提案(proposal
)分发给所有follower
。Follower
服务器在接收到proposal
之后,都会将其以事务日志的形式写入到本地磁盘中,成功写入后反馈给Leader
一个ACK
。- 当
Leader
收到半数ACK
响应之后,就会广播一个Commit
消息给所有Follower
,通知它们进行提交,同时Leader
也会完成自身的提交。 - 当
follower
收到COMMIT
时,会执行该消息
因此follower
要么回ACK
给Leader
,要么抛弃Leader
,这样可能会导致某一时刻leader
和follwer
状态不一致。因此ZAB
协议还有恢复模式。
崩溃恢复:
- 情况一:
leader
收到合法数量的ACK
后,开始广播COMMIT
命令,但是在所有follower
都收到之前出现宕机,于是剩下的服务器没有执行这条命令。此时:- 选举拥有
zxid
最大的节点作为leader
(此节点必为保存了所有COMMIT
消息的) - 新的
leader
将自己事务日志中未COMMIT
的消息处理 - 新的
leader
将自身有follwer
没有的proposal
发送给follower
,并将COMMIT
命令发送给所有follower
,保证所有follower
都处理了消息。最终实现已经处理的消息不会丢
- 选举拥有
- 情况二:
leader
还没发送COMMIT
就已经宕机了,当leader
重新上线成为follower
之后,会丢弃被跳过的proposal
用以保持与整个系统一致。
leader
选举
服务器启动时的选举
- 每台服务器都会发出投票,投给自己
- 每台服务器也会受到其他服务器的投票请求,首先比较
zxid
,如果对方zxid
较大,则会认可把投票发送出去。如果zxid
相同,则比较myid
。 - 如果服务器收到了超过半数机器相同的投票信息,那么它将成为
leader
。 - 一旦确定了
leader
,每个服务器就会更新自己的状态,如果是follower
,那么就变更为FOLLOWING
,如果是leader
,就变更为LEADING
。
对于后面启动的较晚的机器,此时集群已经正常工作,因此只需要和Leader
机器简历连接,进行状态同步。
运行期间的选举
leader
宕机后,follower
会变更服务器状态为LOOKING
,进入选举- 运行期间,每个服务器上的
zxid
可能不同,每个服务器在第一轮投票也同样地会投给自己。然后与启动时投票类似。
可见每次投票都是对(vote_sid, vote_zxid)
和(self_sid, self_zxid)
对比的过程。
监听机制
- 仅触发一次:当数据改变时,一个监听事件被发送到客户端,并取消监听,除非客户端再次设置监听,否则不再监听,所以在回调中应该添加监听。
- 服务端维护了两个监听队列:数据监听队列和孩子监听队列。
getData()
和exists()
设置数据监听,getChildren()
设置孩子监听。setData()
将触发数据监听,create()
和delete()
将触发一个节点被创建的数据监听和孩子变化的孩子监听。 - 当一个客户端连接到一个新服务端时,监听将被触发。在客户端与服务端断开连接后,监听将不能发送到客户端,当客户端重连后,任何先前的注册监听将自动重新注册并根据情况触发,整个过程自动完成。
- 存在一种情况将出现监听事件丢失:当客户端与服务端断开连接期间,一个被监听的节点创建并且被删除,客户端将收不到任何监听事件。
ZooKeeper
服务端的监听列表仅保存在内存中,不做持久化。当一个客户端与服务端断开连接后,它所有的watch
都会从内存中移除,客户端会在重连后自动重新注册它的所有watch
。
References
:
《大数据日知录》p96-p104