(终于迁移完了 iteye 上的文章,可以在这写一点新东西了。)
注:此文是基于我的一次技术分享讲座整理的。文章内容顺序大致遵循讲演的顺序,所以部分知识点会穿插提到。
可下载附件提供的PPT,结合其中动画更易理解。(附件)
目录
9. 【应用案例】主从模式任务系统状态变化处理策略(接上文)
12.4 Zab:ZooKeeper Atomic Broadcast Protocol
1. “实用 + 硬核”?
切入门槛低,循序渐进,非程序员也能听懂。但又不是纯粹的蜻蜓点水式走过场。
避免无趣易得的灌水话题/知识。不讲如何安装 ZooKeeper,不讲如何写业务代码对接 ZooKeeper。
讲解核心原理,不纠结晦涩算法。分析 ZooKeeeper 核心逻辑,但不陷入具体如何实现的 “泥潭”。
讲解重点与易忽略的关键点。
讲“术”的目的是为了传“道”,而非“术”本身。
2. 先导说明:各种“节点”
此文设计多个“节点”概念,需注意区分。
PS:“节点”是一个含义非常宽泛的词,实际使用时要尽量为其铺垫足够清晰的上下文。否则很容易出垃圾文案,甚至把自己给弄糊涂(可能本就是因为自己思维混乱,才搞出了个模糊的“节点”概念)。
2.1 应用服务节点
多个应用服务实例通过 ZooKeeper 协同工作:
2.2 ZooKeeper 数据节点(znode)
一个任务处理系统在 ZooKeeper 中维护的数据:
2.3 ZooKeeper 服务节点
一个 ZooKeeper 集群:
3. 应用实例数据展示
3.1 ZooKeeper 架构总览
各应用程序通过 ZooKeeper 客户端与 ZooKeeper 服务端连接,并实现相关业务逻辑。
ZooKeeper服务端可以是独立模式(一个实例),也可以是仲裁模式(多个实例)。
3.2 实例数据展示
读者可自行找一个使用了 ZooKeeper 的系统进行研究;借助 zkui 之类的工具查看 ZooKeeper 中存储的数据。
如,Dubbo 服务就是应用 ZooKeeper 的典型场景。可通过 zkui 找到数据节点 dubbo,研究 providers 和 consumers 等各级子节点内容。这对于了解 Dubbo 也是很有帮助的。
zkui初始界面:
4. ZooKeeper 的角色
4.1 ZooKeeper 的作用
- ZooKeeper旨在简化构建分布式系统的任务。
- 让一个应用中多个独立的程序协同工作是非常困难繁琐的。这很容易让开发人员陷入协同工作逻辑,而无暇思考与实现业务逻辑。
- ZooKeeper提供了一套易用的API,简化了协同逻辑的实现。
4.2 应用进程协同方式
- 分布式系统中进程通信的两种方式:
- 直接通过网络进行信息交换
- 读写某些共享存储
- 应用程序通过读写ZooKeeper这个共享存储交换信息。
- ZooKeeper集群中各服务实例直接通过网络进行信息交换。
4.3 ZooKeeper存的是什么数据?
- ZooKeeper管理的是协同数据,不是应用数据。
- 例,邮箱服务中,邮箱与某邮箱服务器之间的映射关系是协同数据,邮箱内容是应用数据。
- ZooKeeper不适合用于海量数据存储。设计应用时需将应用数据和协调数据分开管理。
4.4 ZooKeeper 适合处理什么问题?
“主节点选举”、“崩溃检测”等常见分布式系统技术问题。
4.5 关于CAP
- ZooKeeper设计目标是一致性和可用性。
- 当发生网络分区时,提供只读服务(需额外配置,并根据业务规则进行取舍)
5. ZooKeeper名字的由来
ZooKeeper 最初由雅虎研究院开发。其开发小组参与过很多以动物命名的项目,不想再使用动物的名称。他们觉得分布式系统就像一个动物园,混乱且难以管理,而 ZooKeeper 让这些系统中的应用变得可控。所以就将此项目命名为 ZooKeeper。
6. 典型应用案例:【主从模式】一种主从模式的任务处理系统
注:此案例仅用于辅助讲解 ZooKeeper。真实的任务处理系统通常会比这个案例复杂得多。
6.1 应用程序角色(分工)
- 主实例:分配任务给从实例
- 从实例:执行任务、发布结果
- (客户端)提交任务、接收结果
6.2 常见状态变化
- 管理权变更
- 从实例列表变更
- 出现新任务待分配(主实例)
- 出现新任务待执行(从实例)
- 任务执行结束(结果)
6.3 ZooKeeper树形数据结构
此案例中需要这些基本数据节点实现任务处理系统的关键逻辑。
6.4 主从模式 之 管理权变更(分布式锁)
这个管理权的变更是为了确定任务系统的核心控制者——群首。它其实是对分布式锁的应用。
6.4.1 群首选举
-
各客户端实例尝试创建 /master 节点,成功者成为主实例。
-
如果 /master 节点已存在,则创建操作将失败。
6.4.2 从实例自动取代已崩溃的主实例
-
主实例崩溃后自动释放 /master 节点
-
/master 是一个临时性的 znode
-
持久节点:只能通过 delete 删除。
-
临时节点:创建该节点的客户端 会话过期 或 关闭 时会被删除;也可以被客户端(不一定是创建者)主动删除。
-
-
-
从实例监听 /master 节点的删除事件(监视点)
-
当收到 /master 节点被删除的通知时,发起新一轮的群首选举
-
7. ZooKeeper 会话
会话状态转换:
- 客户端会向服务端发送心跳以维护会话
- ZooKeeper 服务端复杂声明某会话已过期(客户端不能声明)
- 客户端可以关闭会话
- 客户端可在创建连接时指定会话超时时间(如,30s)
8. ZooKeeper 监视与通知
8.1 客户端如何感知 znode 发生了变化?
- 变化类型:节点添加与删除、节点数据变化、子节点列表变化
- 低效的实现方式:客户端轮询。
- 反面教材:Curator 中的 Reaper 和 ChildReaper。客户端可以利用这两个类清除 ZooKeeper 上无子节点的数据节点。它们的实现方式就是轮询目标数据节点是否有子节点,如果没有,就发送删除指令。
8.2 ZooKeeper 的通知机制
- 客户端向 ZooKeeper 注册针对目标 znode 的监视点。
- znode 发生变化时,ZooKeeper 会向客户端发生通知。
8.3 监视点类型
- 数据监视点(NodeCreated,NodeDeleted,NodeDataChanged)
- 子节点监视点(NodeChildrenChanged)
8.4 单次触发机制 与 变更事件丢失
- 一个监视点只触发一次。客户端可在收到通知后重建监视点。
- 从触发监视点到重建监视点之间,可能有变更事件丢失。
- 即,多次变更被 “压缩” 为一次通知。
相关流程:
此处的 “变更事件丢失” 对业务会有什么影响?
对于此文提到的任务系统来说没有影响。当从实例节点上下线触发监视点时,主实例会从 ZooKeeper 读取从实例列表。在这个读取操作中可以 原子化 地重建监视点。而读取到的从实例列表信息必定是读取操作发生时的准确信息(ZooKeeper的数据一致性)。也就是说,上图中,从 Worker-1上线触发监视点 到 Client(主实例)读取 Worker 列表(从实例)之间,无论发生任何 Worker 上下线事件,都不会破坏 主实例 管理 从实例 的任何业务逻辑。这也是此任务系统的业务特征决定的。它需要的是一个类似“最终数据一致性”的效果,而不用关系中间过程的数据变化。
如果你的业务应用需要知道每次 Worker 上下线事件,那么这种模式显然是不合适的。“A-B-A”问题就是类似的问题。
9. 【应用案例】主从模式任务系统状态变化处理策略(接上文)
基于上述对 ZooKeeper 的接收,我们可以确定此任务系统中那5个状态变化的处理策略:
10. ZooKeeper 的基础API 与 高级封装库
10.1 基础API(部分)
API | 作用 |
create /path data | 创建一个路径为 /path 的节点,并包含数据 data |
delete /path | 删除路径为 /path 的节点 |
exists /path | 检查是否存在路径为 /path 的节点 |
setData /path data | 设置路径为 /path 的节点的数据为 data |
getData /path | 返回路径为 /path 的节点的数据 |
getChildren /path | 返回 /path 节点的所有子节点列表 |
multiop op1 op2 ... | 原子性地执行多个操作 |
10.2 为什么 ZooKeeper 不直接提供原语服务?
目前,为了满足分布式锁等需求,客户端会通过 ZooKeeper 提供的那些基础API,组成一个类似文件系统的数据结构,也就是那个树形结构,进而在客户端实现相关原语。在前述任务系统中,单一个“管理权变更”就需要编写大量代码,还很容易出错。这似乎与 ZooKeeper 的初衷。为什么 ZooKeeper 不直接提供一“群首选举”的原语服务呢?采用这个设计策略主要是基于灵活性考虑。
原语服务API是分布式系统的基本需求。如,分布式锁需要三个基本方法:
- create —— 创建锁
- acquire —— 获取锁
- release —— 释放锁
各种编程语言中也有自己的锁实现。如:
- Python threading 模块中的 Lock、Condition、Semaphore
- GO 中的 sync.WaitGroup
- Java 中的 synchonized、ReentrantLock、CountDownLatch、Semaphore
如果 ZooKeeper 要直接提供原语服务,那么它得预先提供一份详尽的原语列表,或提供API扩展,以便引入新的原语。毕竟实际项目中的需求类别是非常多的,很难由直接提供的原语服务涵盖。另外,直接使用原语服务也会使得客户端应用与服务端具体原语类型耦合。这些限制对于项目研发和维护都是比较大的坑。
10.3 高级封装库 —— Curator
虽然 ZooKeeper 不直接提供原语服务是明智的策略,但是对业务应用开发使用原语的需求还是存在的。在业务代码中直接大量使用 ZooKeeper 原生基础API,也不是一个明智的选择。毕竟这些API太偏底层,开发效率会很低。所以我们可以使用高级封装库——Curator。
Curator最初是 Netflix 实现的,后来成为 Apache 基金会的项目。它的核心目标是“管理 ZooKeeper 操作,隐藏复杂的连接管理”。它提供一些常见的分布式系统原语,如,LeaderSelector、PathChildrenCache、DistributedAtomicInteger、DistributedBarrier 等。此外,它还提供了 fluent API,比原生 ZooKeeper 客户端库的API更友好。
fluent API示例:
- 使用原生客户端库API:
-
zk.create(“/mypath”, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT)
-
- 使用 Curator API:
-
zk.create() .inBackground() .withMode(CreateMode.PERSISTENT) .forPath(“/mypath”, new byte[0])
-
10.4 分层设计
这里多说几句分层设计思想。这是最基本的管理思想之一。它对项目管理和公司运营来说也是非常重要的。
- 明确的分工是高效率的基石
- 任何含混不清,边界不明晰的地方都是坑
10.4.1 C语言中的“共用体(union)”
一个 共用体 可以存放多种数据类型的成员,这些数据成员共享同一块内存,但任一时刻只有一个成员有效。例如,此共用体 Tester 中,如果设置 id 字段,则 id 字段数据生效 name 字段数据无效;反之亦然。在以前计算机内存非常小的时候(嵌入式开发中的内存也很小),共用体的使用场景比较多。现在内存已经不再那么昂贵,几乎很少会用到它。众多高级语言中已经没有这类数据结构了。因为这类数据结构的职责不够单一明确,容易错用。它省下的那点内存,远远不如开发效率与易维护性来得重要。
union Tester {
int id;
char name[30];
} t;
11. ZooKeeper 客户端故障处理
11.1 处理 可恢复的故障
如果网络故障等原因导致客户端与 ZooKeeper 服务端连接断开,且会话尚未失效,那么这个故障就是可恢复的故障。
(前文已经说过客户端会向 ZooKeeper 发送心跳来维持会话,由 ZooKeeper 服务端负责声明会话已过期。)
客户端(库)会自动重连来恢复此故障。
但是这个自动恢复也是有危害的。我们需要根据实际业务来规避这个危害。此处提供一个“注册主实例”相关的案例:
Client 通过创建 /master 节点使自己注册为主实例。ZooKeeper 服务端成功创建 /master 节点,然后在响应 Client 时发生了网络故障,导致 Client 未收到响应。继而 Client 重试,并发现 /master 节点已存在。Client 就认为其它实例已成为主实例,自己以从实例的角色运行。
在这个案例中,我们可以为 代表主实例的数据节点 /master 设置一个特有的标识,比如将 Client 的唯一标识作为 /master 节点的内容;当后续的重试失败时,检查 /master 的内容判断 /master 是不是此 Client 自己创建的。
11.2 处理 不可恢复的故障
如果会话失效,那么 ZooKeeper 会丢弃该会话。会话过期 和 认证方式失效 都导致会话失效。
这种情况下,只能通过重启客户端,用新的会话来重新初始化。
如果强行自动恢复,即 客户端重建 ZooKeeper 句柄,替换原句柄,那么可能导致非常严重的后果。
还是以“注册主实例”为例:
C1 注册成为主实例后,因为某些原因,ZooKeeper 服务端长时间未收到 C1 的心跳,所以将其会话判定为过期,并通知 C2。C2 注册成为主实例。C1 发现自己的会话被 ZooKeeper 判定为已过期。然后 C1 强制自动恢复 —— 创建一个新的 ZooKeeper 句柄,替换原来已被判定为过期的句柄。而且 C1 也不检查当前 ZooKeeper 上的主实例注册信息是不是自己的,认为自己还保有之前获取的主实例角色,继续以主实例的角色处理业务。这样导致该业务系统中同时存在多个主实例(C1 和 C2),继而破坏业务正确性。
11.3 访问外部资源时的数据一致性问题
不经过 ZooKeeper 访问外部资源的行为无法得到一致性保障。
乍一看,这句话是正确的废话。因为我们使用 ZooKeeper 就是为了协调分布式系统中各应用实例,从而获得一致性保障。所以我们在编写业务代码时肯定会考虑这一点,保证各项外部资源在 ZooKeeper 的管理范围内。
但现实情况可能会让人很意外,明明代码逻辑“毫无破绽”,但是非一致性的外部资源访问行为还是出现了。
11.3.1 案例
还是以主从模式为例:
假设在这系统中,规定只有主实例才能访问数据库,以保证数据一致性。
因为 C1 所在服务器的负载非常高,导致心跳发送不及时。ZooKeeper 服务端就将 C1 判定为会话过期。然后 C2 替代 C1 成为主实例,获得访问数据库的权限。ZooKeeper 服务端也会告知 C1 会话过期,我们编写的客户端代码逻辑也是在收到会话过期事件时终止数据库访问。但是因为线程调度的不可预知性,客户端的事件处理线程并未及时通知业务线程。C1 服务器负载过高也会增大这种情况发生的几率。这样就导致同时存在两个实例(C1 和 C2)访问数据库。
注:这个场景确实比较极端。绝大多数组织都不会考虑到这个场景。但这种场景就属于组织综合水平越差越容易发生的那一类。越是对软件架构意识薄弱的组织,IT运维水平也越垃圾,越可能出现网络高延迟与服务器负载过高,越会为这种极端场景“创造”条件。而且组织的业务架构水平也是越垃圾,既不会从技术层面考虑到这类异常,也不会有完善自洽的业务设计。所以一旦发生此类异常,很可能就是一个排查成本极高的严重事故。
11.3.2 解决方法
- 避免系统负载过高
- 改善系统设计并监控系统负载,避免负载过高。这样可以降低客户端发送心跳不及时的几率,也能降低通知业务线程不及时的几率。
- 隔离符机制
- 用群首节点的 czxid 作为访问外部资源的隔离符
- 前述案例中的 /master 节点就是关键的群首节点
- czxid 是节点创建时的 zxid
- zxid 是 ZooKeeper 分配给事务的标识符。它是一个单调递增的整数。
- 当访问外部资源时,如果外部资源系统持有更高版本的 czxid,它就会拒绝访问
- 用群首节点的 czxid 作为访问外部资源的隔离符
示例如下:
C1 获得的 czxid 为 3。后续的 C2 获得的 czxid 为 4。数据库会记住访问者提供的最大 czxid,作为隔离符。数据库收到 C2 的访问请求时,会记住最大的隔离符为 4。后续 C1 访问数据库时提供的 czxid 为3,数据库就会拒绝 C3 的访问。
显然,隔离符机制比“避免系统负载过高”更有效。但它的实现代价也很高:
- 需要修改外部资源的访问协议
- 外部资源系统需持久化保存最新的隔离符
12. ZooKeeper 内部原理
12.1 ZooKeeper 服务器角色
ZooKeeper 可以是一个服务实例单独部署,也可以是多实例集群部署。在集群模式中有三种角色:群首、追随者、观察者。
12.1.1 追随者 vs 观察者
- 追随者:参与群首选举。可能成为群首。
- 观察者:也执行来自群首的事务,但不参与选举。
12.1.2 观察者机制的作用
- 提高 读请求 的可扩展性
- 不牺牲 写操作 吞吐率 的前提下,提高 读操作 性能。
- 写操作 的吞吐率取决于 仲裁 数量。
- 优化跨数据中心部署的性能
- 将 参与仲裁的服务器 部署在同一个数据中心内
- 这样可以使得 写操作 的延迟较低。因为仲裁操作在同一个数据中心内完成。
- 其它数据中心部署 观察者
- 服务实例增多可以提高吞吐率
- 将 参与仲裁的服务器 部署在同一个数据中心内
注意:观察者无法提高整个 ZooKeeper 集群的可用性!
12.2 群首选举
12.2.1 常规流程
在群首选举时,每个 ZooKeeper 服务实例都会向其它各实例发送自身的 服务实例ID 和 当前数据的最新版本号(最新ZooKeeper事务的ID)。每个 ZooKeeper 实例收到其它实例发给它的信息后会按照一定的规则确定群首投票。数据版本号最新的服务实例优先成为群首;如果版本号相同,则服务实例ID更大的实例优先成为群首。每个 ZooKeeper 服务实例确定投票后,又会将投票信息公布给其它各实例。最终整个集群达成共识,确定一个唯一的群首。
12.2.2 异常情况——网络延迟
当发生网络延迟导致群首选举的正常流程被破坏时,处理流程会比较复杂:
上图中,S3 向 S2 发送信息时发生了网络延迟。S2 迟迟未收到 S3 的信息,认为 S3 已宕机。所以 S2 根据 S1 和 自身的情况,认为应该选 S1 当群首。还是因为网络延迟,S2 迟迟未等到 S1 的响应,所以会不断重试。过了很久之后 S2 才收到 S1 的投票信息:“选 S3 当群首”。也就是说 S2 之前的投票是错误的。收到 S1 的投票信息后 S2 才会意识到自己投错票,并纠正自身状态。上图红色粗线代表了 S2 的这段异常状态。在此期间:
- S1 不会以群首的身份响应 S2 的请求;
- S2 等待 S1 的响应超时,触发重试;
- S2 在完成选举之前,无法处理客户端请求。
所以错误的群首选举可能导致整个 ZooKeeper 长时间不可用。可能需要多轮群首选举才能最终进入正常工作状态。
针对这种情况,ZooKeeper 的优化方法就是多等一会儿。也就是说让 S2 不要那么早就认定 S3 已宕机,提高对网络延迟的容忍度。那么要等多久呢?ZooKeeper 采用了一个固定值:200毫秒。为什么是这个值?这是根据现代数据中心的特征确定。200毫秒已远大于数据中心内部延迟,但它又小于纠正错误选举所耗费的时间。
12.3 多数原则
有个概念叫“法定人数”。它的意思是“进行一项投票所需的立法者的最小数量”。在 ZooKeeper 中,它指的是“集群正常工作所需有效实例的最小数量”。
ZooKeeper 以 多数原则 来确定法定人数。这样可以防止“脑裂”(多个服务实例认为自己是群首)。在群首选举和写操作事务中都运用了多数原则。
多数原则中的法定人数(示例):
总数 | 法定人数 | 允许宕机的最大数量 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 2 |
7 | 4 | 3 |
可以发现,从高可用性方面而言,在多数原则下,部署偶数(2n)个实例还不如部署奇数(2n-1)个实例的性价比高。因为它们允许宕机的实例数量是相同的。
12.3.1 多数原则的变种
ZooKeeper 支持对服务实例进行分组,并对每个服务实例设置一个权重。这个设置会影响ZooKeeper的仲裁方式。它本质上还是遵循多数原则,所以我将这个配置策略称为“多数原则的变种”。
ZooKeeper 分组配置示例(部分):
group.1=1:2:3
group.2=4:5:6
group.3=7:8:9
根据上述配置:
- 法定人数来自 2个组。因为总共有 3个组,“多数”就是2。
- 每组内至少有 2个实例参与仲裁。因为未显式配置各实例权重,所以每个实例的权重都是 1,每组权重总和为3。
- 根据上述两条,可得出法定人数为 4。(2 x 2 = 4)
“分组 + 自定义权重” 模式的优势:
- 更少的法定人数。这有利于提高ZooKeeper集群的可用性。在上述示例中,如果按照普通的多数原则,总共9个实例,则需要5个实例才能组成法定人数,即最多允许4个实例宕机。现在只需4个,即最多允许5个实例宕机。
- 更灵活的服务等级设置。如,因为主数据中心复杂处理大多数用户的请求,所以可以为主数据中心的ZooKeeper实例赋予更高的权重,以便让其它数据中心出现故障时,主数据中心仍能正常运行,确保大多数用户正常使用。
12.4 Zab:ZooKeeper Atomic Broadcast Protocol
Zab 直译就是“ZooKeeper 原子广播协议”。这个协议用于确保事务的提交顺序,保证整个集群状态一致性。其大致流程是一个典型的两阶段提交工程。
12.5 日志 与 磁盘的使用
12.5.1 两类日志:事务、快照
- 事务:ZooKeeper 中的每个写请求都会产生一个事务。但是读请求不会。
- 读请求:在服务器本地处理(getData、getChildren、exists)(查)
- 写请求:转发给群首处理(create、delete、setData)(增、删、改)
- 快照:ZooKeeper 数据树的拷贝副本。它可以提高恢复数据的效率(后文会提到)。
日志文件名示例:
上图中,前4个文件是事务日志文件。它们的后缀是十六进制的整数,表示此事务日志文件中,第一个事务的 zxid。后3个文件是快照日志文件。它们的后缀也是十六进制的整数,表示此快照开始时的 zxid。
12.5.2 “模糊”快照
“模糊”的意思就是不准确,也就是说快照记录的数据并不能真实反馈制作快照时 ZooKeeper 数据树的真实内容。为什么会是“模糊”的呢?既然数据不准确,还有什么用呢?通过示例来说明:
在制作快照的过程中,ZooKeeper 还是正常对外提供服务。所以最终得到的快照数据(a=1,b=文),既不是开始制作快照时的版本(a=1,b=2),也不是快照完成时的版本(a=中,b=文)。
那么如何使用快照恢复数据呢?
首先是根据快照记录的数据恢复内容,然后再逐个重现快照之后的事务,这样就能最终得到准确的数据。如果没有快照,那么就得完整地从头播放所有事务,效率非常低。
12.5.3 优化事务日志配置
事务日志显然比快照日志更重要。而且写事务日志的效率,即数据落盘效率,直接影响到 ZooKeeper 处理写操作的效率。所以通常会采用以下两个策略来优化:分配独立的磁盘、关闭磁盘的写缓存。
分配独立的磁盘:将快照文件和其它操作系统文件都存放到其它磁盘,以降低对事务日志的影响。
关闭磁盘的写缓存
为了防止系统以外崩溃(如,断电)导致事务日志丢失,ZooKeeper 在写事务日志时会调用相关API,强制操作系统禁用缓存,直接将数据写入磁盘。(FileChannel.force() )
但是磁盘本身也是有缓存机制的。所以还需额外去关闭磁盘的缓存。不同操作系统上,关闭磁盘缓存的设置也不同。
云环境中接触不到磁盘,怎么办?那就得靠云环境自己的可靠性了。其实磁盘都会配有电容,以供在断电时将磁盘缓存中的数据持久化。企业级的磁盘配备的电容更大。
12.6 重配置
此处重配置专指 ZooKeeper 服务端实例调整。如,添加一个实例、删除一个实例等。
12.7 ZooKeeper 服务端配置
首先来看一个 ZooKeeper集群的配置示例(部分):
每个 ZooKeeper 实例都会有一个 zoo.cfg 配置文件,其中对各服务实例地址的配置都是相同的。也就是上图中 “server.0”、“server.1”、“server.2”。那么各实例如何知道自己是哪么个实例呢?还有另一个配置文件 myid:
每个 ZooKeeper 实例都有个 myid 配置文件。这个文件存放的是此实例的ID。上图内容“0”就表示,该实例的ID为0,即 zoo.cfg 配置中 server.0 就是它的配置。
12.8 客户端配置
ZooKeeper 客户端也需要有相关配置来确定使用哪个 ZooKeeper服务(集群)。这个配置类似于MySQL的连接串。不同的客户端有不同的格式要求。如,Curator 要求的格式就非常直白:
10.1.60.238:2181,10.1.60.239:2181,10.1.60.240:2181
有一些客户端会支持一些扩展功能,可以提供更复杂的配置形式。如:
zookeeper://10.1.60.238:2181?backup=10.1.60.239:2181,10.1.60.240:2181
它们的核心内容就是 ZooKeeper 服务端的 IP 和 端口号。
12.9 重配置案例
上图所示案例中,原 ZooKeeper 集群是由3个60网段的实例组成的。现在因为某些原因(如,使用性能更好的服务器),需要让另外3个61网段的服务实例替代原来的实例。
一种直观的解决方法就是停止原集群,迁移数据到新集群实例,并启动新集群。这个过程中,ZooKeeper 服务端和客户端都需要重启,以便新的配置生效。也就是说需要停服维护,对业务持续性非常不友好。
当然也有不停服更改配置的方案:
12.9.1 重配置——服务端改动
服务端主要有两种配置更改方式:手动配置、动态配置。
手动配置也成为“滚动重启”:
- 每次都只更改并重启一个服务实例;
- 每次变更信息尽量少;
- 每次变更后需等待集群完成信息同步,才能继续更新其它实例。
显然,手动配置也是比较麻烦的,也容易出错。
从 ZooKeeper 3.5.0 开始,ZooKeeper 支持“动态配置”,可以自动完成这个配置更改过程。新版本中的配置文件内容格式(策略)也发生了变化。
原来硬编码的服务实例配置改为“动态”模式:
可以使用 reconfig 命令来发起实例变更操作。ZooKeeper 服务端会自动完整整个变更。示例:
# 替换整个集群中的实例:
reconfig –members server.1=10.1.61.131:2555:3555;2181,server.2=10.1.61.132:2555:3555;2181…
# 移除ID为3的实例,并添加一个ID为5的新实例:
reconfig –remove 3 –add server.5=10.1.61.133:2555:3555;2181
12.9.2 重配置——客户端改动
客户端也需要更改对应的配置,也就是上文提到类似“MySQL连接串”的那些信息。
还是同一个关注点,业务持续性。
从 ZooKeeper 3.5.0 开始,ZooKeeper 服务端会将这些配置信息都发布到数据节点 /zookeeper/config 中。客户端只要监听此节点的数据变更,就可以进行相应的处理,切换到新服务实例。
这些监听处理方式对于之前已连接的客户端来说当然可以。那对于新加入的客户端,要怎么处理呢?很多架构中是将 ZooKeeper 服务端地址配置人为发布到某个配置中心的。客户端都是在启动时从配置中心获取 ZooKeeper 服务器地址。所以配置中心的配置内容也得改。当然,我们可以利用DNS来处理,即配置中心只提供主机名,不提供IP地址。但是这种方式要求服务端对客户端提供服务端端口号不能变,比如保持 2181。另外,还得考虑DNS解析缓存的问题。因为有缓存,只有在客户端第一次连接服务端时才会去真正解析IP地址。
总之,这是一个比较开放的问题。解决方案并不唯一。需要根据实际场景设计方案。对某些应用来说,直接重启客户端的综合成本反而是最低的。
12.10 更多高级特性
对于成熟规范的大型软件系统来说,信息安全与子系统精细化管理时非常必要的。认证与授权、配额管理、多租赁管理 等各种高级特性都是使用 ZooKeeper 时需要考虑的问题。此处的“认证与授权”与子系统使用独立账号访问 MySQL 非常相似。“配额管理”则类似于 MySQL 的的表空间设置。“多租赁管理”类似于使用不同的 MySQL schema。
小作坊没有这方面的沉淀,肯定考虑不到这些问题。但如果你想做一款合格的软件产品,就必须考虑到这些问题。是的,我用的词是“合格”。网络上大多数所谓的“产品”根本不能算“产品”。“互联网公司”几乎就是“垃圾”的别称。
各位可以参考相关官方文档了解更多内容。
13. 结语
整篇文章读下来,读者能记住的东西也许很少。但是我还是想强调部分知识:
会话连接与恢复——监听并处理连接状态变更事件:
- 连接断开时,应暂停受影响业务;
- 重连后,应重新获取服务端数据,执行必要的业务状态清理。
之所以强调这一点,是因为这是使用 ZooKeeper 必须要考虑的,它直接业务逻辑正确性。而且它反映了分布式系统架构中普遍的问题:网络是不可靠的,状态数据是会过期的。
孙子兵法:“未虑胜,先虑败。”
其实这些东西并不是什么“高大上”的知识。绝大多数人都处于“工程”的范畴,几乎一辈子不会遇到一个真正做“纯原理科研”的人。用过各种所谓的新技术并不值得说道。没用过也不能说明一个人的水平低。甚至不能说明这个人的技术前瞻性有问题。只要给他一个机会静下心来研究,上手非常容易。知识与技能真的仅仅是冰山最上层浅显的小小一部分。越看重这些表层信息的人,越“幼稚”,难堪大用。这也是为什么很多站在风口边缘的年轻程序员各种技术名词讲得头头是道,各种框架工具用得很溜,但在实际软件工程硬战中,其综合实力远远不如老将的原因。
为之,则难者亦易矣;不为,则易者亦难矣。