ZK 的应用场景

统一命名服务

命名服务也是分布式系统中比较常见的一类场景,在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务地址、提供者信息。被命名的实体通常可以是集群中的机器,提供的服务地址,远程对象等等,这些我们都可以统称它们为名字(Name)。其中较为常见的就是一些分布式服务框架中的服务地址列表,通过 ZK 提供的创建节点的 API,能够很容易创建一个全局的唯一 PATH,这个 PATH 就可以作为一个名称

配置中心

配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多个 PC Server 运行,但是它们运行应用的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每一台 PC Server,这样很麻烦,而且容易出错,像这样的配置信息完全可以交给 ZooKeeper 来管理,将配置信息保存在 ZK 中的某一个节点中,然后将所有需要修改的机器监控配置信息的状态,一旦发生改变,每台机器就会收到 ZK 的通知,然后从 ZK 中获取最新的配置信息并应用到系统中去

集群管理与 Master 选举

集群机器监控:这通常用于那种对集群中机器状态,机器在线率有较高要求的场景,能够快速对集群中机器的变化做出响应。这样的场景中,往往有一个监控中心,实时检测集群机器是否存活。过去的做法是:监控中心通过某种手段(比如:ping,心跳)定时检测每个机器,或者每个机器定时向监控中心发送心跳。

利用 ZK 有两个特性,就可以实时与另外一台集群机器存活性进行监控:

  1. 客户端在节点 X 上注册一个 Watcher,那么如果 X 的子节点变化了,会通知客户端
  2. 创建 Ephemeral 类型的节点,一旦客户端和服务器会话结束或过期,那么该节点就会消失

例如:监控系统在 /clusterServer 节点注册一个 Watcher,以后每动态添加机器,那么就往 /clusterServer 下创建一个 Ephemeral 类型的节点,如:/clusterServer/{hostname},这样,监控系统就能实时的知道机器增减情况

在分布式环境中,相同业务应用分布在不同的机器中,有些业务逻辑(如耗时计算,网络 I/O 处理),往往只要让集群中的某一台机器进行执行,其余机器可以共享这个结果,这样可以大大减少重复劳动,提高性能,于是 master 选举便是这种场景下碰到的主要问题

分布式锁

分布式锁,这个主要得益于 ZK 为我们保证了数据强一致性。锁服务一般分为两类,一个是保持独占,另一个是控制时序。

所谓保持独占:就是所有试图来获取这个锁的客户端,最终只有一个可以成功获取到这把锁,通常的做法就是把 ZK 上的 znode 看作一把锁,通过 create znode 方式来实现,所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端就拥有了这把锁。

控制时序:就是所有试图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序罢了,做法和独占类似,只是这里的 /distribute_lock 节点已经预先存在,客户端在它的下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL 来指定),ZK 的父节点(/distribute_lock)维护一份 sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序,然后依次执行

结点类型

ZK 有 4 种类型的节点,分别为持久节点,临时节点,持久顺序节点,临时顺序节点

持久节点

所谓持久****点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点

  1. create /test 1

临时节点(-e)

和持久节点不同的是,临时节点生命周期客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会被清除掉,注意:这里说的会话失效,而非断开连接。另外,临时节点下面不能创建子节点

create -e /temp 1

客户端连接服务端,会创建一个 session,当创建一个临时节点的时候,会将临时节点与 session 绑定,同时客户端会一直与服务端保持心跳,保证 session 不会过期,当调用 quit 指令的时候,表示通知服务端,断开与客户端的绑定,并删除 session,而当强行退出客户端的时候,服务端并没有接收到断开指令,同时此时也收不到客户端的心跳,那么 session 就会开始倒计时,一直到 session 过期,这就解释了,为什么断开连接的时候,再连接上,会看到临时节点,过一会又看不到临时节点的原因

持久顺序节点(-s)

这个节点的基本特性和持久节点是一致的,额外的特性是,在 ZK 中,每个父节点会为他的第一级子节点维护一份时序,会记录每个节点创建的先后顺序,基于这个特性,在创建子节点的时候,可以设置 -s 这个属性,那么在创建节点过程中,ZK 会自动为给定节点名加上一个数字后缀,作为新的节点名称,这个数字后缀的范围是整形的最大值。

$ create -s /seq/seq_ 1
Created /seq/seq_0000000000

临时顺序节点

临时顺序节点,类似临时节点和顺序节点

