1. zookeeper 概述
1. 概述
1.1 Zookeeper 简介
ZooKeeper 由雅虎研究院开发,后来捐赠给了 Apache。 ZooKeeper 是一个开源的分布式 应用程序协调服务器
,其为分布式系统提供一致性服务
。其一致性是通过基于 Paxos 算法的 ZAB 协议完成的
。其主要功能包括:配置维护、域名服务、分布式同步、集群管理等。 zookeeper 的官网: zookeeper.apache.org
1.2一致性
zk 是如何保证分布式系统的一致性的呢?是因为 zk 具有以下几方面的特点:
1.2.1 顺序一致性
zk 接收到的 n 多个事务请求(写操作请求),最终会严格按照其接收顺序被应用到 zk 中。
1.2.2 原子性
所有事务请求的结果在集群中每一台 zk 上的应用情况都是一致的。
1.2.3 单一视图
无论客户端连接的是 zk 集群中的哪个服务器,其看到的服务端数据模型都是一致的, 即 zk 主机中的 znode 都是相同的,这样可以保证用户读取到的数据都是相同的。 原子性是针对是写操作的,而单一视图是针对读操作的。
1.2.4 可靠性
一旦 zk 成功应用了一个事务,那么该事务所引起的 zk 状态变更(持久节点上的变更) 将会被一直保留下来,除非有另一个事务又对其进行了变更。
1.2.5 最终一致性
一旦一个事务被成功应用, zk 可以保证在一段较短的时间后,客户端最终能够从 zk 上 读取到最新的数据状态。但不能保证实时读取到。
1.3 基础理论
1.3.1 数据模型 znode
zk 数据存储结构与标准的 Unix 文件系统非常相似,都是在根节点下挂很多子节点。 zk 中没有引入传统文件系统中目录与文件的概念
,而是使用了称为 znode
的数据节点概念。znode 是 zk 中数据的最小单元,每个 znode 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间
。
zk 中虽然可以存放数据,但其主要作用并不是用于存储数据的
,而是通过在不同时间 创建不同关系、不同类型的 znode 节点,来描述和体现某种关系。
每个 znode 节点可以存放数据的大小上限为 1M 字节
。
节点类型
每个 znode 根据节点类型的不同,具有不同的生命周期。
持久节点
: 节点被创建后会一直保存在 zk 中,直到将其删除。持久顺序节点
: 一个父节点可以为它的第一级子节点维护一份顺序,用于记录每个子节 点创建的先后顺序。其在创建子节点时,会在子节点名称后添加数字序号,作为该子节 点的完整节点名。 序号由 10 位数字组成,从 0 开始计数。 bj0000000000 sh0000000001分布式协调服务器 Zookeeper临时节点
: 临时节点的生命周期与客户端的会话绑定在一起,会话消失则该节点就会被 自动清理。 临时节点只能作为叶子节点,不能创建子节点。临时顺序节点
: 添加了创建序号的临时节点。
节点状态
cZxid
: Created Zxid,表示当前 znode 被创建时的事务 IDctime
: Created Time,表示当前 znode 被创建的时间mZxid
: Modified Zxid,表示当前 znode 最后一次被修改时的事务 IDmtime
: Modified Time,表示当前 znode 最后一次被修改时的时间pZxid
: 表示当前 znode 的子节点列表最后一次被修改时的事务 ID。注意,只能是其子节点列表变更了才会引起 pZxid 的变更,子节点内容的修改不会影响 pZxid
。cversion
: Children Version,表示子节点的版本号。该版本号用于充当乐观锁。dataVersion
:表示当前 znode 数据的版本号。该版本号用于充当乐观锁。aclVersion
:表示当前 znode 的权限 ACL 的版本号。该版本号用于充当乐观锁。ephemeralOwner
:若当前 znode 是持久节点,则其值为 0;若为临时节点,则其值为创建该节点的会话的 SessionID。当会话消失后,会根据 SessionID 来查找与该会话相关的临时节点进行删除。dataLength
:当前 znode 中存放的数据的长度。numChildren
:当前 znode 所包含的子节点的个数。
1.3.2 Watcher 机制
zk 通过 Watcher 机制实现了发布/订阅模式。
(1) watcher 工作原理
(2) watcher 事件
对于同一个事件类型,在不同的通知状态中代表的含义是不同的。
客户端所处状态 | 事件类型(常量值) | 触发条件 | 说明 |
---|---|---|---|
SyncConnected | None(-1) | 客户端与服务器成功建立会话 | 此时客户端与服务器处于连接状态 |
NodeCreated(1) | Watcher 监听的对应数据节点被创建 | ||
NodeDeleted(2) | Watcher 监听的对应数据节点被删除 | ||
NodeDataChanged(3) | Watcher 监听的对应数据节点的数据内容发生变化 | ||
NodeChildrenChanged(4) | Watcher 监听的节点的子节点列表发生变化 | ||
Disconnected(0) | None(-1) | 客户端与 zk 断开连接 | 此时客户端与服务器处于连接断开状态 |
Expired(-112) | None(-1) | 会话失效 | 此时客户端会话失效,通 常 会 收 到SessionExpiredException异常 |
AuthFailed | None(-1) | 使用错误的 scheme进行权限检查 | 通 常 会 收 到AuthFailedException |
(3) watcher 特性
zk 的 watcher 机制具有非常重要的三个特性:
一次性
: 一旦一个 watcher 被触发, zk 就会将其从客户端的 WatchManager 中删除, 当然也就会从服务端删除。 当需要再使用时客户端需要再向 zk 重新注册 watcher。串行性
: 同一 znode 的相同事件类型所引发的 watcher 回调方法的执行是串行的,同步的。轻量级
: 客户端向服务端注册 watcher,并没有将整个 watcher 实例发送到服务端,而是向服务端发送了 watcher 中的部分必要数据
。 watcher 实例及回调逻辑仍是存放在客户端的。这是一种轻量级设计。
zk 的 watcher 机制不适合监听变化非常频繁的场景
,也不适合在 watcher 回调中进行耗时 IO 型操作。
kafka 的 Consumer 的消费 offset 在 0.8 版本之前是保存在 zk 中,而之后的版本是保存在 broker 中。
2. 典型应用场景
为进一步加强对 zk 的认识,理解 zk 的作用,下面再详细介绍一下 zk 在生产环境中的典 型应用场景解决方案设计。
2.1配置维护
2.1.1 什么是配置维护
分布式系统中,很多服务都是部署在集群中
的,即多台服务器中部署着完全相同的应用, 起着完全相同的作用。当然,集群中的这些服务器的配置文件是完全相同的
。
若集群中服务器的配置文件需要进行修改,那么我们就需要逐台修改这些服务器中的配 置文件。如果我们集群服务器比较少,那么这些修改还不是太麻烦,但如果集群服务器特别 多,比如某些大型互联网公司的 Hadoop 集群有数千台服务器,那么纯手工的更改这些配置 文件几乎就是一件不可能完成的任务。即使使用大量人力进行修改可行,但过多的人员参与, 出错的概率大大提升,对于集群所形成的危险是很大的。
同类产品还有 Spring Cloud Config、 Nacos Config、 Apollo 等。
2.1.2 实现原理
zk 可以通过“发布/订阅模型”实现对集群配置文件的管理与维护。
2.2命名服务
2.2.1 什么是命名服务
命名服务是指可以为一定范围内的元素命名一个唯一标识
,以与其它元素进行区分。在 分布式系统中被命名的实体可以是集群中的主机、服务地址等。
UUID、 GUID,其存在的明显问题有两个:长度太长,无语义。
2.2.2 实现原理
通过利用 zk 中节点路径不可重复的特点来实现命名服务的。当然,也可以配带上顺序 节点的有序性来体现唯一标识的顺序性。
2.3集群管理
2.3.1 什么是集群管理
对于集群,我们总是希望能够随时获取到当前集群中各个主机的运行时状态、当前集群 中主机的存活状况等信息
。通过 zk 可以实现对集群的随机监控。
2.3.2 分布式日志收集系统
下面以分布式日志收集系统为例来分析 zk 对于集群的管理。
(1) 系统组成
首先要清楚,分布式日志收集系统由四部分组成:日志源集群、日志收集器集群, zk 集群,及监控系统。
(2) 系统工作原理
分布式日志收集系统的工作步骤有以下几步:
- 收集器的注册:在 zk 上创建各个收集器对应的节点。
- 任务分配:系统根据收集器的个数,将所有日志源集群主机分组,分别分配给各个收集器。
- 状态收集:这里的状态收集指的是两方面的收集:
- 日志源主机状态,例如,日志源主机是否存活,其已经产生多少日志等
- 收集器的运行状态,例如,收集器本身已经收集了多少字节的日志、当前 CPU、内存的使用情况等
- 任务再分配 Rebalance : 当出现收集器挂掉或扩容,就需要动态地进行日志收集任务再分配了,这个过程称为Rebalance。只要发现某个收集器挂了,则系统进行任务再分配。
- 全局动态分配:简单粗暴,但对系统性能的影响非常巨大。一般不会使用。
- 局部动态分配:需要首先定义各个收集器的负载判别标准,根据负载情况进行再分配。
2.3.3 集群管理的一般性原理
zk 进行集群管理的一般性原理如下图所示。
2.4DNS 服务
zk 的 DNS 服务的功能主要是实现消费者与提供者的解耦合。可以防止提供者的单点问 题,实现对提供者的负载均衡等。
2.4.1 什么是 DNS
DNS, Domain Name System,域名系统, 即可以将一个名称与特定的主机 IP+Port 进行 绑定。 zk 可以充当 DNS 的作用,完成域名到地址的映射。
2.4.2 基本 DNS 实现原理
假设提供者应用程序 app1 与 app2 分别用于提供 service1 与 service2 两种服务,现要将其注册到 zk 中,具体的实现原理如下图所示。
2.4.3 具有状态收集功能的 DNS 实现原理
以上模型存在一个问题,如何获取各个提供者主机的健康状态、运行状态呢?可以为每 一个域名节点再添加一个状态子节点,而该状态子节点的数据内容则为开发人员定义好的状 态数据。这些状态数据是如何获取到的呢?是通过状态收集器(开发人员自行开发的)定期 写入到 zk 的该节点中的。 阿里的 Dubbo 就是使用 Zookeeper 作为域名服务器的。
2.4.4 对方案的优化
前面的方案无法对提供者的存活状态进行实时监控。
2.5 Master 选举
2.5.1 什么是 Master 选举
集群是分布式系统中不可或却的组成部分,是为了解决分布式系统中计算单元的单点问 题,水平扩展计算单元的处理能力的一种解决方案。
一般情况下,会在群集中选举出一个 Master,用于协调集群中的其它 Slave 主机,对于 Slave 主机的状态具有决定权。
2.5.2 广告推荐系统
(1) 需求
系统会根据用户画像,将用户归结为不同的种类。系统会为不同种类的用户推荐不同的 广告。每个用户前端需要从广告推荐系统中获取到不同的广告 ID。
(2) 分析
这个向前端提供服务的广告推荐系统一定是一个集群,这样可以更加快速高效的为前端 进行响应。需要注意,推荐系统对于广告 ID 的计算是一个相对复杂且消耗 CPU 等资源的过 程。如果让集群中每一台主机都可以执行这个计算逻辑的话,那么势必会形成资源浪费,且 降低了响应效率。此时,可以只让其中的一台主机去处理计算逻辑,然后将计算的结果写入 到某中间存储系统中,并通知集群中的其它主机从该中间存储系统中共享该计算结果。那么, 这个运行计算逻辑的主机就是 Master,而其它主机则为 Slave。
(3) 架构
(4) Master 选举
这个广告推荐系统集群中的 Master 是如何选举出来的呢?使用 zk 可以完成。 使用 zk 中多个客户端对同一节点创建时,只有一个客户端可以成功的特性实现。 使用 DBMS 是否也可以实现 Master 选举?当然可以。让所有集群 server 向同一张表中 同时写入同一个主键的数据,只有一个可以写入成功。成功的就是 Master。但 Master 宕机 无法实现 Master 的自动重新选举。 例如 Kafka 集群中 Broker Controller 就是集群中的 Master,就是通过 zk 选举的。 Broker 中的 partition 分区中的 leader 是由 Broker Controller 负责选举的。
2.6分布式同步
2.6.1 什么是分布式同步
分布式同步,也称为分布式协调,是分布式系统中不可缺少的环节,是将不同的分布式 组件有机结合起来的关键。对于一个在多台机器上运行的应用而言,通常需要一个协调者来 控制整个系统的运行流程,例如执行的先后顺序,或执行与不执行等。
2.6.2 MySQL 数据复制总线
下面以“MySQL 数据复制总线”为例来分析 zk 的分布式同步服务。
(1) 数据复制总线组成
MySQL 数据复制总线是一个实时数据复制框架,用于在不同的 MySQL 数据库实例间进 行异步数据复制。其核心部分由三部分组成:生产者、复制管道、消费者。
那么, MySQL 数据复制总线系统中哪里需要使用 zk 的分布式同步功能呢?以上结构中 可以显示看到存在的问题: replicator 存在单点问题。为了解决这个问题,就需要为其设置 多个热备主机。那么,这些热备主机是如何协调工作的呢?这时候就需要使用 zk 来做协调 工作了,即由 zk 来完成分布式同步工作。
(2) 数据复制总线工作原理
MySQL 复制总线的工作步骤,总的来说分为三步:
- 复制任务注册:复制任务注册实际就是指不同的复制任务在 zk 中创建不同的 znode,即将复制任务注册 到 zk 中。
- replicator 热备:复制任务是由 replicator 主机完成的。为了防止 replicator 在复制过程中出现故障,replicator 采用热备容灾方案, 即将同一个复制任务部署到多个不同的 replicator 主机上,但仅使一个处于 RUNNING 状态,而其它的主机则处于 STANDBY 状态。当 RUNNING 状态的主机出现故障,无法完成复制任务时,使某一个 STANDBY 状态主机转换为 RUNNING 状态,继续完成复制任务。
- 主备切换 : 当 RUNNING 态的主机出现宕机,则该主机对应的子节点马上就被删除了,然后在当前处于 STANDBY 状态中的 replicator 中找到序号最小的子节点,然后将其状态马上修改为RUNNING,完成“主备切换”。
2.7分布式锁
分布式锁是控制分布式系统同步访问共享资源的一种方式。 Zookeeper 可以实现分布式 锁功能。根据用户操作类型的不同,可以分为排他锁(写锁)与共享锁(读锁)。
2.7.1 分布式锁的实现
在 zk 上对于分布式锁的实现,使用的是类似于“/xs_lock/[hostname]-请求类型-序号” 的临时顺序节点。当客户端发出读写请求时会在 zk 中创建不同的节点。根据读写操作的不 同及当前节点与之前节点的序号关系来执行不同的逻辑。
具体实现步骤:
- Step1: 当一个客户端向某资源发出读/写操作请求时,其首先会尝试着在 zk 中创建一个根节点。当然,若其不是第一个请求,则该根节点已经创建完毕,就无需再创建了。
- Step2: 根节点已经存在了,客户端会对
根节点注册子节点列表变更事件的 watcher 监听
,随时监听子节点的变化情况。 - Step3: watcher 注册完毕后,
其会在根节点下创建一个读写操作的临时顺序节点
。读写操作的顺序性就是通过这些子节点的顺序性体现的。每个节点都只关心序号比自己小的节点。因为它们的请求是先于自己提出的,需要先执行。 注意,读写操作创建的节点名称是不同的:- 若当前为读请求,则会创建一个“hostname-R-序号”的子节点
- 若当前为写请求,则会创建一个“hostname-W-序号”的子节点
- Step4: 节点创建完后,其就会触发客户端的 watcher 回调,读取根节点下的所有子节点列表,然后会查看序号比自己小的节点,并根据读写操作的不同,执行不同的逻辑。
读请求
: 若没有比自己序号小的子节点,或所有比自己序号小的子节点都是读请求,则表明自己可以开始读数据了;若比自己序号小的子节点中有写请求,则当前客户端不能对数据进行读操作,而是进入等待状态,等待前面的写操作执行完毕。写请求
: 若发现自己是序号最小的子节点,则表明当前客户端可以开始数据更新了;若发现还有比自己序号更小的子节点,无论是读还是写节点,当前客户端都不能对数据进行写操作,而是进入等待状态,等待前面的所有操作执行完毕。
- Step5: 客户端操作完毕后,与 zk 的连接断开,则 zk 中该会话对应的节点消失。当然,该操作会引发各个客户端再次执行 watcher 回调,查看自己是否可以执行操作了。
2.7.2 分布式锁的改进
前面的实现方式存在“惊群效应”
,为了解决其所带来的性能下降,可以对前述分布式 锁的实现进行改进。
由于一个操作而引发了大量的低效或无用的操作的执行,这种情况称为惊群效应。
当客户端请求发出后,在 zk 中创建相应的临时顺序节点后马上获取当前的/xs_lock 的所 有子节点列表,但任何客户端都不向/xs_lock 注册用于监听子节点列表变化的 watcher。而是改为根据请求类型的不同向“对其有影响的”子节点注册 watcher
。
读请求
: 若其前面都是读请求节点,则直接开始读操作;若其前面有写请求节点,其只 需向序号小于自己的最后一个写请求节点注册“节点删除” watcher,然后等待。写请求
: 若其查看到自己就是序号最小的节点,则直接开始写操作;若发现还有更小的 节点,则其只需向序号小于自己的最后一个节点注册“节点删除” watcher,然后等待。
2.8分布式队列
说到分布式队列,我们马上可以想到 RabbitMQ、 Kafka 等分布式消息队列中间件产品。 zk 也可以实现简单的消息队列。
2.8.1 FIFO 队列
zk 实现 FIFO 队列的思路是:利用顺序节点的有序性
,为每个数据在 zk 中都创建一个相 应的节点。然后为每个节点都注册 watcher 监听。一个节点被消费,则会引发消费者消费下 一个节点,直到消费完毕。
2.8.2 分布式屏障 Barrier 队列
Barrier,屏障、障碍物。 Barrier 队列是分布式系统中的一种同步协调器,规定了一个队 列中的元素必须全部聚齐后才能继续执行后面的任务,否则一直等待。其常见于大规模分布 式并行计算的应用场景中:最终的合并计算需要基于很多并行计算的子结果来进行。
zk 对于 Barrier 的实现原理是,在 zk 中创建一个/barrier 节点,其数据内容设置为屏障打开的阈值,即当其下的子节点数量达到该阈值后, app 才可进行最终的计算,否则一直等 待。每一个并行运算完成,都会在/barrier 下创建一个子节点,直到所有并行运算完成。