参考:
- java guide : https://javaguide.cn/distributed-system/distributed-process-coordination/zookeeper/zookeeper-intro.html
- jsongj: http://www.jasongj.com/zookeeper/fastleaderelection/
- 倪超《从 Paxos 到 Zookeeper》
- 官方文档:https://zookeeper.apache.org/doc/current/zookeeperInternals.html
Zookeeper 基础
Zookeeper提供了一个类似于Linux文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制。
每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。
以 dubbo 为例,zookeeper 作为注册中心:
- 提供者节点: /GLOBAL_REGISTRY/tuling.dubbo.server.UserService/providers
- 存放的内容是一个所有注册的提供者的 URL List [dubbo://127.0.0.1:12345/tuling.dubbo.server.UserService?application=myapp1&dubbo=2.0.2&protocal=dubbo&..., dubbo://..... ]
- 消费者节点:/GLOBAL_REGISTRY/tuling.dubbo.server.UserService/consumers
- 存放的内容是一个所有注册的提供者的 URL List [consumer://127.0.0.1:12345/tuling.dubbo.server.UserService?application=myapp2&dubbo=2.0.2&protocal=dubbo&..., consumer://..... ]
- 提供者动态配置节点:/GLOBAL_REGISTRY/tuling.dubbo.server.UserService/configurators
- 消费者路由策略节点:/GLOBAL_REGISTRY/tuling.dubbo.server.UserService/routers
数据节点 znode
我们通常是将 znode 分为 4 大类:
- 持久(PERSISTENT)节点:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
- 临时(EPHEMERAL)节点:临时节点的生命周期是与 客户端会话(session) 绑定的,会话消失则节点消失 。并且,临时节点只能做叶子节点 ,不能创建子节点。
ZooKeeper 还允许用户为每个节点添加一个特殊的属性:SEQUENTIAL。一旦节点被标记上这个属性,那么在这个节点被创建的时候,ZooKeeper 会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。比如 /node1/app0000000001
、/node1/app0000000002
。它和 持久顺序节点、临时节点组合,又衍生出了:
- 持久顺序(PERSISTENT_SEQUENTIAL)节点:持久节点特性+ 子节点具有 SEQUENTIAL顺序属性
- 临时顺序(EPHEMERAL_SEQUENTIAL)节点:临时节点特性+ 子节点具有 SEQUENTIAL顺序属性
每个 znode 由 2 部分组成:
- stat:状态信息(stat /1/app1/p_1 可获取节点状态信息)
- data:节点存放的数据的具体内容(get /1/app1/p_1 可获取节点下数据)
数据节点状态信息
ACL(权限控制)
ZooKeeper 采用 ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。
Watcher(事件监听器)
Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
会话(Session)
Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watcher 事件通知。
Session 有一个属性叫做:sessionTimeout
,sessionTimeout
代表会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在sessionTimeout
规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。
另外,在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionID
。由于 sessionID
是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID
的,因此,无论是哪台服务器为客户端分配的 sessionID
,都务必保证全局唯一。
Zookeeper 集群
Zookeeper 集群的构建理论是 ZAB(ZooKeeper Atomic Broadcast 原子广播),而 ZAB 被认为是 Multi Paxos 的等价派生实现(Raft 也是Multi Paxos 的等价派生实现),Multi Paxos 是基于 Paxos 的应用改进。
Paxos: 《https://www.cnblogs.com/suBlog/p/17554677.html》
Multi Paxos:《https://www.cnblogs.com/suBlog/p/17563164.html》
集群角色
Zookeeper集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色中的一种
- Leader: 一个Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及Observer间的心跳。所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。
- Follower: 一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳。Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。
- Observer: 角色与Follower类似,但是无投票权。
集群 Server 状态
服务器具有四种状态,分别是 LOOKING、FOLLOWING、LEADING、OBSERVING。
- LOOKING:寻 找 Leader 状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入 Leader 选举状态。
- FOLLOWING:跟随者状态。表明当前服务器角色是 Follower。
- LEADING:领导者状态。表明当前服务器角色是 Leader。
- OBSERVING:观察者状态。表明当前服务器角色是 Observer。(Observer角色除了不能投票(以及和投票相关的能力)和过半写成功策略外,其它和follower功能一样。observer角色减轻了投票的压力,在以前通过增、减follower的数量提高伸缩性。投票来说,follower是有状态的,都直接影响投票结果,特别是follower的数量越多,投票过程的性能就越差。)
写操作
1.写 Leader
由上图可见,通过Leader进行写操作,主要分为五步:
- 客户端向Leader发起写请求
- Leader将写请求以Proposal的形式发给所有Follower并等待ACK
- Follower收到Leader的Proposal后返回ACK
- Leader得到过半数的ACK(Leader对自己默认有一个ACK)后向所有的Follower和Observer发送Commmit
- Leader将处理结果返回给客户端
这里要注意
- Leader并不需要得到Observer的ACK,即Observer无投票权
- Leader不需要得到所有Follower的ACK,只要收到过半的ACK即可,同时Leader本身对自己有一个ACK。上图中有4个Follower,只需其中两个返回ACK即可,因为(2+1) / (4+1) > 1/2
- Observer虽然无投票权,但仍须同步Leader的数据从而在处理读请求时可以返回尽可能新的数据
2.写 Follower/Observer
从上图可见
- Follower/Observer均可接受写请求,但不能直接处理,而需要将写请求转发给Leader处理
- 除了多了一步请求转发,其它流程与直接写Leader无任何区别
读操作
Leader/Follower/Observer都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。
ZK并不保证读取的为最新数据
有时开发人员错误地假定ZooKeeper实际上没有做出另一个保证:跨客户端的强一致性
当有三个Zookeeper节点,进行写入操作 , 如果有两个已经写入成功,Zookeeper就会认为本次写入请求成功。
假如此时读取的正好为 未写入完成的1 那么就会出现读到的未旧数据
Quorum: Vr = (N + 1)/2 满足 Vr > N/2 ,而 Vw = 1 不满足 Vr + Vw > N ,所以写操作满足 Quorum ,读操作不满足 Quarum ,是这样吗?
来看看官方文档对这一点做的解释
官方文档:https://zookeeper.apache.org/doc/current/zookeeperInternals.html
Consistency Guarantees 一致性保证
The consistency guarantees of ZooKeeper lie between sequential consistency and linearizability. In this section, we explain the exact consistency guarantees that ZooKeeper provides.
ZooKeeper的一致性保证介于顺序一致性和线性化之间。在本节中,我们将解释ZooKeeper提供的确切一致性保证。
Write operations in ZooKeeper are linearizable. In other words, each
write
will appear to take effect atomically at some point between when the client issues the request and receives the corresponding response. This means that the writes performed by all the clients in ZooKeeper can be totally ordered in such a way that respects the real-time ordering of these writes. However, merely stating that write operations are linearizable is meaningless unless we also talk about read operations.ZooKeeper 中的写入操作是线性化的。换句话说,每个 write 操作都将在客户端发出请求和接收相应响应之间的某个时间点以原子方式生效。这意味着 ZooKeeper 中所有客户端执行的写入可以完全排序,以遵从这些写入的实时顺序。但是,除非我们也谈论读取操作,否则仅仅声明写入操作是线性化的是没有意义的。Read operations in ZooKeeper are not linearizable since they can return potentially stale data. This is because a
read
in ZooKeeper is not a quorum operation and a server will respond immediately to a client that is performing aread
. ZooKeeper does this because it prioritizes performance over consistency for the read use case. However, reads in ZooKeeper are sequentially consistent, becauseread
operations will appear to take effect in some sequential order that furthermore respects the order of each client's operations. A common pattern to work around this is to issue async
before issuing aread
. This too does not strictly guarantee up-to-date data becausesync
is not currently a quorum operation. To illustrate, consider a scenario where two servers simultaneously think they are the leader, something that could occur if the TCP connection timeout is smaller thansyncLimit * tickTime
. Note that this is unlikely to occur in practice, but should be kept in mind nevertheless when discussing strict theoretical guarantees. Under this scenario, it is possible that thesync
is served by the “leader” with stale data, thereby allowing the followingread
to be stale as well. The stronger guarantee of linearizability is provided if an actual quorum operation (e.g., awrite
) is performed before aread
.ZooKeeper 中的读取操作不是线性化的,因为它们可能会返回潜在的过时数据。这是因为 ZooKeeper 中的 read 不是仲裁(quorum)操作,服务器会立即响应正在执行读取操作的客户端。ZooKeeper 之所以这样做,是因为它优先考虑读取时的性能而不是一致性。但是,ZooKeeper 中的读取是顺序一致性的,因为 read 操作按某种顺序生效,从而进一步遵从每个客户端操作的顺序。解决此问题的常见模式是在每次读取前进行一次同步 sync ,但是这也不能严格保证最新数据,因为 sync 当前不是仲裁(quorum)操作。为了说明这一点,请考虑这样一种情况:两台服务器同时认为自己是领导者,如果 TCP 连接超时小于 syncLimit * tickTime ,则可能会发生这种情况。请注意,这在实践中不太可能发生,但在讨论严格的理论保证时仍应牢记这一点。在这种情况下,“领导者”可能会使用过时的数据提供服务,由此 sync 使得后续的读取也是过时的。如果在 read 之前执行一次实际的仲裁操作(例如 a write ),则可以提供更强的线性化保证。Overall, the consistency guarantees of ZooKeeper are formally captured by the notion of ordered sequential consistency or
OSC(U)
to be exact, which lies between sequential consistency and linearizability.总体而言,ZooKeeper 的一致性保证的是“有序顺序一致性” OSC(U) ,确切的说,它介于顺序一致性和线性化之间。
由于使用主从复制模式,所有的写操作都要由Leader主导完成,而读操作可通过任意节点完成,因此Zookeeper读性能远好于写性能,更适合读多写少的场景
选主流程,参照:
Paxos: 《https://www.cnblogs.com/suBlog/p/17554677.html》
Multi Paxos:《https://www.cnblogs.com/suBlog/p/17563164.html》
- 在服务器启动阶段,会进行磁盘数据的恢复,完成数据恢复后就会进行Leader选举。一旦选举产生 Leader 服务器后,就立即开始进行集群间的数据同步——在整个过程中,ZooKeeper都处于不可用状态,直到数据同步完毕(集群中绝大部分机器数据和Leader一致),ZooKeeper 才可以对外提供正常服务。
- 在运行期间,如果 Leader 服务器所在的机器挂掉或是和集群中超过半数服务器断开连接,那么就会触发新一轮的Leader选举。同样,在新的Leader服务器选举产生之前,ZooKeeper无法对外提供服务。