由于 ZooKeeper 集群的实现采用了一致性算法,所以它成为一个非常可靠的、强一致性的、没有单点故障的分布式数据存储系统。但它的目标不是提供简单的数据存储功能,而是成为分布式集群中不可或缺的基础设施。
1.ZooKeeper 的原理与功能
前面我们提到,绝大多数分布式系统都采用了中心化的设计理念,一些新的分布式系统的设计表面上看似乎是无中心的,但实际上隐含了中心化的内核,在这类架构中往往有如下普适性的共性需求。
(1)提供集群的集中化的配置管理功能。该看起来简单,但实际上也有复杂之处,比如不重启程序而让新的配置参数即时生效,这在分布式集群下就没那么简单了,如果我们认真思考或者开发过配置中心,那么应该对这个需求的实现难度有深刻的理解。
(2)需要提供简单可靠的集群节点动态发现机制。该需求是构建一个具备动态扩展能力的分布式集群的重要基础,通过实现一个便于使用的集群节点动态发现的服务,我们可以很容易开发先进的分布式集群:在一个节点上线后能准确得到集群中其他节点的信息并进行通信,而在某个节点宕机后,其他节点也能立即得到通知,从而实现复杂的故障恢复功能。这个需求的实现难度更大,因为涉及多节点的网络通信与心跳检测等复杂编程问题。
(3)需要实现简单可靠的节点 Leader 选举机制。该需求用来解决中心化架构集群中领导选举的问题。
(4)需要提供分布式锁。该需求对于很多分布式系统来说也是必不可少的,为了不破坏集群中的共享数据,程序必须先获得数据锁,才能进行后面的更新操作。
ZooKeeper 通过巧妙设计一个简单的目录树结构的数据模型和一些基础 API 接口,实现了上述看似毫无关联的需求,而且能满足很多场景和需求,比如简单的实时消息队列。如下所示是 ZooKeeper 基于目录树的数据结构模型示意图。
ZooKeeper 的数据结构可以被认为是模仿 UNIX 文件系统而设计的,其整体可以被看作一棵目录树,其中的每个节点都可作为一个 ZNode,每个 ZNode 都可以通过其路径(Path)唯一标识,比如上图中第 3 层的第 1 个 ZNode,它的路径是/app1/p_1。每个 ZNode 都可以绑定一个二进制存储数据(Data)来存储少量数据,默认最大为 1MB。我们通常不建议在 ZNode 上存储大量的数据,这是因为存在数据多份复制的问题,当数据量比较大时,数据操作的性能降低,带宽压力也比较大。
ZooKeeper 中的 ZNode 有一个 ACL 访问权限列表,用来决定当前操作 API 的用户是否有权限操作此节点,这对于多个系统使用同一套 ZooKeeper 或者不同的 ZNode 树被不同的子系统使用来说,提供了必要的安全保障机制。ZooKeeper 除了提供了针对 ZNode 的标准增删改查的 API 接口,还提供了监听 ZNode 变化的实时通知接口——Watch 接口,应用可以选择任意 ZNode 进行监听,如果被监听的 ZNode 或者其 Child 发生变化,则应用可以实时收到通知,这样很多场景和需求就都能通过 ZooKeeper 实现了。
此外,ZNode 是有生命周期的,这取决于节点的类型,节点可以分为如下几类。
-
持久节点(PERSISTENT):节点在创建后就一直存在,直到有删除操作来主动删除这个节点。
-
临时节点(EPHEMERAL):临时节点的生命周期和创建这个节点的客户端会话绑定,也就是说,如果客户端会话失效(客户端宕机或下线),这个节点就被自动删除。
-
时序节点(SEQUENTIAL):在创建子节点时可以设置这个属性,这样在创建节点的过程中,ZooKeeper 就会自动为给定的节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。
-
临时性时序节点(EPHEMERAL_SEQUENTIAL):同时具备临时节点与时序节点的特性,主要用于分布式锁的实现。
从上面的分析说明来看,持久节点主要用于持久化保存的数据,最典型的场景就是集群的配置信息,如果结合 Watch 特性,则可以实现集群的配置实时生效的高级特性。典型的设计思路如下图所示。
ZooKeeper 的临时节点比较有趣,在创建这个临时节点的应用与 ZooKeeper 之间的会话过期后就会被 ZooKeeper 自动删除。这个特性是实现很多功能的关键,比如我们做集群感知,应用在启动时会将自己的 IP 地址作为临时节点创建在某个节点(如/Cluster)下,当应用因为某些原因如断网或者宕机,使得它与 ZooKeeper 的会话过期时,这个临时节点就被删除了,这样我们就可以通过这个特性来感知服务的集群有哪些机器可用了。
此外,临时节点也可以实现更为复杂的动态服务发现和服务路由功能,通常的做法是:分布式集群中部署在不同服务器上的服务进程都连接到同一个 ZooKeeper 集群上,并且在某个指定的路径下创建各自对应的临时节点,例如/services/X 对应 X 节点的服务进程,/services/Y 对应 Y 节点的服务进程,所有要访问这些服务的客户端则监听(Watch)/services 目录。当有新的节点如 Z 加入集群中时,ZooKeeper 会实时地把这一事件通知(Notify)到所有客户端,客户端就可以把这个新的服务地址加入自己的服务路由转发表中了。而当某个节点宕机并从 ZooKeeper 中脱离时,客户端也会及时收到通知,客户端就可以从服务路由转发表中删除此服务路由,从而实现全自动的透明的动态服务发现和服务路由功能了。如下所示就是上述做法的一个简单原理示意图。
下面说说 ZooKeeper 的时序类型的节点(时序节点与临时性时序节点),这种类型的节点在创建时,每个节点名都会被自动追加一个递增的序号,例如/services/server1、/services/server2、/services/server3 等,这就类似于数据库的自增长主键,每个 ZNode 都有唯一编号,而且不会冲突。ZooKeeper 时序类型的节点可以实现简单的 Master(Leader)节点选举机制,即我们把一组 Service 实例对应的进程都注册为临时性时序节点类型的 ZNode,每次选举 Master 节点时都选择编号最小的那个 ZNode 作为下一任 Master 节点,而当这个 Master 节点宕机时相应的 ZNode 会消失,新的服务器列表就被推送到客户端,继续选择下一任 Master 节点,这样就做到了动态 Master 节点选举。另外,著名的 ZooKeeper 客户端工具——Apache Curator(后简称 Curator)也采用临时性时序节点类型的 ZNode 实现了一个跨 JVM 的分布式锁——InterProcessMutex。
最后,我们谈谈分布式集群一致性场景中的命令序列是如何对应到 ZooKeeper 上的。之前说的命令序列其实就是对 ZNode 的一系列操作,例如增删改查,ZooKeeper 会保证任意命令序列在集群中的每个 ZooKeeper 实例上执行后的最终结果都是一致的。此外,如果 ZooKeeper 集群的 Leader 宕机,则会重新自动选择下一任 Leader,而 ZooKeeper 集群中的每个节点都知道谁是当前 Leader,因此,程序在通过 ZooKeeper 的客户端 API 连接 ZooKeeper 集群时,只要把集群中所有节点的地址都作为连接参数传递过去即可,无须弄清楚谁是当前 Leader,这要比很多传统分布式系统使用起来简单很多。
2.ZooKeeper 的应用场景案例分析
ZooKeeper 主要应用于以下场景中。
(1)实现配置管理(配置中心)。
(2)服务注册中心。
(3)集群通信与控制子系统。
基本上每个使用 ZooKeeper 的集群,都会同时采用 ZooKeeper 存储集群的配置参数。可以说,实现配置管理(配置中心)是 ZooKeeper 最广泛、最基础的使用场景。
服务注册中心是 ZooKeeper 最「重量级」的需求场景,ZooKeeper 是这里的关键组件,同时最能体现其复杂能力,这个场景也是所有「以服务为中心」的分布式系统的核心设计之一。如下所示分别是来自 Web Services 技术鼎盛时期由 IBM 等巨头主导的全球服务注册中心(UDDI Registry)的原理架构图和某个互联网公司采用 ZooKeeper 实现分布式服务注册与服务发现的原理架构图。
如上图所示,在此架构中有三类角色:服务提供者、服务注册中心和服务消费者。
首先,服务提供者作为服务的提供方,将自身的服务信息注册到服务注册中心。通常服务的注册信息包含如下内容。
-
服务的类型。
-
隶属于哪个系统。
-
服务的 IP、端口。
-
服务的请求 URL。
-
服务的权重。
其次,服务注册中心主要提供所有服务注册信息的中心存储,同时负责将服务注册信息的更新通知实时推送给服务消费者(主要通过 ZooKeeper 的 Watch 机制来实现)。
最后,服务消费者只在自己初始化及服务变更时依赖服务注册中心,而在整个服务调用过程中与服务提供方直接通信,不依赖于任何第三方服务,包括服务注册中心。服务消费者的主要职责如下。
-
服务消费者在启动时从服务注册中心获取需要的服务注册信息。
-
将服务注册信息缓存在本地,作为服务路由的基础信息。
-
监听服务注册信息的变更,例如在接收到服务注册中心的服务变更通知时,在本地缓存中更新服务的注册信息。
-
根据本地缓存中的服务注册信息构建服务调用请求,并根据负载均衡策略(随机负载均衡、Round-Robin 负载均衡等)转发请求。
-
对服务提供方的存活进行检测,如果出现服务不可用的服务提供方,则将其从本地缓存中删除。
如下所示是来自某个系统的 RPC 原理架构图,其中也采用了 ZooKeeper 来实现服务的注册中心功能,其实现机制和主要逻辑基本上和上述案例大同小异。
Kubernetes 也采用了 Etcd 作为服务注册中心的核心组件,从而构建出一个很先进的微服务平台,可见 ZooKeeper 这种基础设施对于分布式系统架构的重要性。
ZooKeeper 的第 3 个重要业务场景是实现整个集群的通信与控制子系统,大多数系统都需要有命令行及 Web 方式的管理命令,这些管理命令通常实现了以下管理和控制功能。
-
强制下线某个集群成员。
-
修改配置参数并且生效。
-
收集集群中各个节点的状态数据并汇总展示。
-
集群停止或暂停服务。
下面是用 ZooKeeper 设计实现的一个集群的控制子系统的原理架构图。
在 ZooKeeper 里规划了一个用于存放控制命令和应答的 ZNode 路径(如上图中的/Comands),集群中的所有节点在启动后都监听(Watch)此路径,命令行程序(CLI)发给集群节点的命令及参数被包装成一个 ZNode 节点(如上图中的 ReloadConfig),写入/Comands 路径下,同时在 ReloadConfig 上监听事件。紧接着集群中的所有节点都通过/Comands 上的 Watch 事件收到此命令,然后开始执行 ReloadConfig 命令对应的逻辑,在某个节点执行完成后就在 ReloadConfig 路径下新建一个 ZNode 节点(如 node1result)作为应答。由于 CLI 之前在 ReloadConfig 上监听,所以很快就被通知此命令已经有节点执行完成,CLI 就可以实时输出结果到屏幕上,在所有节点的应答都返回后(或者等待超时),命令行结束。
上述采用 ZooKeeper 的集群控制子系统实现简单且无须复杂的网络编程即可完成任意复杂的集群控制命令,命令集也很容易扩展,同一套命令集既可以用于命令行控制,也可以用于 Web 端的图形化管理界面。
参考:从分布式到微服务