第 3 章 Zookeeper 内部原理
我们通过一个示例需求,来说明Zookeeper的内部原理.
3.1 示例需求
假设有一组服务器用于给给一些客户端提供某种特殊服务.
我们希望每个服务端都能找到其中一台服务器, 这样一来, 他们就可以使用这项服务.
在这个例子中, 一个挑战就是如何维护这组服务器的成员列表
这组服务器的成员列表显然不能存储在网络中的单个节点上, 否则该节点的故障将意味着整个系统的故障(意思是说我, 希望这个成员列表示高度可用的).
我们先假设已经有了一种可靠的方法来解决成员列表的的存储问题.
接下来, 如果其中一台服务器出现故障, 我们需要解决如何从服务器成员列中将他删除的问题.
某个进程需要去负责删除故障服务器, 但注意不能由故障服务器来完成, 因为故障服务器已经不再运行!
我们需要的不是一个被动的分布式数据结构, 而是一个主动的, 能够在某个外部事件发生时修改数据项状态的数据结构.
Zookeeper可以提供这种服务, 因为Zookeeper的组成员管理是一种牛逼的存在.
3.2 Zookeeper 中组成员之间的关系
方便理解 Zookeeper 的一种方式是将 Zookeeper 看作一个具有高可用性特征的"文件系统".
这个文件系统中没有文件和目录, 而是统一使用"节点(node
)"的概念, 这些节点我们称为znode
.
znode
可以看做一种容器, 只是这种容器相对比较特殊:
- 可以保存数据, 比如文件
- 可以保存其他
znode
(其他znode
如同目录)
所有的 znode
构成一个层次化的命名空间, 一种自然的方便的建立组成员列表的方式就是利用这种层次结构.
- 创建一个以组名为节点名的
znode
作为父节点 - 然后以组成员名(服务器名)为节点来创建作为子节点的
znode
.
![](https://i-blog.csdnimg.cn/blog_migrate/786c75230e2c4e9a4ba2569449c53376.png)
上图给出了一组具有层次结构的znode
在这个示例中, 我们没有在任何的 znode
中存储数据, 但是在实际应用中, 你可以想象将成员相关的数据存储在他们的 znode
中, 例如主机名.
Zookeeper 是一个具有高可用的高性能协调服务.
接下来我们从三个方面来学习这个服务: 数据模型, 操作和实现原理
3.3 Zookeeper 数据模型
3.3.1 Zookeeper数据存储
从上一节我们已经了解到: Zookeeper 维护着一个树形层次结构, 书中的节点称为 znode
.
znode
可以用来存储数据, 也可以用来存储其他的znode
.
并且每个znode
都要一个与之管理的 ACL
.
Zookeeper 被设计用来实现协调服务, 而不是用于大容量数据存储. 这类服务通常使用小数据文件, 因此一个 znode
能存储的数据被限制在 1MB
以内.
Zookeeper的数据访问具有原子性:
- 当客户端从一个
znode
读取数据时, 要么读到的所有数据, 要么度操作失败, 不会只读到部分数据. - 一个写操作将替换
znode
存储的所有数据. Zookeeper会保证写操作不成功就失败, 不会出现部分写入的情况发生. - Zookeeper 不支持添加操作.
这些特征都是与HDFS
所不同. 因为HDFS
是被设计用于存储大容量数据, 支持流式数据访问和添加操作.
关于
ACL
:
Access Control List
访问控制列表.
用于控制资源的访问权限。Zookeeper 利用ACL
策略控制节点的访问权限,如节点数据读写、节点创建、节点删除、读取子节点列表、设置节点权限等。
引用znode
znode
通过路径被引用.
像 Unix
中的文件系统路径一样, 在 Zookeeper 中路径被表示成用斜杠分割的 Unicode
字符串.
与 Unix
中的文件系统路径不同的是, Zookeeper 中的路径必须是绝对路径, 也就是说每条路径必须从一个斜杠字符开始
此外, 所有的路径表示必须是规范的, 即每条路径只有唯一的一种表示方式, 不支持路径解析. (比如: .和..
在 Zookeeper 中不具有特殊含义, 用他们表示路径名是不合法的.)
znode
的分类
根据znode
的存活时间的长短, znode
有两种类型:
- 短暂
znode
(ephemeral znode
) - 持久
znode
(persistent znode
)
在创建短暂 znode
的客户端会话结束时, Zookeeper 会将该短暂 znode
删除.
相比之下, 持久 znode
不依赖于客户端会话. 当任何一个客户端(可以是不是创建这个znode
)明确要求删除该持久 znode
时, 该持久 znode
才会被删除.
持久 znode
可以有子节点, 其子节点可以是短暂 znode
也可以是持久znode
.
但是短暂 znode
不可以有任何子节点. 即使是短暂子节点也不行.
顺序号(Sequence number)
如果在创建 znode
时设置了顺序标识, 那么该 znode
名称之后便会附近一个值, 这个值是由一个单调递增的计数器(由父节点维护)所添加的.
像这种名称中包含 Zookeeper 指定顺序号的 znode
我们称为 顺序znode
例如:
如果一个客户端请求创建一个名为 /a/b-
的顺序 znode
, 则所创建 znode
的名字可能是 /a/b-3
.
如果稍后, 另外一个名为 /a/b-
的顺序 znode
被创建, 计数器会给出一个更大值来保证 znode
名称的唯一性, 例如: /a/b-5
.
在一个分布式系统中, 顺序号可以用来为所有的事件进行全局排序, 这样客户端就可以通过顺序号来推断事件的顺序.
观察机制
znode
以某种方式发生变化时, 观察机制可以让客户端得到通知.
可以针对 Zookeeper 服务的操作来设置观察, 该服务的其他操作可以出发观察.
例如:
客户端针对某一个znode
条用 exists
操作, 同时设定一个观察.
如果目前这个 znode
不存在, 则客户端所有调用的 exsists
操作将返回 false
.
如果过一段时间之后, 另外一个客户端创建了这个 znode
, 则这个观察会被触发, 通知前一个客户端这个znode
被创建.
另外一点需注意的是: 观察只能被触发一次. 为了能够多次受到通知, 客户端需要重新注册所需的观察.
3.4 Zookeeper 服务基本操作
Zookeeper服务中有 9 种基本操作:
基本操作的演示我们稍后在后面的章节展示.
3.5 Zookeeper实现原理
Zookeeper 服务有两种不同的运行模式.
- 一种是独立模式(standalone mode)
- 一种是复制模式(replicated mode)
3.5.1 独立模式
这种模式比较简单, 只有一个 Zookeeper 服务器. 比较适合于测试环境(也可以在单元测试中采用), 但是不能保证高可用和可恢复性.
3.5.2 复制模式
在生产环境中的 Zookeeper 通常以复制模式运行在一个计算机集群上, 这个计算机集群通常被称为一个集合体(ensemble).
Zookeeper 通过复制来实现高可用性, 只要集合体中半数以上(不包括半数)的机器处于可用状态, 它就能够提供服务.##
例如:
在一个有 5 台机器的集合体中, 任意 2 机器出现故障, 都可以保证服务继续, 因为剩下的 3 台机器超过了半数.
在 6 个机器的集合体中, 最多只能容忍 2 台机器出现故障, 因为如果 3 台机器出现故障, 剩下的 3 台就没有超过集合体的半数了.
处于这个原因, 一个集合体中通常包含奇数台机器.
从概念上来说, Zookeeper 是非常简单的: 他所要做的就是确保对 znode
树的每一个修改都会被复制到集合体中的超过半数的机器上.
如果有不到半数的机器出现故障, 则最少会有一台机器保存最新的状态, 其余的副本最终也会更新到这个状态.
然而, 这个简单的想法的实现却不简单. Zookeeper 使用了 Zab 协议, 该协议包括以下两个无限重复的阶段:
阶段1: 领导者选择(Leader election)
集合体中每台机器都有一个身份: 要么是领导者(leader), 要么是跟随者(follower).
一个集合体中只有一个领导者, 其余都是跟随者.
集合体中所有机器通过一个选举过程来选出一台机器来作为领导者, 其他的机器就称为了跟随者.
阶段2: 原子广播(Atomic broadcast)
所有的写请求都会被转发给领导者, 再有领导者将更新广播给跟随者.
当半数以上的跟随者已经将修改持久化之后, 领导者才会真正的提交这个更新, 然后客户端才会收到一个更新成功的响应.
这个用来达成共识的协议被设计成具有原子性, 因此每个修改要么成功要么失败.
如果领导者出现故障, 其余的机器会自动再选出另外一个领导者, 并和新的领导者一起继续提供服务. 随后, 如果之前的领导者恢复正常, 则会成为一个跟随者.
领导者的选举过程是非常快的, 只需要大概200毫秒, 因此在领导者选举的过程中不会出现系统性能的明显降低.
任何一台机器都可以为读请求提供服务, 并且由于读请求只涉及内存检索, 因此非常快.
3.5.3 领导者选举过程详解
Leader 选举是保证分布式数据一致性的关键所在。当 Zookeeper 集群中的一台服务器出现以下两种情况之一时,需要进入 Leader 选举:
- 服务器初始化启动。
- 服务器运行期间无法和 Leader 保持连接。
选举机制中的一些概念
- 服务器ID(myid)
比如有 5 台服务器, 他们的 ID 编号分别是1, 2, 3, 4, 5.
编号越大, 在选举算法中的权重也越大.
- 数据ID(或者也叫事务ID, zxid)
服务器中存放的最大数据ID.
值越大表示数据越新, 在选举算法中数据越新权重越大.
- 逻辑时钟
或者叫投票的次数.
同一轮投票过程中的逻辑时钟值是相同的。
每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断。
- 选举状态
- LOOKING : 精选状态(正在找领导)
- FOLLOWING : 随从状态,同步leader状态,参与投票。
- OBSERVING : 观察状态, 同步leader状态,不参与投票。
- LEADING : 领导者状态(我是领导了)
- 选举消息内容
在投票完成后,需要将投票信息发送给集群中的所有服务器,它包含如下内容。 - 服务器ID
- 数据ID
- 逻辑时钟
- 选举状态
服务器启动时期的 Leader 选举
目前有 5 台服务器,每台服务器均没有数据(可以认为他们的数据id都是0, 所以选领导看每个服务器的id),它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下:
-
服务器1启动, 给自己投票, 然后广播投票信息(
<1, 0>
格式:<myid, zxid>
). 由于其他服务器都没有启动, 所以收不到任何反馈信息.服务器1的状态一直属于LOOKING
-
服务器2启动, 给自己投票, 然后广播投票信息(
<2, 0>
)(与其他服务器交换选举信息).由于服务器的2的id大于服务器1的id, 所以服务器2胜出. 由于现在启动的服务器的没有超过半数, 所以服务器2并不能当成为leader
, 仍然是LOOKING
状态. 但是服务器1会更新的自己的投票信息为(<2, 0>
). -
服务器3启动, 给自己投票(
<3, 0>
), 与其他服务器交换投票信息. 最终的结果是服务器3胜出, 且由于启动的服务器的数量已经超过了半数, 所以服务器3成为leader
, 服务器1和服务器2自动成为follower
-
服务器4启动, 根据前面的信息, 虽然服务器4的
id
比较大, 但是由于已经产生了领导, 所以服务器4只能是follower
-
服务器5启动, 命运和服务器4一样.
服务器运行时期的 Leader 选择( Leader 挂了)
在 Zookeeper 运行期间,Leader
与 非Leader
服务器各司其职,即便当有非Leader
服务器宕机或新加入,此时也不会影响Leader
.
但是一旦Leader
服务器挂了,那么整个集群将暂停对外服务(200ms
左右),进入新一轮Leader
选举,其过程和启动时期的Leader
选举过程基本一致。
假设正在运行的有Server1
、Server2
、Server3
、Server4
、Server5
三台服务器,当前Leader
是Server3
,若某一时刻Leader
挂了,此时便开始Leader
选举。选举过程如下:
为了分析方便, 假设仍然没有数据.(如果有数据的话, 数据id优先.)
-
变更状态. 所有的
follower
都会讲自己的服务器状态变成LOOKING
. -
开始选举, 每个服务器发送一个投票. 第一轮投票都会选举自己.
<自己的id, 0>
-
每个服务器都会接收到其他服务器的投票. 与气动过程相同
-
处理投票
-
统计投票
-
选出领导, 更新状态.
3.6 分布式安装部署(复制模式运行Zookeeper)
在hadoop201、hadoop202和hadoop203
三个节点上部署 Zookeeper。
现在hadoop201
配置完成之后, 再把配置好的 Zookeeper 复制到其他节点上.
步骤1: 解压安装 Zookeeper
- 解压 Zookeeper 到
/opt/module
cd /opt/software
tar -zxvf zookeeper-3.4.13.tar.gz -C /opt/module/
- 在 Zookeeper 根目录下创建目录
zkData
cd /opt/module/zookeeper-3.4.13
mkdir zkData
- 复制
./conf/zoo_sample.cfg
并重命名为zoo.cfg
cd conf
cp zoo_sample.cfg zoo.cfg
步骤2: 配置 zoo.cfg
文件
dataDir=/opt/module/zookeeper-3.4.13/zkData
# 增加如下配置:
server.1=hadoop201:2888:3888
server.2=hadoop202:2888:3888 server.3=hadoop203:2888:3888
配置参数解读
格式: server.A=B:C:D
A: 一个数字, 表示第几号服务器
B: 服务器的ip地址
C: follower
与leader
的通讯端口(通常设置为2888
)
D: 选举端口(通常设置为3888
)
步骤3: 创建myid
文件
在dataDir
所定义的目录下创建文件, 文件名: myid
填入服务器的id
, 就是server.A=B:C:D
中的A
cd /opt/module/zookeeper-3.4.13/zkData
vim myid
# 输入id值
步骤4: 把配置好的 Zookeeper 到其他机器上
cd /opt/module
my_rsync zookeeper-3.4.13/
步骤5: 修改其他设备上myid
文件的内容
把myid
文件的内容分别更改为2
和3
步骤6(可选): 把 Zookeeper 添加到 PATH
中
为了执行 Zookeeper 的命令方便, 建议把 Zookeeper 添加到 PATH
中
vim /etc/profile
# 在打开的文件中增加如下两行
export ZOOKEEPER_HOME=/opt/module/zookeeper-3.4.13
export PATH=$PATH:$ZOOKEEPER_HOME/bin
步骤7: 分别在 3 台设备上启动 Zookeeper 服务
zkServer.sh start
步骤8: 分别检查 3 台 Zookeeper 服务器的状态
在任何一台设备执行如下命令即可.
echo stat | nc hadoop201 2181
echo stat | nc hadoop202 2181
echo stat | nc hadoop203 2181
3.7 客户端命令行操作
我们可以使用客户端去操作 znode
. (其实就是远程操作 Zookeeper Server
)
注意:
znode
都是存储在 Zookeeper 服务器上的.
1. 启动客户端并连接到 Zookeeper 服务器
zkCli.sh # 默认连接到当前的Zookeeper服务器
# zkCli.sh -server hadoop201:2181 # 连接到指定的Zookeeper服务器
2. 显示所有操作命令
help
3. 查看根znode
中所包含的内容
ls /
4. 查看根znode
中节点数据更新次数等数据
ls2 /
stat
结构说明:
cZxid:这是导致创建znode更改的事务ID。
mZxid:这是最后修改znode更改的事务ID。
pZxid:这是用于添加或删除子节点的znode更改的事务ID。
ctime:表示从1970-01-01T00:00:00Z开始以毫秒为单位的znode创建时间。
mtime:表示从1970-01-01T00:00:00Z开始以毫秒为单位的znode最近修改时间。
dataVersion:表示对该znode的数据所做的更改次数。
cversion:这表示对此znode的子节点进行的更改次数。
aclVersion:表示对此znode的ACL进行更改的次数。
ephemeralOwner:如果znode是ephemeral类型节点,则这是znode所有者的 session ID。 如果znode不是ephemeral节点,则该字段设置为零。
dataLength:这是znode数据字段的长度。
numChildren:这表示znode的子节点的数量。
5. 创建普通节点(持久节点)
creat /app1 "hello app1"
说明:
app1
是节点名hello app1
是这个节点中存储的数据(上限大小是1M
)
create /app1/server201 "192.168.1.201"
6. 获得节点的值
get /app1
7. 创建短暂节点
create -e /app-temp "temp data ...."
注意:
- 只要创建临时节点的会话(session)没有关闭, 不仅仅当前客户端可以看访问到临时节点, 使用其他客户端也是可以看到这个临时节点.
- 临时节点不能有子节点
- 关闭会话之后, 临时节点自动被删除
8. 创建带序号的节点
- 先创建一个比普通节点
app2
create /app2 "my name is app2"
- 在
app2
内创建带序号的节点
create -s /app2/aa "123455679"
说明:
- 没创建一个带序号的节点, 序号都会自增1.
- 序号的含义表示这个带序号的节点是他父节点的第几个节点.
创建/app3
, /app3/a
, -e /app3/b
然后创建-s /app3/cc
, 则序号从2
开始.
9. 修改节点中存储的数据
set /app1 "new data"
10. 监听节点中数据的变化
- 在
201
主机上的客户端监听:
get /app1 watch
-
在
203
主机上的客户端修改数据
-
在
201
主机的客户端收到的数据变化的通知
11. 监听节点结构的变化
- 在
201
主机上的客户端监听:
ls /app1 watch
-
在
203
主机上的客户端给/app1
添加新的节点
-
在
201
主机的客户端收到的数据变化的通知
12. 删除空节点
delete /app1/server201
注意:
-
delte
只能删除空节点(没有子节点的节点) -
不能删除非空节点
13. 递归的删除节点
递归的方式删除节点, 空节点和非空节点都可以删除.
rmr /app1
14. 查看某个节点的状态
stat /app2
3.8 配置参数解读
解读 zoo.cfg
文件中的参数含义
-
tickTime
定义了 Zookeeper 中基本的时间周期, 并被用来 Zookeeper 各个服务器用来定义相互交互的时间表.
其他的关于时间的设置都是根据这个参数来定义的, 或者至少与这个参数有关.
例如: 会话超时时间(session timeout
)的值必须大于等于2 * tickTime
和 小于等于20 * tickTime
. 如果超出了这个时间, 就自动的修改到这个范围之内.
通常会把tickTime
的值设为2000
毫秒. -
initLimit
表示领导者和跟随者之间进行连接同步的时间范围(单位是一个tickTime
).
如果在这个时间内, 半数以上的跟随者没有同步, 则会重新选择领导.
如果经常发生这种情况, 证明这个值设置的太小了. -
syncLimit
允许跟随者与领导者进行同步的事件.
如果在这个时间内, 有一个跟随者没有与领导者同步, 这个认为这个跟随者死掉了, 所有与这个跟随者连接的客户端会转移给其他跟随者. 这个跟随者也会自动重启. -
dataDir
数据文件目录+数据持久化路径
保存内存数据库快照信息的位置,如果没有其他说明,更新的事务日志也保存到数据库。
5. clientPort
监听客户端连接的端口