- 架构解密:从分布式到微服务(第2版)
- 吴治辉
- 3804字
- 2020-08-27 12:47:26
2.3 分布式系统的基石之ZooKeeper
由于ZooKeeper集群的实现采用了一致性算法,所以它成为一个非常可靠的、强一致性的、没有单点故障的分布式数据存储系统。但它的目标不是提供简单的数据存储功能,而是成为分布式集群中不可或缺的基础设施。
2.3.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.3.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端的图形化管理界面。