1. ZooKeeper概述
1.1 ZooKeeper介绍
-
ZooKeeper 是 Apache 软件基金会的一个项目,它确实提供了一种非常有用的服务,用于维护分布式系统中的配置信息、命名、提供分布式同步和提供组服务等。它的核心是原子广播和大约一致性模型,这使得它能够在分布式环境中提供一致性保证。
-
ZooKeeper 的数据模型类似于传统的文件系统,具有节点和层次结构,但它是为分布式环境特别设计的。每个节点称为 znode,可以存储数据和状态信息。客户端可以对 znode 执行创建、读取、更新和删除操作,并且 ZooKeeper 保证了这些操作的顺序性和一致性。
-
ZooKeeper 的设计目标是简单易用,它提供了一个客户端库,使得开发者可以很容易地在自己的应用程序中集成 ZooKeeper。此外,它还提供了一些高级抽象,如 Watches(监视器),它们允许客户端在数据变化时得到通知。
-
ZooKeeper 通过其一致性模型和事务机制来减少这些问题的发生。例如,它的事务操作是原子的,这意味着要么全部成功,要么全部失败,这有助于避免争用条件。同时,ZooKeeper 的设计也考虑到了避免死锁,例如通过使用序列号来确保操作的全局顺序。
1.2 ZooKeeper设计目标
-
ZooKeeper 是一个设计用来简化分布式系统中的协调问题的系统。
-
共享的分层命名空间:ZooKeeper 通过一个分层的命名空间(类似于文件系统的目录结构)来组织数据。这个命名空间中的每个节点称为 znode,可以包含数据、子节点以及与节点相关的元数据。
-
数据存储:虽然 ZooKeeper 的数据是保存在内存中的,以实现高性能,但它也周期性地将数据写入到磁盘上,以保证在系统崩溃后能够恢复状态。这种机制称为快照(Snapshot)和事务日志(Transaction Log)。
-
高性能和低延迟:由于数据存储在内存中,ZooKeeper 能够提供高吞吐量和低延迟的操作响应,这对于需要快速协调的分布式应用来说非常重要。
-
高可用性:ZooKeeper 通过集群的形式运行,集群中的服务器可以是领导者(Leader)或追随者(Follower)。如果领导者发生故障,追随者中的一个将被选举为新的领导者,从而保证了服务的连续性和高可用性。
-
严格的顺序访问:ZooKeeper 保证了所有客户端对 znode 的操作都是全局有序的。这意味着即使在分布式环境中,操作也会按照它们被接收的顺序进行处理。
-
复制:ZooKeeper 的复制是指其集群中的多个服务器持有数据的副本。这种复制机制不仅提高了系统的可用性,也增强了容错能力。集群中的每个服务器都参与到数据的复制过程中,确保了数据的一致性。
-
选举机制:ZooKeeper 使用一种称为 ZAB(ZooKeeper Atomic Broadcast)协议的一致性协议来处理领导者选举和状态同步,确保了在领导者变更时集群状态的一致性。
-
-
服务器间的了解:ZooKeeper 集群中的服务器通过内部通信机制相互了解。它们通过心跳检测和领导者选举协议来维护彼此的状态和协调操作。
-
客户端连接:客户端与 ZooKeeper 服务器的单个实例建立 TCP 连接。这个连接用于发送请求、接收响应、处理监视事件以及发送心跳以保持连接的活跃状态。
-
容错:如果客户端与当前连接的服务器的 TCP 连接中断,客户端库会自动尝试连接到集群中的其他服务器,以确保操作的连续性。
-
事务编号:ZooKeeper 为每个事务分配一个递增的事务 ID(zxid),这个 ID 反映了所有事务的全局顺序。这使得客户端可以根据事务的顺序来实现复杂的同步原语和协调逻辑。
-
性能特点:ZooKeeper 在"读取主导"的工作负载下表现出色,特别是在读操作远多于写操作的场景中。这种场景下,ZooKeeper 能够提供非常快速的响应时间。
-
大规模部署:ZooKeeper 能够支持在数千台机器上运行的应用程序,这得益于其高效的数据复制和同步机制,以及优化的网络通信。
-
读写比率:在典型的 ZooKeeper 应用场景中,读操作与写操作的比例可能高达 10:1,这反映了许多分布式协调任务更倾向于读取状态信息而非频繁更改状态。
1.3 数据模型和分层命名空间
ZooKeeper 的数据模型和分层命名空间是其核心特性之一,下面是对这些概念的进一步解释:
- 分层命名空间:ZooKeeper 的命名空间是一个层次结构,类似于文件系统的目录树。这种结构使得数据组织有序,易于管理和访问。
-
路径元素:在 ZooKeeper 中,每个节点的路径由一系列路径元素组成,这些元素通过斜杠(/)分隔。路径元素可以包含字母、数字、下划线、连字符以及点,但不能包含斜杠。
-
根节点:ZooKeeper 的命名空间从根节点开始,路径为 "/"。根节点下可以包含多个子节点,这些子节点可以进一步包含更多的子节点,形成一个树状结构。
-
唯一标识:每个 znode(ZooKeeper 中的节点)在命名空间中都有一个唯一的路径标识。这意味着即使两个 znode 具有相同的名称,它们的路径也必须是唯一的。
-
顺序性:ZooKeeper 允许在节点名称中包含一个序列号,这样创建的节点将具有唯一的路径。例如,如果创建了一个名为 "/app" 的节点,随后创建的另一个节点可以命名为 "/app00000001",其中序列号由 ZooKeeper 自动分配。
-
持久性和临时性:ZooKeeper 中的 znode 可以是持久的或临时的。持久节点在 ZooKeeper 服务重启后仍然存在,而临时节点在创建它的会话结束时会被删除。
-
数据存储:每个 znode 可以存储数据,客户端可以读取或写入这些数据。此外,znode 还可以存储元数据,如节点的创建时间、最后更新时间、ACL(访问控制列表)等。
-
监视器(Watchers):客户端可以在特定的 znode 上设置监视器。当 znode 发生变化(如数据更新、子节点添加或删除等)时,ZooKeeper 会通知设置了监视器的客户端。
-
API 操作:ZooKeeper 提供了一系列 API 操作,允许客户端创建、读取、更新、删除 znode,以及设置和处理监视器。
1.4 节点和临时节点
-
与标准文件系统不同,ZooKeeper 命名空间中的每个节点都可以具有与其关联的数据以及子节点。这就像有一个文件系统,允许文件也是一个目录。(ZooKeeper 设计用于存储协调数据:状态信息、配置、位置信息等,因此每个节点存储的数据通常很小,在字节到千字节的范围内。我们使用术语 znode 来明确表示我们谈论的是 ZooKeeper 数据节点。
-
Znodes 维护一个统计信息结构,其中包括数据更改、ACL 更改和时间戳的版本号,以允许缓存验证和协调更新。每次 znode 的数据发生变化时,版本号都会增加。例如,每当客户端检索数据时,它也会接收数据的版本。
-
znode 的特性:在 ZooKeeper 中,每个 znode 都可以存储数据并拥有子节点,这与文件系统中的文件和目录类似,但 znode 可以同时具备这两种特性。
-
数据大小:ZooKeeper 节点中存储的数据通常很小,因为它们主要用于存储协调数据,如状态信息、配置和位置信息等。数据量通常在字节到千字节范围内。
-
版本号:znode 维护一个统计信息结构,包括数据版本号和 ACL 版本号,以及时间戳。每次数据或 ACL 发生变化时,相应的版本号会增加,这有助于客户端进行缓存验证和协调更新。
-
原子读写操作:ZooKeeper 保证对 znode 数据的读取和写入操作是原子的。这意味着读取操作会获取到 znode 当前关联的所有数据,而写入操作会替换掉 znode 的所有数据。
-
访问控制列表(ACL):每个 znode 都有一个 ACL,定义了哪些用户或用户组可以对 znode 执行哪些操作(如读取、写入、创建子节点等)。
-
临时节点:ZooKeeper 支持创建临时节点,这些节点的生命周期与创建它们的会话相关联。如果会话由于网络问题或其他原因而结束,那么所有临时节点都会被自动删除。
-
会话管理:客户端与 ZooKeeper 服务器之间的会话通过心跳来保持活跃。如果心跳丢失,会话将超时并结束,导致所有临时节点被删除。
-
顺序性:ZooKeeper 还为每个 znode 提供了顺序性保证,即在节点名称后附加一个自增的序列号。这确保了即使在大量并发请求的情况下,znode 的创建也是全局有序的。
-
监视器(Watchers):客户端可以在 znode 上设置监视器来监听数据变化或子节点的添加和删除事件。当这些事件发生时,ZooKeeper 会通知设置了监视器的客户端。
-
持久性和临时性:除了临时节点,ZooKeeper 还支持持久节点,这些节点即使在创建它们的会话结束后仍然存在。
-
-
存储在命名空间中每个 znode 的数据都是以原子方式读取和写入的。读取操作获取与 znode 关联的所有数据字节,写入操作将替换所有数据。每个节点都有一个访问控制列表 (ACL),用于限制谁可以执行哪些操作。
-
ZooKeeper 也有临时节点的概念。只要创建 znode 的会话处于活动状态,这些 znodes 就存在。当会话结束时,znode 将被删除。
1.5 有条件的更新和监视
-
ZooKeeper 支持手表的概念。客户端可以在 znode 上设置监视。当 znode 发生变化时,将触发并删除监视。当触发监视时,客户端会收到一个数据包,指出 znode 已更改。如果客户端与其中一个 ZooKeeper 服务器之间的连接断开,客户端将收到本地通知。
-
监视器(Watchers):ZooKeeper 允许客户端在特定的 znode 上注册监视器。监视器是一种回调机制,当 znode 发生变化时,ZooKeeper 会通知设置了监视器的客户端。
-
触发和删除:当监视的 znode 发生变化(如数据更新、子节点的添加或删除)时,监视器会被触发。触发后,监视器会被自动删除,除非是永久的递归监视。
-
客户端通知:当监视器被触发时,客户端会收到一个通知事件,告知它们 znode 发生了变化。这使得客户端可以根据这些变化做出响应。
-
连接断开:如果客户端与 ZooKeeper 服务器之间的连接断开,客户端会收到一个 SESSION_EXPIRED 事件,告知会话已过期。这通常是由于网络问题或服务器故障导致的。
-
永久递归监视:在 ZooKeeper 3.6.0 版本中引入了永久递归监视的新特性。这种监视器在触发时不会被删除,并且会递归地监视已注册的 znode 及其所有子 znode 上的更改。
-
递归监视的优势:递归监视允许客户端一次性注册对整个 znode 树的监视,而不需要为每个子节点单独设置监视器。这简化了客户端的逻辑,并减少了需要管理的监视器数量。
-
监视器的限制:需要注意的是,监视器只能检测到 znode 的存在性变化和子节点列表的变化,而不能检测到 znode 数据内容的变化。如果需要监视数据内容的变化,客户端需要在接收到通知后主动查询 znode 的数据。
-
使用场景:监视器在分布式协调中非常有用,例如在分布式锁、配置管理、服务发现等场景中,客户端可以依赖监视器来响应集群状态的变化。
-
1.6 ZooKeeper提供的保证
ZooKeeper 非常快速且非常简单。但是,由于其目标是成为构建更复杂服务(例如同步)的基础,因此它提供了一套保证。
-
顺序一致性:来自客户端的更新将按照发送的顺序应用。
-
原子性:更新要么成功,要么失败。没有部分结果。
-
单一系统映像:无论客户端连接到哪个服务器,它都将看到服务的相同视图。也就是说,即使客户端故障转移到具有相同会话的不同服务器,客户端也永远不会看到系统的旧视图。
-
可靠性:应用更新后,它将从那时起一直存在,直到客户端覆盖更新。
-
时效性:保证客户对系统的视图在一定时间范围内是最新的。
1.7 ZooKeeper简单的API
ZooKeeper 的 API 设计确实注重简洁性,以便于开发者使用和集成。以下是您提到的操作的简要说明:
-
创建(Create):允许客户端在 ZooKeeper 的层次命名空间中的指定位置创建一个新的 znode。创建操作可以指定节点为持久的或临时的,以及是否允许序列化(即自动添加一个单调递增的数字后缀)。
-
删除(Delete):客户端可以使用此操作删除一个已存在的 znode。删除操作需要指定版本号,以确保删除的是期望的节点状态,防止删除操作与并发更新冲突。
-
存在测试(Exists):此操作用于检查指定路径的 znode 是否存在。如果存在,还可以返回节点的状态信息,但不返回节点的数据内容。
-
获取数据(GetData):从指定的 znode 读取数据。此操作同时返回节点的状态信息,包括数据的版本号。
-
设置数据(SetData):向指定的 znode 写入数据。可以指定数据的版本号,以确保只有当节点数据未被其他客户端更新时才写入。
-
获取子节点列表(GetChildren):检索指定 znode 的所有子节点的列表。此操作也可以注册子节点变更的监视器。
-
同步(Sync):这是一个特殊的操作,用于确保客户端对 znode 的视图是最新的。这通常用于确保在进行读取操作之前,客户端拥有的数据是最新的。
1.8 Zookeeper组件和服务实现
- ZooKeeper 组件显示 ZooKeeper 服务的高级组件。除请求处理器外,组成 ZooKeeper 服务的每个服务器都复制其自己的每个组件副本。
-
ZooKeeper 的复制数据库和客户端请求处理机制是其核心功能的基础。
-
内存数据库:每个 ZooKeeper 服务器维护一个内存中的数据库,该数据库包含了整个 Znode 树。内存数据库提供了快速的数据访问能力。
-
数据持久化:为了保证数据的持久性,所有的更新操作在应用到内存数据库之前,都会先序列化并记录到磁盘上的事务日志中。这样即使服务器崩溃,也能从日志中恢复数据。
-
客户端请求处理:
-
客户端连接到任意一个 ZooKeeper 服务器并提交请求。
-
读取请求通常直接由服务器的本地数据库副本提供服务,因为这些操作是幂等的。
-
写入请求需要经过一致性协议处理,以确保所有副本的数据一致性。
-
-
领导者和追随者:
-
写入请求首先发送到领导者服务器。
-
追随者服务器接收领导者的提案,并参与到一致性协议中,以确保所有服务器对写入操作达成共识。
-
-
消息传递层:负责处理领导者的选举和故障恢复。如果领导者失败,消息传递层会选举新的领导者,并确保所有追随者与新领导者同步。
-
原子消息传递协议:ZooKeeper 使用的自定义协议保证了消息的原子性,这意味着消息要么完全传递,要么完全不传递,没有中间状态。这有助于保持系统状态的一致性。
-
事务处理:当领导者服务器收到写入请求时,它会计算出应用写入后系统的状态,并将这个状态转换为一个事务。这个事务会被记录到事务日志中,并在确认后应用到内存数据库。
-
数据同步:追随者服务器通过接收领导者的消息来同步数据。这确保了所有服务器上的数据库副本保持最新和一致。
-
客户端视角:客户端通常不需要关心后端的复制和一致性协议。它们通过与单个服务器交互来提交请求,并依赖 ZooKeeper 内部机制来保证操作的一致性和顺序性。
-
1.9 ZooKeeper使用
使用 ZooKeeper 的简单编程接口,开发者可以构建和实现各种复杂的分布式系统协调原语和模式。
-
同步原语(Synchronization Primitives):
-
事件等待:利用 ZooKeeper 的监视器(Watchers)机制,进程可以在某些事件发生时得到通知,实现等待事件的同步。
-
屏障(Barriers):所有进程在继续执行前必须等待其他进程到达同一状态,可以使用有序节点创建实现。
-
锁(Locks):包括排他锁和共享锁。ZooKeeper 可以用来实现分布式锁,例如利用临时顺序节点来创建一个可重入的分布式锁。
-
-
组成员身份(Group Membership):
-
ZooKeeper 可以用来维护服务或任务的组成员信息,包括成员的加入和退出。
-
通过使用 ZooKeeper 的 ephemeral nodes,可以确保如果成员崩溃,其对应的节点会被自动删除,从而实现成员的动态管理。
-
-
所有权(Ownership):
- 在分布式系统中,确定资源的所有权是一个常见问题。ZooKeeper 可以用来选举主节点或领导者,确保对共享资源的协调访问。
-
配置管理(Configuration Management):
- ZooKeeper 可以用来存储和管理集群的配置信息,这些信息可以被集群中的所有节点访问和监视。
-
服务发现(Service Discovery):
- 客户端可以在 ZooKeeper 上注册服务,并在启动时查找可用的服务实例。其他客户端可以使用 ZooKeeper 来发现并连接到这些服务。
-
队列管理(Queue Management):
- ZooKeeper 可以用来实现分布式队列,包括先进先出(FIFO)队列和有界队列。
-
分布式计数器和分布式屏障(Distributed Counters and Barriers):
-
使用 ZooKeeper 的序列号特性,可以轻松实现跨多个节点的计数器。
-
通过创建临时节点,可以实现所有节点必须等待的屏障。
-
-
领导者选举(Leader Election):
- 在多个进程或服务中,可能需要选举出一个领导者来协调操作。ZooKeeper 的顺序节点可以用于实现这一模式。
-
分布式原子广播(Distributed Atomic Broadcast):
- ZooKeeper 可以用来实现一个原子广播系统,确保消息以相同的顺序被所有接收者接收。
1.10 ZooKeeper性能
- ZooKeeper 被设计为高性能。但事实如此吗?雅虎研究部门的 ZooKeeper 开发团队的结果表明确实如此。在读多于写的应用程序中,它的性能尤其高,因为写涉及同步所有服务器的状态。(读多于写通常是协调服务的情况。)
1.11 ZooKeeper可靠性
为了展示随着故障注入系统行为随时间推移的变化,我们运行了一个由 7 台机器组成的 ZooKeeper 服务。我们运行了与之前相同的饱和基准,但这次我们将写入百分比保持在恒定的 30%,这是我们预期工作负载的保守比率。
- 从该图表中可以得出一些重要的观察结果。首先,如果跟随者快速故障并恢复,则 ZooKeeper 能够在故障情况下维持高吞吐量。但可能更重要的是,领导者选举算法允许系统足够快地恢复,以防止吞吐量大幅下降。根据我们的观察,ZooKeeper 花费不到 200 毫秒来选举新领导者。第三,随着跟随者恢复,ZooKeeper 能够在他们开始处理请求后再次提高吞吐量。