create -e -s /seq/seqt_ 1

ZK 对每个结点最大的数据量默认上限是 1M

stat

ZK 命名空间中的每一个 znode 都有一个与之关联的 stat 结构,类似于 unix/linux 文件系统中文件的 stat 结构,znode 的 stat 结构中的字段显示如下:

[zk: localhost:2181(CONNECTED) 4] get /test
content # 数据
cZxid = 0x22 # 创建 znode 的事务 ID
ctime = Tue Feb 18 22:15:52 CST 2020 # 创建时间
mZxid = 0x22 # 最后修改 znode 的事务 ID
mtime = Tue Feb 18 22:15:52 CST 2020 # 最近修改时间
pZxid = 0x22 # 最后修改或删除子节点的事务 ID
cversion = 0 # 对此 znode 的子节点进行更改的次数
dataVersion = 0 # 对此 znode 的数据进行更改的次数
aclVersion = 0 # 对此 znode 的 ACL 进行更改的次数
ephemeralOwner = 0x0 # 如果 znode 是 ephemeral 类型,则是 znode 的所有者的 session ID,否则为 0
dataLength = 7 # 此 znode 的数据字段长度
numChildren = 0 # 此 znode 的子结点数量

Zxid

类似于 RDBMS 中的事务 ID,用于标识一个更新操作的 Proposal ID,为了保证顺序性,该 zkid 必须单条递增。因此 ZK 使用一个 64 位的数来标识,高 32 位是 Leader 的 epoch,从 1 开始,每次选出新的 Leader,epoch 加 1,低 32 位为该 epoch 内的序号,每次 epoch 变化,都将低 32 位的序号重置,这样就保证了 zkid 的全局递增性

watch

一个 ZK 的结点可以被监控,包括这个目录中存储的数据,子节点目录的变化,一旦变化可以通知监控的客户端,这个功能是 ZK 对应应用最重要的特性,通过这个特性可是实现的功能包括配置的集中管理,集群管理,分布式锁等。

watch 机制官方说明:一个 watch 事件是一个一次性的触发器,当被设置了 watch 的数据发生了改变的时候,则服务器将这个改变发送给设置了 watch 的客户端,以便通知他们

  • 可以注册 watcher 的方法:getData、exists、getChildren
  • 可以触发 watcher 的方法:create、delete、setData。断开连接的情况下触发的 watcher 会丢失

一个 watcher 实例就是一个回调函数,被回调一次后就被移除了,如果还需要关注数据的变化,就需要再次注册 watcher。New Zookeeper 时注册的 watcher 叫 default watcher,它不是一次性的,只对 client 的连接状态变化而做出反应

Action event For “/path/child” event For “/path/child”
create(“/path”)


| EventType.NodeCreated | 无 | | delete(“/path”) | EventType.NodeDataChanged | 无 | | setData(“/path”) | EventType.NodeDataChanged | 无 | | create(“/path/child”) | EventType.NodeChildrenChange(getChild) | EventType.NodeCreated | | delete(“/path/child”) | EventType.NodeChildrenChange(getChild) | EventType.NodeDeleted | | setData(“/path/child”) | 无 | EventType.NodeDeleted |

event For “/path” Default
Watcher
exists(“/path”) getData(“/path”) getChildren(“/path”)
EventType.None
EventType.NodeCreated
EventType.NodeDeleted
EventType.NodeDataChanged
EventType.NodeChildrenChanged

exists 和 getData 设置数据监视,而 getChildren 设置子节点监视
**

常用命令

创建结点(znode)

用给定路径创建一个结点,flag 参数指定创建的节点时临时的、持久的还是顺序的。默认情况下,所有节点都是持久的。当会话过期或客户端断开连接时,临时节点(-e)将会被自动删除;顺序节点保证节点将是唯一的。ZK 集合将向节点路径填充 10 位序列号。例如:节点路径 /seq/seq_ 将转换位 /seq/seq_0000000001,下一个序列号将为 /seq/seq_0000000002,如果没有指定 flag,则节点默认是持久的

# 持久节点
create /path data

# 临时节点
create -e /path data

# 顺序节点
create -s /path/prefix data

获取数据

它返回节点的关联数据和指定节点的元数据,此 CLI 还用于分配监视器以显示数据相关的通知

$ get /test

