# Zookeeper

[Zookeeper](http://zookeeper.apache.org) 是开源的分布式协调服务，它提供了一些简单的原语。应用程序可以基于这些原语实现一些复杂的高级服务。

不同于Redis的KV结构，zk的数据结构是一个类似目录树的结构：

![ZooKeeper's Hierarchical Namespace](/files/QvXqYgzdIlGEL4gKpCHP)

1. 层级结构中的每个节点称为 `znode`，每个节点都可以存储数据，但是最大数据存储大小为`1MB`。
2. 他们统一存储在内存中（高性能、低延迟、高可用）。
3. 存储的内容同Redis相同，也是二进制安全的，也就是说zk不关心他们的数据编码格式，给什么byte流就存储什么byte流。
4. 为了保证zk快速、高可用的特点，不可以将zk当作数据库使用，这也是为什么每个znode只能存储1MB大小的原因。

每个Client在连接zk集群时，都会产生一个Session，代表当前的会话，依托于Session，可以将znode分为一下几种类型：

1. 持久节点
2. 临时节点，会话级别，会话不存在数据就会消失。Session消失通过事件通知删除节点，典型的应用场景为分布式锁（可以有效的避免死锁的问题，因为应用程序一旦结束会话也会结束，锁也会被释放）
3. 序列节点

Zookeeper 通常也作为一个集群使用：

![ZooKeeper Service](/files/CEudFUae6A1ICsa3ROIh)

1. zk集群是一种主从复制集群（Redis Cluster是一种数据分片集群），主从复制集群中的每个Server的数据都是完全相同的，且主节点用作写操作，从节点用来扩展读操作
2. 在集群中分为两种角色：`Leader`和`Follower`，`Leader`写、`Follower`读
3. 当Client对`Follower`发生写操作，会将写操作重定向给`Leader`

Zookeeper集群还是一个可以快速自我修复的集群环境：

1. 主从复制集群的主要问题是`Leader`节点的单点故障问题
2. 当zk的Leader发生故障后，zk集群会进入无主模型，zk会拒绝所有的请求，此时集群处于不可用状态
3. 此时zk内部多个`Follower`会发生选举，重新选举出一个Leader（这个过程只有不到200ms，并可以立即处理请求，并恢复原来的吞吐量）
4. 选举成功后，zk变为可用状态，并开始处理客户端请求

Zookeeper集群的读写性能也是极高的（横坐标为客户端读写的占比，纵坐标为每秒请求数）：

![ZooKeeper Throughput as the Read-Write Ratio Varies](/files/yj9lBGfbDWFkErRx1PdJ)

Zookeeper同时具有以下特性：

1. 顺序一致性：因为时主从模型，写操作只有一个Leader节点
2. 原子性：操作一个znode，集群中的所有Server要么都操作成功，要不都不成功（强一致性写入会导致不可用性，故采用最终一致性，也就是过半写入）
3. 统一视图：主从复制模型，连任何一个集群节点都可以看到所有的完整的数据（包括session，也就是说是session共享的）
4. 持久性：zk的数据存储在内存中，同redis相同他也有持久化策略
5. 实时性：在时间范围内是最新的数据

## 使用zk

```shell
# zkCli.sh

# 查看根路径下的znode
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]


# 创建一个znode，但是不指定值，那么他的值为 null
[zk: localhost:2181(CONNECTED) 1] create  /test1
Created /test1
[zk: localhost:2181(CONNECTED) 2] get /test1
null


# 创建一个znode，指定值
[zk: localhost:2181(CONNECTED) 3] create /test2 "test2"
Created /test2
[zk: localhost:2181(CONNECTED) 4] get /test2
test2


[zk: localhost:2181(CONNECTED) 5] ls /
[test1, test2, zookeeper]


# 已经存在的znode，不能被再次创建
[zk: localhost:2181(CONNECTED) 6] create /test2
Node already exists: /test2

# 使用get -s 查看znode的状态信息
[zk: localhost:2181(CONNECTED) 7] get -s /test2
test2
# zk是顺序执行的，因为Leader是一个单机只负责写入，所以通过Leader内部的自增ID来实现顺序执行的
# 这个顺序ID就是Zxid，代表zk的事务ID，其中c代表create创建操作
# cZxid的值是一个16进制数，前32位代表Leader纪元（每个Leader上任都是一个新的纪元），后32位代表事务在该纪元的递增ID
# 0x100000003 的纪元是1，事务ID为3
cZxid = 0x100000003
# 创建事务的时间
ctime = Thu Mar 24 03:11:13 UTC 2022
# 修改事务的事务id
mZxid = 0x100000003
# 修改事务的时间
mtime = Thu Mar 24 03:11:13 UTC 2022
# 当前节点下创建的最后子节点的节点号，如果没有子节点，那么这个值就是他本身
pZxid = 0x100000003
cversion = 0
dataVersion = 0
aclVersion = 0
# 节点所属的sessionID，如果是持久化节点，值为0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0


# 使用-e参数创建临时节点，临时节点的生命周期依托与session
[zk: localhost:2181(CONNECTED) 8] create -e /test3 "test3"
Created /test3
[zk: localhost:2181(CONNECTED) 9] get -s /test3
test3
cZxid = 0x100000005
ctime = Thu Mar 24 03:29:32 UTC 2022
mZxid = 0x100000005
mtime = Thu Mar 24 03:29:32 UTC 2022
pZxid = 0x100000005
cversion = 0
dataVersion = 0
aclVersion = 0
# 这里可以看到依托的session的ID
# 注意，连接session、断开session也会消耗事务id
ephemeralOwner = 0x300038d66c80000
dataLength = 5
numChildren = 0


# 在某个节点下创建子节点
[zk: localhost:2181(CONNECTED) 10] create /test1/a ""
Created /test1/a
[zk: localhost:2181(CONNECTED) 11] create /test1/b ""
Created /test1/b
[zk: localhost:2181(CONNECTED) 12] ls /test1
[a, b]
[zk: localhost:2181(CONNECTED) 13] get -s /test1/b
cZxid = 0x100000007
....
[zk: localhost:2181(CONNECTED) 14] get -s /test1
null
...
pZxid = 0x100000007 # 可以看到pZxid的值为最近一个添加的子节点的创建事务ID
...


# 创建序列节点，有序节点是会自动更改节点名称加上序列的节点
# 执行create后，将会返回节点的名称，需要程序自己记录这个名称，后续操作都要使用这个节点处理
# 因为会自动添加序列，所以有序节点可以创建多个
[zk: localhost:2181(CONNECTED) 19] create -s /test4
Created /test40000000003
[zk: localhost:2181(CONNECTED) 20] create -s /test4
Created /test40000000004
# zk内部会维护指定节点的序列，如果将所有的/test4序列节点删除，再次创建，他的序列不会重新开始
[zk: localhost:2181(CONNECTED) 29] delete /test40000000003
[zk: localhost:2181(CONNECTED) 30] delete /test40000000004
[zk: localhost:2181(CONNECTED) 31] create -s /test4
Created /test40000000005


# 使用 set 命令修改值
[zk: localhost:2181(CONNECTED) 32] set /test1 "aaa"
[zk: localhost:2181(CONNECTED) 33] get -s /test1
aaa
cZxid = 0x100000002    # 创建事务ID不变
ctime = Thu Mar 24 03:09:10 UTC 2022
mZxid = 0x10000000f    # 修改事务ID发生了变化
mtime = Thu Mar 24 06:03:37 UTC 2022
....
```

## 应用场景

### 同步（分布式锁）

相比较于Redis，zk实现分布式锁：

1. zk具有统一视图，其一致性相比于Redis要好得多
2. 锁的释放与死锁的判断可以通过基于session的临时节点，相比与Redis的方式（主动轮询）
   1. 基本没有什么延迟
   2. 如果等待锁的客户端较多，每次心跳将会给redis带来很大的压力
3. zk锁在释放时会回调所有的客户端，将会造成太大的压力
   1. 可以使用`序列节点+watch`是新公平锁，减少回调的客户端
   2. 注意，每个等待的客户端都去watch前一个锁（像一个链表）

分布式锁的实现：

1. 使用create命令添加一个znode，其名称为分布式锁的唯一标志
2. 当有一把锁存在时，其他进程使用create加锁会失败
3. 为了保证不出现死锁的情况，Redis会加上过期时间处理，但是zk不需要设置国企时间，只要保证这个节点的类型是临时节点即可。当应用程序的逻辑完成会自动结束会话，这时会自动释放锁。

分布式公平锁（队列式的锁）：

1. 首先创建锁的根节点
2. 每当有个进程需要加锁时，会在根节点下创建一个有序的临时节点 `create -e -s`
3. 进程加锁完毕后，会使用`ls`命令查询刚刚创建的有序节点是否在子节点中序列最小，如果最小，说明获取到了锁

### 分布式配置（配置中心/注册中心）

![image-20220327184019860](/files/oIDK4wVwq93AU0QrS2JW)

配置中心：

1. 将配置数据放置在znode节点中，使用目录结构对配置分类，znode的值存储配置内容（注意最多只有1MB大小）
2. 配置使用方**watch**目标配置znode，当配置发生变更时，通过事件`NodeDataChanged`可以保证客户端可以实时更新配置内容

注册中心：

1. 所有的应用程序启动会将其注册到一个znode的子节点下
2. 所有的应用程序watch该znode，当znode的children发生变更时，会触发`NodeChildrenChanged`事件，来通知注册中心的其他程序该服务的上线、下线的情况

## 集群

### 集群角色

一台zookeeper一般无法满足需求，通常都会对zookeeper集群

*参考：<https://blog.csdn.net/fu123123fu/article/details/81193780>*

1. Leader：
   * 事务请求的唯一调度和处理者，保证集群事务处理的顺序性
   * 集群内部各服务器的调度者
2. Follower：
   * 处理客户端非事务请求、**转发事务请求给Leader**
   * 参与事务请求Proposal 的投票（Leader 发起的提案，要求 Follower 投票，需要半数以上follower节点通过，leader才会commit数据）
   * 参与Leader的投票选举
3. Observer：
   * 观察者，不参与任何投票
   * 其余部分与Follower一致
   * 用于保证高可用，避免集群节点多，投票延时大的问题
   * 增加observer不影响集群中事务处理能力，同时还能提高集群非事务处理能力

> 一个zookeeper集群要对外提供服务，必须要保证过半的机器正常工作并且彼此之间能够正常通信。

### 集群搭建注意事项

如果要搭建一个能够允许F台机器down掉的集群，那么就要部署`2F+1`台服务器构成的 zookeeper 集群。因此3台机器构成的 zookeeper 集群，能够在挂掉1台机器后依然正常工作。一个5台机器集群的服务，能够对2台机器挂掉的情况下进行容灾。如果一个由6台服务器构成的集群，同样只能挂掉2台机器。因此，5台和6台在容灾能力上并没有明显优势，反而增加了网络通信负担。系统启动时，集群中的server会选举出一台server为 Leader，其它的就作为 follower（这里不考虑 observer 角色）。

### 集群特性

Zookeeper集群具有以下特性：

1. 扩展性：主要通过三个方面体现
   1. 集群角色分为 Leader、Follower、Observer，增加Follower和Observer可以扩展读能力，增加Observer在扩展读能力的情况下，不会影响zk的选举效率
   2. 采用读写分离的方式，且是一个主从复制集群，容易进行横向扩展
   3. 扩展方式通过修改配置文件即可
2. 可靠性：攘其外必先安其内
   1. 攘其外：具有最终一致性
   2. 安其内：当zk集群出现问题时，可以快速进行自我修复，在修复过程中集群会变为不可用状态，恢复之后再提供服务

zk基于分布式算法ZAB，参见文章`分布式一致性算法`。

## watch机制

watch机制是zk的核心。

![img](/files/PwcN3op2pD6AGarJTIs5)

1. 客户端1通过watch命令监控某个节点
2. 客户端2对节点进行一些操作，当发生操作后，客户端1会接受到一个事件通知
3. 事件分为如下几个类型：
   1. NodeCreated
   2. NodeDeleted
   3. NodeDataChanged
   4. NodeChildrenChanged
   5. None，客户端与Zookeeper断开连接


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yangsx95.gitbook.io/notes/distributed/fen-bu-shi-xie-tiao/zookeeper.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
