对于一些富含经验的技术大佬来说 ZooKeeper
都是耳熟能详;但对于广大的应届生或者分布式微服务的初学者,可能是刚听说不久,但是知道笔试或者面试经常也会有它的身影存在!
如果您已经了解了基础,只想知道如何简单搭建一个集群,可以直接阅读该文章: zookeeper 集群搭建
那么
ZooKeeper
是什么呢?
ZooKeeper
最初由雅虎公司开发,并于 2010 年成为 Apache 软件基金会的顶级项目。它的设计灵感来自于 Google 的 Chubby
分布式锁服务。雅虎在构建大规模分布式系统时遇到了一些挑战,包括配置管理、命名服务和协调问题。为了解决这些问题,他们开发了 ZooKeeper
。随着时间的推移,ZooKeeper
在开源社区中获得了广泛的认可和采用,并成为了一个重要的 分布式协调服务。
ZooKeeper
使用树形结构来组织数据,并通过保持副本在多个服务器上来实现高可用性。它被广泛应用于分布式系统中,如 Apache Hadoop
、Apache Kafka
等。
特点 | 说明 |
---|---|
一致性 | 客户端无论连接到集群中的哪个节点,读到的数据都是一样的 |
实时性 | ZooKeeper 保证客户端在一定的时间间隔内获得结果,包括成功和失败,但是由于网络延迟原因,Zookeeper 不能保证两台客户端同时得到刚更新的消息。如果都需要最新的消息需要调用sync()接口。 |
原子性 | leader在同步数据的时候,同步过程保证事务性,要么都成功,要么都失败。 |
顺序性 | 一台服务器上如果消息a在消息b前发布,那么所有的server上的消息a都是在消息b前发布的。 |
Session
是指 ZooKeeper
服务器与客服端建立的会话。
在 ZooKeeper
中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接;客户端在启动的时候就会和服务器端建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也就开始了。
客户端会定期通过该长连接向服务器端发送心跳包,以确保和服务器端保持有效的会话;同时客户端也是通过该会话连接和服务器端通讯发起请求并接收响应,也是通过此来接收服务器的 Watch
事件通知。
sessionTimeout
配置是用来设置一个客户端会话的超时时间;当服务器压力过大、网络故障或者是客户端主动断开连接等各种原因导致客户端会话连接断开时,只要在 sessionTimeout
规定的时间范围内客户端能重新连接到集群的任何一台服务器,那么之前创建的会话还算是有效会话!
在为客户端创建会话之前,服务端都会给每个客户端分配一个 sessionID
,该标志是 ZooKeeper
会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID
的,因此,无论哪台服务器为客户端分配的 sessionID
, 都务必保证全局唯一。
常规来说,节点
指代的是集群中的每一台机器。但是在 ZooKeeper
中,节点
分为两类:
Znode
,指数据模型中的数据单元ZooKeeper
的数据结构,跟 Unix
文件系统非常类似,可以看做是一颗树,每个节点叫做 Znode
。每一个节点可以通过路径来标识 (如图)
Znode
其实也分为两种类型
Ephemeral
短暂/临时:当客户端和服务端断开连接后,所创建的 Znode
会自动删除。Persistent
持久:当客户端和服务端断开连接后,所创建的 Znode
不会删除。下面简单讲下什么时候使用临时节点、什么时候使用持久节点
临时 Znode
来表示他们的在线状态。当玩家断开连接时,与其相关联的 临时 Znode
将被自动删除。这样,其他玩家可以通过监视 ZooKeeper
上的 临时 Znode
来知道哪些玩家当前在线。持久 Znode
来存储各种配置信息,例如数据库连接字符串、API 密钥等。这些 持久Znode
将一直存在,即使某个节点发生故障或重启,配置信息也不会丢失。其他节点可以通过监视 ZooKeeper
上的 持久Znode
来获取最新的配置信息。ZooKeeper
的每个 Znode
上都会存储数据,对应于每个 Znode
,ZooKeeper
都会为其维护一个叫作 Stat
的数据结构,Stat
中记录了这个 Znode
的三个数据版本,分别是:
version
: 当前 Znode
的版本cversion
: 当前 Znode
子节点的版本aversion
: 当前 Znode
的 ACL
版本️ 扩展 :
ZooKeeper Stat Structure
字段 | 说明 |
---|---|
czxid |
The zxid of the change that caused this znode to be created. |
mzxid |
The zxid of the change that last modified this znode. |
ctime |
The time in milliseconds from epoch when this znode was created. |
mtime |
The time in milliseconds from epoch when this znode was last modified. |
version |
The number of changes to the data of this znode. |
cversion |
The number of changes to the children of this znode. |
aversion |
The number of changes to the ACL of this znode. |
ephemeralOwner |
The session id of the owner of this znode if the znode is an ephemeral node. If it is not an ephemeral node, it will be zero. |
dataLength |
The length of the data field of this znode. |
numChildren |
The number of children of this znode. |
Watcher
(事件监听器),是 ZooKeeper
实现分布式协调服务的重要特性。
ZooKeeper’s definition of a watch: a watch event is one-time trigger, sent to the client that set the watch, which occurs when the data for which the watch was set changes.
ZooKeeper
允许用户在指定节点上注册一些 Watcher
,并且在一些特定事件触发的时候,ZooKeeper
服务端会将事件通知到感兴趣的客户端上去。
ZooKeeper
中的所有读取操作 - getData()
、getChildren()
和 Exists()
- 都可以选择设置监视器。
Znode
节点的数据变化1️⃣ One-time trigger (一次性触发)
数据变更的时候,会往客户端推送一个监听事件。
举个例子,如果客户端设置了 getData("/Java/mq", true)
,而之后 /Java/mq
内容变更了或者该节点删除了,客户端就会收到服务器端推送的 /Java/mq
对应的监听事件。
但是如果 /Java/mq
之后又产生了变更行为,除非客户端重新设置了读取并监听该节点数据事件,否则不会再次推送对应的监听事件给到客户端!
2️⃣ Sent to the client (正在发送给客户端)
当你在 ZooKeeper
中设置一个 watch
时,它表示有一个事件即将发送到客户端。然而,在成功返回更改操作的代码给发起更改的客户端之前,该事件可能尚未到达客户端。
Watches
是++异步发送给观察者++的。ZooKeeper
提供了一个顺序保证:客户端在首次看到 watch
事件之前,不会看到其设置了 watch
的 znode
更改内容。
网络延迟或其他因素可能导致不同的客户端在不同的时间看到 watch
和更新的返回代码。++关键点在于,不同客户端所看到的一切都具有一致的顺序++。
3️⃣ The data for which the watch was set (监视特定的数据变化)
在 ZooKeeper
中,节点的变化可以包括 ++数据的变化 (data watch
)++ 和 ++子节点的变化 (child watch
)++。
可以使用 getData()
和 exists()
方法设置数据观察,而使用 getChildren()
方法设置子节点观察。
也可以根据返回的数据类型来设置观察,getData()
和 exists()
返回有关节点数据的信息,而 getChildren()
返回子节点列表。
因此假设有一个节点是 /Java
,它的子节点是 /Java/mq
。根据之前提到的规则:
setData()
成功设置了 /Java
节点的数据,那么将触发对该节点的数据观察。create()
成功创建了 /Java/mq
节点,那么将触发对被创建节点 /Java/mq
的数据观察以及对父节点 /Java
的子节点观察。delete()
成功删除了 /Java/mq
节点,那么将触发对被删除节点 /Java/mq
的数据观察和子节点观察(因为没有更多子节点可用),以及对父节点 /Java
的子节点观察。ZooKeeper
采用 ACL
( AccessControlLists )策略来进行权限控制,类似于 UNIX 文件系统的权限控制,与标准 UNIX 权限不同,ZooKeeper
节点不受用户(文件所有者)、组和全局(其他)的三个标准范围的限制。ZooKeeper
没有 Znode
的所有者概念。
权限 | 说明 |
---|---|
CREATE |
创建子节点的权限 |
READ |
获取节点数据和子节点列表的权限 |
WRITE |
更新节点数据的权限 |
DELETE |
删除子节点的权限 |
ADMIN |
设置节点 ACL 的权限 |
ZooKeeper
不支持 LOOKUP
权限(目录上的执行权限位,允许您进行 LOOKUP
,即使您无法列出目录)。每个人都隐式具有 LOOKUP
权限。这允许您对节点进行状态查询,但不能进行其他操作。(问题是,如果您想在不存在的节点上调用 zoo_exists()
,则没有要检查的权限。)
scheme | 说明 |
---|---|
world |
即 anyone,即所有用户都具有该权限。这意味着无论是谁,都可以对节点执行相应的操作,例如读取数据、设置数据、创建子节点或删除子节点。 |
auth |
不使用任何 id,表示任何经过身份验证的用户 |
digest |
使用 用户名:密码 字符串生成 MD5 哈希,然后将其用作 ACL ID 标识。身份验证是通过以明文形式发送 用户名:密码 来完成的。在 ACL 中使用时,表达式将是 用户名:base64编码的SHA1密码 摘要。 |
host |
使用客户端主机名作为 ACL ID 标识。ACL 表达式是主机名后缀。例如,ACL 表达式 host:corp.com 匹配 ids host:host1.corp.com 和 host:host2.corp.com ,但不匹配 host:host1.store.com 。 |
ip |
使用客户端主机 IP 作为 ACL ID 标识。ACL 表达式的格式为 addr/bits ,其中 addr 的最高有效位与客户端主机 IP 的最高有效位进行匹配。假设您希望允许具有 IP 地址为 192.168.0.0/24 的客户端访问某个节点。在 ACL 中,您可以设置 ip:192.168.0.0/24 来表示这个权限。 |
ZooKeeper
支持可插拔的身份验证方案。使用 scheme:id
的形式指定 ids
,其中 scheme
是 id
对应的身份验证方案。例如,host:host1.corp.com
是一个名为 host1.corp.com
的主机的 id
。
当客户端连接到 ZooKeeper
并进行身份验证时,ZooKeeper
将与客户端关联的所有 id
与客户端的连接关联起来。当客户端尝试访问节点时,这些 id
将与 Znode
的 ACL
进行检查。ACL
由( scheme:expression, perms
)对组成。表达式的格式特定于方案。例如,( ip:19.22.0.0/16, READ
)这对给具有以 19.22
开头的 IP 地址的任何客户端赋予了读取权限。
ZooKeeper
集群是由多个 ZooKeeper
服务器组成的分布式系统。每个服务器都是一个独立的实例,它们协同工作以提供高可用性和容错性。
相对于单个 ZooKeeper
实例的 Standalone Mode
(独立运行模式), 集群使用的是 Quorum Mode
(仲裁模式)。
角色 | 职责 |
---|---|
Leader |
领导者,负责进行投票的发起和决议,更新系统状态,Leader 是由选举产生 |
Follower |
跟随者,用于接受客户端请求并向客户端返回结果,在选主过程中参与投票 |
Observer |
观察者,可以接受客户端连接,接受读写请求,写请求转发给 Leader ,但 Observer 不参加投票过程,只同步 Leader 的状态,Observer 的目的是为了扩展系统,提高读取速度 |
ZooKeeper
集群服务器有以下 4 种状态:
状态 | 说明 |
---|---|
LOOKING |
寻找 Leader 状态,当服务器处于该状态时,表示当前集群没有 Leader ,因此会进入 Leader 选举状态。 |
FOLLOWING |
跟随者状态,表示当前服务器角色是 Follower 。 |
LEADING |
领导者状态,表示当前服务器角色是 Leader 。 |
OBSERVING |
观察者状态,表示当前服务器角色是 Observer 。 |
方式 | 说明 |
---|---|
LeaderElection |
领导者选举,这是最基本的选举方式,它使用了 ZooKeeper 中的++原子广播协议++。在 LeaderElection 中,每个节点都可以成为候选人,并通过与其他节点进行通信来达成共识,选择一个节点作为领导者。 |
AuthFastLeaderElection |
认证快速领导者选举, 这是一种改进的选举方式,用于提高 ZooKeeper 集群的安全性。AuthFastLeaderElection 在 LeaderElection 的基础上添加了身份验证和加密功能,以确保只有经过身份验证的节点才能参与选举过程。 |
FastLeaderElection |
快速领导者选举, 这是 ZooKeeper 中最新的默认选举方式。FastLeaderElection 使用了一种称为 Fast Paxos 的一致性算法,它具有更高的性能和更低的延迟。该算法通过减少消息传递次数和选举轮次来提高选举效率。 |
Leader
失联的时候会发生选举ZooKeeper
服务器处于 LOOKING
竞选状态 (注意:观察者状态不能参与竞选投票)ZooKeeper
的集群规模至少要有 3 台机器或以上❓ 为啥集群的规模至少要 3 台机器呢?
集群规则为:2N + 1
台(避免脑裂),N > 0
,即最少需要 3
台。
因为 ZooKeeper
集群的机制是++只有超过半数的节点正常,集群才能正常提供服务++。只有在 ZooKeeper
节点挂得太多,只剩一半或不到一半节点能工作时,集群才会失效。
简单用例子代入理解:
Leader
可以得到 2 票 > 1.5)Leader
可以得到 1 票 <= 1)以三个节点的集群举例子:
server | myid | zxid | 运行状态 |
---|---|---|---|
zk1 | 1 | 0 | ✔️ |
zk2 | 2 | 0 | ✔️ |
zk3 | 3 | 0 | ✔️ |
zxid
为 ZooKeeper
事务 ID 的缩写,每个事务都会被分配一个唯一的 zxid
值,它由两部分组成:高 32位 表示 Leader
的 Epoch
(领导者轮次),低 32位 表示事务计数器。zxid
的作用是确保 ZooKeeper
中的所有事务操作都具有全局唯一的标识,以实现数据的一致性和顺序性。myid
是一个用于标识 ZooKeeper
集群中每个节点的唯一整数。每个节点都需要在其配置文件中指定一个 myid
值,该值范围从 1~255
。myid
的作用是帮助 ZooKeeper
识别每个节点,并为其分配相应的角色(如领导者、跟随者或观察者)。通过 myid
,ZooKeeper
可以维护集群中各个节点的状态和角色信息。依次启动 3 台服务器 ,初始情况下事务 ID (zxid
) 都为 0。
1️⃣ 初始投票
服务启动后,每个 Server 都会默认给自己投上一票,每次投票都会包含 myid
和 zxid
,为方便表示,后续统一使用 Server(myid, zxid)
的格式来体现!
此时投票的数据依次为: zk1(1, 0)
,zk2(2, 0)
,zk3(3, 0)
。
2️⃣ 同步投票结果
集群中的服务器在投票后,会将各自的投票结果同步给集群中其他服务器。
3️⃣ 检查投票有效性
各服务器在收到投票后会检查投票的有效性,如:是否本轮投票,是否来自 LOOKING
状态的服务器的投票等。
4️⃣ 处理投票
服务器之间会进行投票比对,规则如下:
zxid
,较大的服务器优先作为 Leader
zxid
相同,则 myid
较大的服务器作为 Leader
比如依次先启动 zk1 -> zk2
,那么此时的投票数据为 zk1(1, 0)
、zk2(2, 0)
。
server | myid | zxid | 运行状态 | 票数 | 服务状态 |
---|---|---|---|---|---|
zk1 | 1 | 0 | ✔️ | 1 | Looking |
zk2 | 2 | 0 | ✔️ | 1 | Looking |
zk3 | 3 | 0 | ✖️ ️ | 0 |
由于 zxid
都相同,所以 zk2
的投票胜出,此时 zk1
需要更新自己的投票为 zk1(2, 0)
。
server | myid | zxid | 运行状态 | 票数 | 服务状态 |
---|---|---|---|---|---|
zk1 | 1 | 0 | ✔️ | 0 | FOLLOWING |
zk2 | 2 | 0 | ✔️ | 2 | LEADING |
zk3 | 3 | 0 | ✖️ ️ | 0 |
5️⃣ 统计投票结果
每轮投票比对之后都会统计投票结果,确认是否有超过半数的机器都得到相同的投票结果,如果是,则选出 Leader
,否则继续投票。
第四步提到的选举中,由于 zk1
和 zk2
已经都得到相同的投票结果 (2,0)
, 其中 2 代表了 zk2
,可见投票结果 2 > (3/2)
超过了半数机器了,所以此时 zk2
其实已经成为本轮选举的 Leader
了。
所以最后启动的 zk3
识别到集群中已经有 Leader
了,就不再参与选举了,选举也就结束了,后面进来的只能是小弟!
server | myid | zxid | 运行状态 | 票数 | 服务状态 |
---|---|---|---|---|---|
zk1 | 1 | 0 | ✔️ | 0 | FOLLOWING |
zk2 | 2 | 0 | ✔️ | 2 | LEADING |
zk3 | 3 | 0 | ✔️ ️ | 0 | FOLLOWING |
6️⃣ 更改服务器状态
一旦选出 Leader
,每个服务器就会各自更新自己的状态:
zk1 >>> FOLLOWING
zk2 >>> LEADING
Zk3 >>> FOLLOWING
️️ 从上面的例子我们可简以推导出这样的规律:
- 集群有 3 台机器,第 2 大的 myid 所在服务器就是 Leader;
- 集群有 4 台机器,第 3 大的 myid 所在服务器就是 Leader;
- 集群有 5 台机器,第 3 大的 myid 所在服务器就是 Leader;
- 集群有 6 台机器,第 4 大的 myid 所在服务器就是 Leader;
服务器的运行不可能一帆风顺,随时可能因为网络故障或者机器故障等原因离线,假如刚好挂的是 Leader
节点的情况下,集群就会产生新一轮的选举,此处还是使用上面的 3 台机器例子来做说明 (假设运行了一段时间后 zk2
挂掉了)。
server | myid | zxid | 运行状态 | 票数 | 服务状态 |
---|---|---|---|---|---|
zk1 | 1 | 66 | ✔️ | 0 | FOLLOWING |
zk2 | 2 | 42 | ✖️ | 2 | LEADING |
zk3 | 3 | 28 | ✔️ ️ | 0 | FOLLOWING |
1️⃣ 状态变更
如上图所示,此时 zk2
由于异常已经离线了,目前集群已经不存在 Leader
节点,其他的节点会从 FOLLOWING
变更成 LOOKING
,从而开启新一轮的 Leader
选举。
server | myid | zxid | 运行状态 | 票数 | 服务状态 |
---|---|---|---|---|---|
zk1 | 1 | 66 | ✔️ | 0 | LOOKING |
zk2 | 2 | 42 | ✖️ | 2 | LEADING |
zk3 | 3 | 28 | ✔️ ️ | 0 | LOOKING |
2️⃣ 开始投票
投票逻辑和启动初始时一致。
此时 zk1
和 zk3
会先投给自己一票,zk2
离线已不能参与投票,新一轮的投票为 zk1(1, 66)
、zk3(3, 28)
。
3️⃣ 同步投票结果
同步投票逻辑和启动初始时一致。
4️⃣ 检查投票有效性
检查投票逻辑和启动初始时一致。
5️⃣ 处理投票
处理投票逻辑和启动初始时一致。
此时,zk1
和 zk3
票数一致,先对比 zxid
后 zk1
胜出,因此 zk3
需要更新自己的投票结果为 zk3(1, 28)
。
6️⃣ 统计投票结果
统计投票逻辑和启动初始时一致。
本轮选举中,zk1
和 zk3
都得到了相同的投票结果 zk1
,并且超过了半数的机器 2 > (3 / 2)
,所以此时 zk1
就成为了本轮选举的 Leader
。
server | myid | zxid | 运行状态 | 票数 | 服务状态 |
---|---|---|---|---|---|
zk1 | 1 | 66 | ✔️ | 0 | LEADING |
zk2 | 2 | 42 | ✖️ | 2 | |
zk3 | 3 | 28 | ✔️ ️ | 0 | FOLLOWING |
7️⃣ 更改服务器状态
zk1 >>> LEADING
Zk3 >>> FOLLOWING
zookeeper 集群搭建