content
cZxid = 0x22
ctime = Tue Feb 18 22:15:52 CST 2020
mZxid = 0x22
mtime = Tue Feb 18 22:15:52 CST 2020
pZxid = 0x22
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 7
numChildren = 0

设置数据

设置指定 znode 的数据,完成此操作后,可以用 get 命令检查数据

$ set /test someting

cZxid = 0x22
ctime = Tue Feb 18 22:15:52 CST 2020
mZxid = 0x23
mtime = Wed Feb 19 00:03:04 CST 2020
pZxid = 0x22
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 0

创建子节点

创建子节点类似创建新的 znode,唯一区别是,子节点的路径也将具有父路径

$ create /parent/children data

列出子节点

用于显示 znode 的子项

$ ls /path

状态检查

状态描述指定的 znode 元数据,它包含时间戳,版本号,ACL、数据长度、znode子项等明细

$ stat /path

cZxid = 0x22
ctime = Tue Feb 18 22:15:52 CST 2020
mZxid = 0x23
mtime = Wed Feb 19 00:03:04 CST 2020
pZxid = 0x22
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 7
numChildren = 0

移除节点

移除指定的 znode 并递归其所有子节点,只有在 znode 可用的情况下才会发生

$ rmr /path

ACL

ZK 作为分布式架构中的重要中间件,通常会在上面以节点的方式存储一些关键信息,默认情况下,所有应用都可以读写任何节点,在复杂的应用中,这不太安全,ZK 通过 ACL 机制来解决访问权限的问题

  • ZK 的权限控制是基于每一个 znode 节点的,需要对每个节点设置权限
  • 每个 znode 支持设置多种权限控制方案和多个权限
  • 子节点不会继承父节点的权限,客户端无权访问某个节点,但可能可以访问它的子节点

ACL 权限控制,使用 schema:id:permission 来标识,主要包括 3 个方面:

  • 权限模式(schema):鉴权的策略
  • 授权对象(id)
  • 权限(permission)

schema

  • world:只有一个用户:anyone,代表所有人(默认)
  • ip:使用 IP 地址认证
  • auth:使用已添加认证的用户认证
  • digest:使用”用户名/密码”的方式认证

id

  • world:只有一个 id,即 anyone
  • ip:通常是一个 ip 地址段,比如:192.168.0.124 或 192.168.0.1/24
  • auth:用户名
  • degest:自定义,通常是 username:BASE64(SHA-1(username:password))

权限

  • CREATE:简写 c,可以创建子节点
  • DELETE:简写 d,可以删除子节点
  • READ:简写 r,可以读取节点数据及显示子节点列表
  • WRITE:简写 w,可以设置节点数据
  • ADMIN:简写 a,可以设置节点访问控制列表

查看 ACL

$ getAcl /path

'world,'anyone
: cdrwa

默认创建的节点的权限是最开放的,所有都可以进行增删改查管理

设置 ACL

设置节点对所有人都有写和管理权限

# 语法
$ setAcl /parent world:anyone:wa

# 添加/登录 用户
$ addauth digest zhangsan:12345

# 再设置权限,这个节点只有 zhangsan 这个用户拥有权限
$ setAcl auth:zhangsan:12345:rdwca

Curator 客户端

  • Recipes:ZK 典型应用场景的实现,这些实现是基于 Curator Framework
  • Framework:ZK API 的高层封装,大大简化了 ZK 客户端编程,添加了 ZK 连接管理,重试机制等
  • Utilities:为 ZK 提供各种实用程序
  • Client:ZK Client 的封装,用于取代原生的 ZK 客户端(ZooKeeper 类),提供一些非常有用的客户端特性
  • Errors:Curator 如何处理错误,连接问题,可恢复的例外等

Curator 主要解决了三类问题

  • 封装 ZooKeeper Client 与 ZooKeeper Server 之间的连接处理
  • 提供了一套 Fluent 风格的操作 API
  • 提供 ZooKeeper 各种应用场景(Recipes、共享锁服务,集群领导选举机制)的抽象封装

原生 ZK 客户端存在的问题

客户端在连接服务端是会设置一个 sessionTimeout(session 过期时间),并且客户端会给服务端以心跳的形式刷新服务端 session 时间,当网络断开后,服务端无法收到客户端心跳,会进行 session 倒计时,判断是否超过了 session 过期时间,一旦过期,就算后来网络连通了,就会接到 session 过期的事件,从而删除临时节点和 watcher 等,原生客户端不会重建 session