From 9c7ebb86f15786d329b77df4153f2cdea01dea0b Mon Sep 17 00:00:00 2001 From: YangKian <45479280+YangKian@users.noreply.github.com> Date: Fri, 16 Apr 2021 23:28:04 +0800 Subject: [PATCH] update translation (#6) * update translation * update --- README.md | 108 +++++++++++++++++++++++++++--------------------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index d83421f..f08255f 100644 --- a/README.md +++ b/README.md @@ -8,60 +8,60 @@ ZooKeeper 接口使实现高性能服务成为可能。除了 wait-free 的属 ## 1. Introduction -大规模的分布式应用需要不同形式的协调。配置是最基础的形式之一。在配置最简单的形式,系统进程中,配置是一个系统进程的的可操作的参数列表,更复杂的系统有动态的配置参数。Group membership 和领导选举也是分布式系统的共有服务:通常,进程需要知道哪些别的进程状态是 alive 的和这些进程负责什么。Locks 建立了一个强大的同步原语,实现对关键资源的互斥访问。 +大规模的分布式应用需要不同形式的协调。配置是最基础的形式之一。配置最简单的形式仅仅是作为系统进程运行参数的列表,而更复杂的系统有动态的配置参数。Group membership 和领导选举在分布式系统中也很常见:通常,进程需要知道哪些其他进程是可用的(alive)以及这些进程负责什么。Locks 建立了一个强大的同步原语,实现对关键资源的互斥访问。 -一种协调的方式是,为不同的需求开发不同的服务。例如,Amazon Simple Queue Service \[3\] 专注于队列服务。其他服务也因为需要用于特定的用处而开发,比如领导选举和配置。实现了更强原语的服务可以被用于实现没有那么强大原语的服务。例如,Chubby 是一个有很强同步保证的锁服务。锁可以用来实现领导选举,组成员等服务。 +一种协调的方式是,为不同的需求开发不同的服务。例如,Amazon Simple Queue Service \[3\] 专注于队列服务。另外一些服务专门用于领导选举和配置。实现了更强原语的服务可以被用于实现没有那么强大原语的服务。例如,Chubby 是一个有很强同步保证的锁服务。锁可以用来实现领导选举,组成员等服务。 当设计我们的协调服务的时候,我们抛弃了在服务端侧实现特定的原语,作为替代,我们选择暴露一个能够使应用开发者实现他们自己同步原语的 API。这样的决定让我们实现一个不需要改变 ZooKeeper 即可实现不同服务的*协调内核*。这种方法允许用户实现多种形式的协调,以适应应用程序的需求,而不是将开发人员限制在一组固定的原语上。 -当设计 ZooKeeper 的 API 的时候,我们抛弃了阻塞原语,例如 Locks。阻塞原语对于一个协调服务会引起其它问题,速度较慢或者故障的客户端会对速度较快的客户端产生负面的影响。如果处理请求取决于客户端的响应和其他客户端的失败检测,则服务本身的实现将变得更加复杂。因此,我们的系统 ZooKeeper 实现了一个API,该 API 可以按文件系统的层次结构来组织简单的 wait-free 数据对象。 实际上,ZooKeeper API类似于任何其他文件系统,并且仅从 API 签名来看,ZooKeeper 很像没有锁定(`lock`)方法,打开(`open`)和关闭(`close`)方法的 Chubby。 但是,实现 wait-free 数据对象使 ZooKeeper 与基于锁之类的阻塞原语的系统明显不同。 +当设计 ZooKeeper 的 API 的时候,我们抛弃了阻塞原语,例如 Locks。阻塞原语对于一个协调服务会引起其它问题,速度较慢或者故障的客户端会对速度较快的客户端产生负面的影响。如果请求的处理需要依赖其它客户端的响应和故障检测,则服务本身的实现将会变得更加复杂。因此,我们的系统 ZooKeeper 实现了一个API,该 API 可以按文件系统的层次结构来组织简单的 wait-free 数据对象。 实际上,ZooKeeper API类似于任何其他文件系统,并且仅从 API 签名来看,ZooKeeper 很像没有锁定(`lock`)方法,打开(`open`)和关闭(`close`)方法的 Chubby。 但是,实现 wait-free 数据对象使 ZooKeeper 与基于锁之类的阻塞原语的系统明显不同。 -尽管 wait-free 对于性能和容错性很重要,但不足以进行协调。我们还必须提供操作的顺序保证。特殊的,我们发现,对客户端所有操作提供 FIFO 语义与提供 *linearizable writes* 可以高效的实现服务,并且足以实现应用程序感兴趣的协调原语。实际上,对于任意数量使用 API 的进程,都可以实现一致性,根据Herlihy给出的层次结构,ZooKeeper 实现了全局的对象。 +尽管 wait-free 对于性能和容错性很重要,但不足以进行协调。我们还必须提供操作的顺序保证。特别是,我们发现,对客户端所有操作提供 FIFO 语义与提供 *linearizable writes* 可以高效的实现服务,并且足以实现应用程序感兴趣的协调原语。实际上,对于任意数量使用 API 的进程,都可以实现一致性,根据Herlihy给出的层次结构,ZooKeeper 实现了全局的对象。 -ZooKeeper 使用副本,来保证服务的高可用和性能。它的高性能使包含大量进程的应用程序可以使用这种协调内核来管理协调的各个方面。我们能够使用一个简单的流水线架构,让我们能够处理成百上千的请求,同时仍然保持低延迟。这样的流水线很自然地可以保证对于单个客户端所有操作执行的顺序性。客户端的 FIFO 顺序使得客户端可以异步提交操作请求。使用异步操作,客户端一次可以执行多个未完成的操作。这个功能是很实用的,例如,当新客户端成为领导者并且必须操纵元数据并相应地对其进行更新时。如果不可能进行多个未完成的操作,则初始化时间可以是几秒左右,而不是亚秒级。 +ZooKeeper 使用副本来保证服务的高可用和性能。它的高性能使包含大量进程的应用程序可以使用这种协调内核来管理协调的各个方面。我们能够使用一个简单的流水线架构,让我们在处理成百上千个请求的同时仍然保持低延迟。这样的流水线很自然地可以保证单个客户端按照 FIFO 的顺序执行操作。客户端的 FIFO 顺序使得客户端可以异步提交操作请求。使用异步操作,客户端一次可以执行多个未完成的操作。这个功能是很实用的,例如,当新客户端成为领导者时,它需要对相应的元数据进行修改和更新。如果不能并行的执行多个未完成的操作,则初始化时间将会是秒级而不是亚秒级。 -为了保证更新操作满足 linearizability,我们实现了一个基于 leader 的原子广播协议 Zab。同时,一个典型的 ZooKeeper 应用中,在支配地位的负载是读操作,所以需要保证读吞吐量的扩展性。在ZooKeeper中,服务器在本地处理读操作,并不需要使用 Zab 来广播。 +为了保证更新操作满足 linearizability,我们实现了一个基于 leader 的原子广播协议 Zab。一个 ZooKeeper 应用的典型工作负载来自读操作,所以需要保证读吞吐量的扩展性。在ZooKeeper中,服务器在本地处理读操作,并不需要使用 Zab 来广播。 -在客户端侧缓存数据是提升读性能的重要技术。例如,对于一个进程,缓存现有 Leader 的 id,而不是每次需要 leader 时都探测 leader 是很有效的。ZooKeeper 并不直接操作缓存,而是使用一种 watch 机制。有了 watch 机制,一个客户端可以 watch 一个给定对象的更新请求,并接收到更新该对象的请求。(作为对比)Chubby 直接管理客户端的 cache,它会阻塞更新,以使更新部分的客户端的缓存全部失效。在这样的设计下,如果任何客户端响应慢或者出现错误,更新会变得很慢。Chubby 使用 lease 机制防止一个慢或者宕机的客户端阻塞系统。但 leases 只能约束慢客户端或者宕机客户端的影响,ZooKeeper 的 watches 可以完全避免这个问题。 +在客户端侧缓存数据是提升读性能的重要技术。例如,对于一个进程,缓存现有 Leader 的 id,而不是每次需要时都探测 ZooKeeper。ZooKeeper 并不直接操作缓存,而是使用一种 watch 机制。有了 watch 机制,一个客户端可以 watch 一个给定的对象,并在该对象更新时收到提醒。(作为对比)Chubby 直接管理客户端的 cache,它会阻塞更新,以使更新部分的客户端的缓存全部失效。在这样的设计下,如果任何客户端响应慢或者出现错误,更新会变得很慢。Chubby 使用 lease 机制防止一个慢或者宕机的客户端阻塞系统。但 leases 只能约束慢客户端或者宕机客户端的影响,ZooKeeper 的 watches 可以完全避免这个问题。 -本论文讨论ZooKeeper的设计和实现,使用ZooKeeper,即使只有写入是可线性化的,我们也可以实现应用程序所需的所有协调原语。 为了验证我们的方法,我们展示了如何使用 ZooKeeper 实现一些协调原语。 +本论文讨论ZooKeeper的设计和实现,使用ZooKeeper,我们可以实现应用程序所需的所有协调原语,即使只有写入是可线性化的。 为了验证我们的方法,我们展示了如何使用 ZooKeeper 实现一些协调原语。 作为总结,这篇文章中,我们主要的贡献是: -* **Coordination kernel**: 为了在分布式系统中使用,我们添了一个提供 relaxed 的一致性保证的 wait-free 的协作服务。特别是,我们描述了*协调内核*的设计和实现,我们已经在许多关键应用程序中使用了协调内核来实现各种协调技术。 -* **Coordination recipes**: 我们展示了 ZooKeeper 如何可用于构建通常在分布式应用程序中使用的高级协调原语,甚至包含阻塞和强一致性原语。 -* **Experience with Coordination**: 我们分享了一些使用 ZooKeeper 并评估其性能的方式。 +* **Coordination kernel**: 我们提出了一种 wait-free 的协调服务,可用于在分布式系统中提供宽松的(relaxed)一致性保证。特别是,我们描述了*协调内核*的设计和实现,我们已经在许多关键应用程序中使用了协调内核来实现各种协调技术。 +* **Coordination recipes**: 我们展示了如何使用 ZooKeeper 在分布式系统中构建高级协调原语,甚至是常用的阻塞和强一致性原语 +* **Experience with Coordination**: 我们分享了一些我们使用 ZooKeeper 的方式,并评估其性能。 ## 2. The ZooKeeper Service 客户端通过 ZooKeeper 客户端 API 库向 ZooKeeper 递交请求。除了暴露 ZooKeeper 服务的 client API 接口, ZooKeeper 客户端库也管理 client 和 ZooKeeper 服务器间的网络连接。 -在这一节中,我们写提供一个 ZooKeeper 服务的 high-level view。然后我们讨论 client 与 ZooKeeper 操作的 API。 +在这一节中,我们首先提供一个 ZooKeeper 服务的高层级视图(high-level view)。接下来讨论 client 与 ZooKeeper 交互的 API。 -> **术语** 在这篇论文章,我们使用 *`client`* 来表示一个 ZooKeeper 服务的使用者,*`znode`* 表示一个内存中的 ZooKeeper 数据的节点。`znode` 会被组织成一颗被称为 `data tree` 的带层次的名称空间。我们还使用术语“更新和写入”来指代任何修改数据树状态的操作。 客户端在连接到 ZooKeeper 并获得 `session` 句柄以发出请求时建立 `session`。 +> **术语** 在这篇论文章,我们使用 *`client`* 来表示一个 ZooKeeper 服务的使用者,*`znode`* 表示一个内存中的 ZooKeeper 数据的节点,它会被组织成在一个被称为 `data tree` 的分层命名空间中。我们还使用术语“更新和写入”来指代任何修改数据树状态的操作。 客户在连接到 ZooKeeper 时建立一个会话(session),并获得一个会话句柄,通过它发送请求。 ### 2.1 Service Overview -ZooKeeper 给它的客户端提供了“data nodes\(`znodes`\)的集合”的抽象,用层次的名称空间组织他们。client 通过 API 操纵在这个层次中的数据对象。层次的名称空间在文件系统里被广泛使用。这是一种组织层次空间的可靠方法,因为用户习惯这种抽象,同时它使更好的应用元数据组织成为可能。为了引用一个给定的 `znode` ,我们使用标准的 UNIX 文件系统路径。例如,我们使用 `/A/B/C` 来表示到 `znode` C 的路径,B 是 C 的父节点,同时 A 是 B 的父节点。所有的 `znode` 都存储数据,同时,除了 `ephemeral znodes` 外的所有的 `znode` ,都能拥有子节点。 +ZooKeeper 给它的客户端提供了“data nodes\(`znodes`\)的集合”的抽象,用分层的命名空间组织他们。client 通过 API 操纵在这个层次中的数据对象。分层的命名空间在文件系统里被广泛使用。这是一种组织数据对象的可靠方法,因为用户已经习惯了这种抽象,同时它可以更好的组织应用的元数据。为了引用一个给定的 `znode` ,我们使用标准的 UNIX 文件系统路径符号。例如,我们使用 `/A/B/C` 来表示一条到 `znode` C 的路径,B 是 C 的父节点,同时 A 是 B 的父节点。所有的 `znode` 都存储数据,并且除了 `ephemeral znodes` 外的所有 `znode` 都能拥有子节点。 client 可以创建两种 `znode`: -* `Regular`: client 显式操纵和删除 `regular znodes` -* `Ephemeral`: client 会创建这种节点,它可以被显式删除,也可以在创建这个节点的 `session` 断开的时候自动删除(故意或由于故障)。 +* `Regular`: client 可以显式操纵和删除 `regular znodes` +* `Ephemeral`: client 会创建这种节点,这类节点要么被显式删除,要么在创建这个节点的 `session` 断开的时候(故意或由于故障)被系统自动删除。 -此外,当我们创建一个新的 `znode` 的时候,一个 client 可以设置 `sequential` flag. 通过 `sequential` flag 创建的节点会在节点名称中添加一个单调递增的计数器。如果 `n` 是新的 `znode`, `p` 是他的父节点,那么 `n` 的名称中添加的值不会小与 `p` 的子节点中创建过的任何一个添加过的值。 +此外,当我们创建一个新的 `znode` 的时候,client 可以设置 `sequential` flag. 通过 `sequential` flag 创建的节点会在节点名称中添加一个单调递增的计数器的值。如果 `n` 是新的 `znode`, `p` 是他的父节点,那么 `n` 的名称中添加的值不会小与 `p` 的子节点中创建过的任何一个添加过的值。 -ZooKeeper 通过实现 watch 来让 client 不需要轮询即可及时接收到值的变化的通知。当一个客户端带有 `watch` flag 并发起读请求的时候,该操作将正常完成,并且服务器承诺在返回的信息已更改时通知客户端。 watch 是与`session` 相关的一次性触发器; 一旦触发或`session` 关闭,它们将被注销。 `watch` 表明发生了更改,但未提供更改的内容。 例如,如果客户端在两次更改`/foo`之前发出了请求 `getData('/foo',true)`,则客户端将只获得一个 watch事件,告知客户端`/foo`的数据已更改。 `session` 事件(例如连接丢失事件)也将发送到 watch 回调,以便客户端知道 watch 事件可能会延迟. +ZooKeeper 通过实现 watch 来让 client 不需要轮询即可及时接收到值的变化的通知。当客户端在设置了 `watch` flag 的情况下发起读取操作时,除了服务器承诺在返回的信息发生变化时通知客户端外,其他操作都会正常完成。 watches 是被关联到 `session` 的一次性触发器; 一旦触发或者该 `session` 关闭,它们将被注销。 `watch` 表明发生了变更,但未提供变更的内容。 例如,如果客户端在两次更改`/foo`之前发出了请求 `getData('/foo',true)`,则客户端将只获得一个 watch事件,告知客户端`/foo`的数据已更改。`session` 事件(例如连接丢失事件)也将发送到 watch 回调,以便客户端知道 watch 事件可能会延迟. #### 数据模型 -ZooKeeper 的数据模型是一个只有把整个 znode 全部数据全部读/写的文件系统的简化 api,或者可以说是有 key 层次的 key/value 表。层次名称空间对于为不同应用程序的名称空间分配子树、设置对这些子树的访问权限很有用。 我们还将在客户端利用目录的概念来构建更高级别的原语,如我们在2.4节中将看到的。 +ZooKeeper 的数据模型本质上是一个简化了API的文件系统,只支持完整的数据读写,或者可以说是 key 有层级的 key/value 表。分层命名空间对于为不同应用的命名空间分配子树以及设置这些子树的访问权限非常有用。我们还将在客户端利用目录的概念来构建更高级别的原语,如我们在2.4节中将看到的。 ![fig01_hierarchical_name_space](images/fig01_hierarchical_name_space.png) -与文件系统中的文件不同,znode不适用于通用数据存储。 相反,`znodes` 存储 client 引用的数据,通常是用于协调的元数据。为了图解,在 Figure 1 中我们有两个子树,一个用于应用程序1(`/app1`),另一个用于应用程序2(`/app2`)。应用程序1的子树实现了一个简单的组成员身份协议:每个客户端进程`pi`在`/app1`下创建一个`znode` `pi` ,只要该进程正在运行,该节点便会持续存在。 +与文件系统中的文件不同,znode 不是为通用数据存储设计的。 相反,`znodes` 存储 client 引用的数据,通常是用于协调的元数据。为了展示,在 Figure 1 中我们有两个子树,一个用于应用程序1(`/app1`),另一个用于应用程序2(`/app2`)。应用程序1的子树实现了一个简单的组成员身份协议:每个客户端进程`pi`在`/app1`下创建一个`znode` `pi` ,只要该进程还在运行,节点便会持续存在。 -尽管 `znode` 并非设计用于通用数据存储,但是 ZooKeeper 允许客户端存储一些可用于分布式计算中的元数据或配置的信息。例如,在基于 leader 的应用程序中,对应用服务而言,确定当前的Leader很有用。为了实现此目标,我们可以让当前的领导者在 `znode` 空间中的已知位置写入此信息。 `znode` 还将 元数据与 timestamp 和 version counter 关联,这使客户端可以跟踪对 `znode` 的更改并根据 `znode` 的版本执行 条件更新。 +尽管 `znode` 并非设计用于通用数据存储,但是 ZooKeeper 允许客户端存储一些可用于分布式计算中的元数据或配置的信息。例如,在基于 leader 的应用程序中,对应用服务而言,确定当前的Leader很有用。为了实现此目标,我们可以让当前的领导者在 `znode` 空间中的已知位置写入此信息。 `znode` 还将元数据与 timestamp 和 version counter 关联,这使客户端可以跟踪对 `znode` 的更改并根据 `znode` 的版本执行条件更新。 #### Sessions @@ -76,14 +76,14 @@ client 连接到 ZooKeeper 并初始化 `session`。`session `具有关联的 ti * `exists(path, watch)`: 如果 `path` 下的 `znode` 存在,返回 true, 否则返回 false.`watch` 标志可以使 client 在 `znode` 上设置 watch。 * `getData(path, watch)`: 返回 `znode` 的数据和元数据(元数据例如版本信息)。`watch` 和 `exists()` 里面的作用一样,不同之处在于,如果`znode`不存在,则 ZooKeeper 不会设置`watch`。 * `setData(path, data, version)`: 如果 `version` 是 `znode` 现有的版本,把 `data[]` 写进 `znode`. -* `getChildren(path, watch)`: 返回`path` 对应的 `znode` 的子节点集合。 -* `sync(path)`: 等待操作开始时所有没有同步的更新传播到 client 连接到的服务器。 该`path` 当前被忽略。 +* `getChildren(path, watch)`: 返回 `path` 对应的 `znode` 的子节点集合。 +* `sync(path)`: 等待操作开始时所有没有同步的更新传播到 client 连接到的服务器。 该 `path` 当前被忽略。 -所有的方法在 API 中都有一个同步版本和一个异步版本。 当应用程序需要执行单个 ZooKeeper 操作且没有要并发执行的任务时,它会使用同步API,因此它会进行必要的 ZooKeeper 调用并进行阻塞。但是,异步API使应用程序可以并行执行多个未完成的 ZooKeeper 操作和其他任务。 ZooKeeper client 保证按顺序调用每个操作的相应回调。 +所有的方法在 API 中都有一个同步版本和一个异步版本。 当应用程序需要执行单个 ZooKeeper 操作且没有要并发执行的任务时,它会使用同步API,因此它会进行必要的 ZooKeeper 调用并进行阻塞。但是,异步API使应用程序可以并行执行多个未完成的 ZooKeeper 操作和其他任务。ZooKeeper client 保证按顺序调用每个操作的相应回调。 -需要注意的是,ZooKeeper 不使用句柄来操纵 `znode`。作为替代,每个请求都带有需要操作的 `znode` 的完整路径。这样不仅仅简化了 API \(没有 `open()` 和 `close()` 方法\),也消除了服务器需要维护的额外状态。 +需要注意的是,ZooKeeper 不使用句柄来操纵 `znode`。相反,每个请求都带有需要操作的 `znode` 的完整路径。这样不仅仅简化了 API \(没有 `open()` 和 `close()` 方法\),也消除了服务器需要维护的额外状态。 -每种更新方法均需要一个预期的 version,从而可以实施 条件更新。 如果 `znode` 的实际版本号与预期版本号不匹配,则更新将失败,并出现 ` unexpected version error`。 如果给定的预期版本号为 `-1`,则不执行版本检查。 +每种更新方法均需要一个预期的 version,从而可以实现条件更新。如果 `znode` 的实际版本号与预期版本号不匹配,则更新将失败,并出现 `unexpected version error`。如果给定的预期版本号为 `-1`,则不执行版本检查。 ### 2.3 ZooKeeper guarantees @@ -92,18 +92,18 @@ ZooKeeper 有两个基本的顺序保证: * **Linearizable Writes**: 所有的更新 ZooKeeper 状态的请求都是 serializable 的,并且遵循优先级。 * **FIFO client order**: 一个给定客户端发送的所有的请求都是按照客户端发送的顺序有序执行的。 -注意我们对 `linearizability` 的定义和 Herihy 的原始提议不同,我们叫它 *A-linearizability* \(asynchronize linearizability\). 在 Herilihy 的原始的 linearizable 定义,一个客户端只能在同一时间内有一个未完成的请求(一个客户端是一个线程)。在我们的系统中,我们允许一个客户端有复数个未完成的操作,并且我们可以选择不保证未完成操作的执行顺序,或者使用保证 FIFO 顺序。我们选择后者作为我们的属性,可以观察到,对于保持 linearizable 对象也会保持 A-linearizable ,因为满足 A-linearizable 的系统也会满足 linearizable。因为只有更新请求是 A-linearizable 的,所以 ZooKeeper 可以在每个副本本地处理读请求。这样,当服务器添加到系统时,服务可以线型扩展。 +注意我们对 `linearizability` 的定义和 Herihy 的原始提议不同,我们叫它 *A-linearizability* \(asynchronize linearizability\). 在 Herilihy 的原始的 linearizable 定义,一个客户端只能在同一时间内有一个未完成的请求(一个客户端是一个线程)。在我们的系统中,我们允许一个客户端有复数个未完成的操作,并且我们可以选择不保证未完成操作的执行顺序,或者保证 FIFO 顺序。我们选择后者作为我们的属性。可以观察到,对于保持 linearizable 对象也会保持 A-linearizable ,因为满足 A-linearizable 的系统也会满足 linearizable。因为只有更新请求是 A-linearizable 的,ZooKeeper 在每个副本本地处理读请求。这样,当服务器添加到系统时,服务可以线型扩展。 -为了了解两个保证是怎么交互的,考虑下列的场景。一个系统由一组进程组成,它选择出一个 leader,并由 leader 操作工作进程。但一个新的 Leader 接管整个系统时,它必须更新大量的配置参数,并在更新结束的时候通知其它进程。这样我们有两个重要的需求: +为了了解两个保证是怎么交互的,考虑以下场景。一个系统由一组进程组成,它选择出一个 leader,并由 leader 操作工作进程。当一个新的 Leader 接管整个系统时,它必须更新大量的配置参数,并在更新结束的时候通知其它进程。这样我们有两个重要的需求: -1. 当新的 Leader 开始更改系统时,我们不希望其它进程开始使用被更改的配置。 +1. 当新的 Leader 开始更改系统时,我们不希望其它进程开始使用正在被更改的配置。 2. 当新的 Leader 在配置完全更新完成之前就宕机时,我们不希望其它进程使用半更新的配置。 -观察到分布式锁,例如 Chubby 提供的锁,将会帮助我们完成需求1,但是对需求2并不有效。有了 ZooKeeper, 新的 Leader 可以指定一个路径,并把它当成一个 `ready znode`,其他的进程只会在这个 `znode` 存在的时候使用这套配置。新的 leader 靠 \(1\) 删除 `ready` \(2\) 更新配置的 `znode` \(3\) 重新创建 `ready` 来完成上述需求。上述的所有变更可以被流水线处理,并发起一个异步请求来快速更新配置状态。尽管更改操作的等待时间约为2毫秒,但是如果一个请求接一个发出(即同步的一个一个请求处理),则需要更新 5000 个不同znode的新 leader 将花费10秒。 通过异步发出请求,请求将花费不到一秒钟的时间。由于顺序保证,如果进程看到 ready `znode`,则它还必须看到新的 Leader 所做的所有配置更改。 如果新的 Leader 在创建 ready `znode` 之前宕机,则其他进程知道该配置尚未完成,因此不使用它。 +注意到,分布式锁,例如 Chubby 提供的锁,可以满足需求1,但是无法满足需求2。有了 ZooKeeper, 新的 Leader 可以指定一个路径,并把它当成一个 `ready znode`,其他的进程只会在这个 `znode` 存在的时候使用这套配置。新的 leader 靠 \(1\) 删除 `ready` \(2\) 更新各种配置的 `znode` \(3\) 重新创建 `ready` 来完成上述需求。上述的所有变更可以被流水线处理,并发起一个异步请求来快速更新配置状态。尽管更改操作的延迟约为2毫秒,但是如果一个请求接一个发出(即同步的一个一个请求处理),则需要更新 5000 个不同znode的新 leader 将花费10秒。 通过异步发出请求,请求将花费不到一秒钟的时间。由于顺序保证,如果进程看到 ready `znode`,则它还必须看到新的 Leader 所做的所有配置更改。 如果新的 Leader 在创建完 ready `znode` 之前宕机,则其他进程知道该配置尚未完成,因此不使用它。 -上述的模式仍然有一个问题:如果一个进程在新的 Leader 开始变更并读到配置看到 ready 存在会发生什么?这个问题通过 notification 的顺序保证解决:如果一个客户端正在 watch 一个变更,这个客户端会在它看到配置变更之前收到一个通知。因此,如果读取 ready `znode` 的进程请求要在 `znode` 变更的时候被通知,它的 client 会在收到配置变化之前,收到 ready `znode` 变化的通知。 +上述的模式仍然有一个问题:如果一个进程在新的领导开始进行变更之前看到 ready 存在,然后在变更进行中开始读取配置,会发生什么?这个问题通过 notification 的顺序保证解决:如果一个客户端正在 watch 一个变更,这个客户端会在它看到配置变更之前收到一个通知。因此,如果读取 ready `znode` 的进程请求要在 `znode` 变更的时候被通知,它的 client 会在收到配置变化之前,收到 ready `znode` 变化的通知。 -当 client 除了 ZooKeeper之外还拥有自己的通信信道时,可能会出现另一个问题。 例如,考虑两个客户端 A、B在 ZooKeeper 中有共享的配置,并通过一个共享的通信信道通信。如果 A 更改了 ZooKeeper 中的共享配置,并通过 channel 告知 B,B 会期望在重新读取的时候看到配置的变化。 如果B的 ZooKeeper 副本稍微落后于A,则可能看不到新配置(因为读是在本地进行的)。使用上一段中的保证 B 可以通过在重新读取配置之前发出写入操作来确保它能看到最新信息。为了更有效地处理这种情况,ZooKeeper 提供了 `sync` 请求:在进行读取后,构成了一个慢速的读取。`sync`操作不需要一次写操作就可以在读之前使得服务器将所有在它之前未完成的写请求完成。这一原语与ISIS的原语`flush`类似。 +当 client 除了 ZooKeeper 之外还拥有自己的通信信道时,可能会出现另一个问题。 例如,考虑两个客户端 A、B在 ZooKeeper 中有共享的配置,并通过一个共享的通信信道通信。如果 A 更改了 ZooKeeper 中的共享配置,并通过 channel 告知 B,B 会期望在重新读取的时候看到配置的变化。 如果B的 ZooKeeper 副本稍微落后于A,则可能看不到新配置(因为读是在本地进行的)。使用上一段中的保证 B 可以通过在重新读取配置之前发出写入操作来确保它能看到最新信息。为了更有效地处理这种情况,ZooKeeper 提供了 `sync` 请求:当 `sync` 请求之后,下一个请求是读,则构成了一个慢速的读取。`sync` 使服务器在处理读操作前应用所有之前未完成的写请求,节省了一次全量写(full write) 的开销。这一原语与ISIS的 `flush` 原语类似。 ZooKeeper 也有下述两个 liveness 和持久性保证:如果**大部分** ZooKeeper 服务器是活跃的并且可以通信,ZooKeeper 服务是可用的, 如果 ZooKeeper 服务成功响应更改请求,任何数量的 ZooKeeper 服务器故障中,该变更都会持续存在,只要最终大部分服务器可以恢复。 @@ -113,21 +113,23 @@ ZooKeeper 也有下述两个 liveness 和持久性保证:如果**大部分** Z #### 配置管理 -ZooKeeper 可以被用来实现分布式应用中的动态配置。在它最简单的形式中,配置被存储在 `znode` `Zc` 中, 进程以 `Zc` 的完整路径名启动。 启动进程通过将 watch 标志设置为 true 来读取 `Zc` 来获取其配置。 如果`Zc`中的配置出现任何更新,则会通知进程并读取新配置,进程再次将watch标志设置为true。 +ZooKeeper 可以被用来实现分布式应用中的动态配置。在它最简单的形式中,配置被存储在 `znode` `Zc` 中, 进程以 `Zc` 的完整路径名启动。启动进程通过读取 `Zc` 并将 watch 标志设置为 true 来获取其配置。如果`Zc`中的配置出现任何更新,进程会收到通知并读取新配置,然后再次将 watch 标志设置为 true。 -注意在这个模式中,如同大部分其它使用 watch 的模式,watch 被用来保证进程有最近的信息。例如,如果一个 watch `Zc` 的进程,在读取之前发出了3个对 `Zc` 的更改,那么这个进程不会收到三个通知。这不会影响进程的行为,因为这三个事件只会通知进程它已经知道的内容:它对`Zc` 拥有的信息是陈旧的。 +注意在这个模式中,如同大部分其它使用 watch 的模式,watch 被用来保证进程有最近的信息。例如,如果一个 watch `Zc` 的进程收到了 `Zc` 发生变化的通知,而在它读取 `Zc` 之前又发生了3个对 `Zc` 的更改,那么这个进程不会收到后续事件的三个通知。这并不会影响进程的行为,因为这三个事件只是简单的通知进程它已经知道的信息:它拥有的关于 `Zc` 的信息已经过期了。 \(这里我有点不明白,是指更新的时候会完成一次 sync 么) #### Rendezvous -有时在分布式系统中,我们不会预先知道最终的系统配置会是什么样子的。例如,一个客户端可能需要启动一个 master 进程和不少 worker 进程,但是启动进程是由调度器完成的,所以客户端不能提前知道知道需要连接的 Address 和 Port 等连接 master 需要的相关的信息。我们通过 client 创建 `rendezvous znode` (即 `Zr`)来解决这个问题。客户端把 `Zr` 的整个路径作为启动参数传给 master 和 worker 进程。当 master 启动时,它会把自己的地址和端口信息填充进 `Zr`.当 workers 启动的时候,它们会以 watch flag 读取 `Zr`。如果 `Zr` 还没有被填充, worker 就会等待 `Zr` 被更新。如果 `Zr` 是一个 `ephemeral` 节点,master 和 worker 进程可以通过 watch `Zr` 等待是否被删除,决定在 client 终止后对自己进行清理。 +有时在分布式系统中,我们不会预先知道最终的系统配置会是什么样子的。例如,一个客户端可能需要启动一个 master 进程和一些 worker 进程,但是启动进程是由调度器完成的,所以客户端无法提前知道连接 master 需要的 Address 和 Port 等相关的信息。我们通过 client 创建 `rendezvous znode` (即 `Zr`)来解决这个问题。客户端把 `Zr` 的整个路径作为启动参数传给 master 和 worker 进程。当 master 启动时,它会把自己的地址和端口信息填充进 `Zr`.当 workers 启动的时候,它们会以 watch flag 读取 `Zr`。如果 `Zr` 还没有被填充, worker 就会等待 `Zr` 被更新。如果 `Zr` 是一个 `ephemeral` 节点,master 和 worker 进程可以 watch `Zr` 是否被删除,并在 client 终止时自行清理。 #### Group Membership -我们利用 `ephemral` 节点来实现 group membership. 特别的,我们利用 `ephemeral` 节点允许我们看到创建 `znode` 的 `session` 状态的特性。我们从指定一个 `znode` `Zg`,来代表这个 group 开始。当一个group的成员启动后,它在 `Zg` 下创建一个临时节点。如果每个进程都有一个唯一的名称或者标识符,那么这个名称就会被用作创建的子 `znode` 的名称; 否则,他会用 `SEQUENTIAL` flag 来创建一个有独立名称的 `znode`。进程可以将进程信息,入该进程使用的地址和端口,放入子`znode`的数据中。 +我们利用 `ephemral` 节点来实现 group membership. 更确切地说,我们利用了 `ephemeral` 节点允许我们看到创建 `znode` 的 `session` 状态的特性。我们从指定一个 `znode` `Zg`,来代表这个 group 开始。当一个group的成员启动后,它在 `Zg` 下创建一个临时节点。如果每个进程都有一个唯一的名称或者标识符,那么这个名称就会被用作创建的子 `znode` 的名称; 否则,他会用 `SEQUENTIAL` flag 来创建一个有唯一名称的 `znode`。进程可以将进程信息放入子`znode`的数据中,比如该进程使用的地址和端口。 -在`Zg`下创建子`znode`后,该过程将正常启动。 它不需要做任何其他事情。 如果该过程失败或结束,则代表`Zg`的`znode` 会自动删除。进程可以通过简单列出`Zg`的子代来获取组信息。 如果某个进程希望监视组成员身份的更改,则该进程可以将监视标志设置为 true,并在收到更改通知时刷新组信息(这个进程始终将监视标志设置为true)。 +在`Zg`下创建子`znode`后,该进程将正常启动。不需要额外的操作。如果进程失败或结束,在 `Zg` 下代表该进程的 `znode` 会被自动删除。 + +进程可以通过简单列出`Zg`的孩子来获取组信息。 如果某个进程希望监视组成员身份的更改,则该进程可以将监视标志设置为 true,并在收到更改通知时刷新组信息(这个进程始终将监视标志设置为true)。 #### Simple Locks @@ -143,7 +145,7 @@ ZooKeeper 可以被用来实现分布式应用中的动态配置。在它最简 ``` Lock -1: n - create(1 + "/lock-", EPHEMERAL | SEQUENTIAL) +1: n = create(1 + "/lock-", EPHEMERAL | SEQUENTIAL) 2: C = getChildren(1, false) 3: if n is lowest znode in C, exit 4: p = znode in C ordered just before n @@ -154,7 +156,7 @@ Unlock 1: delete(n) ``` -在Lock的第1行中使用`SEQUENTIAL`标志,命令 client 尝试获取锁,并相对其它的尝试获得一个序列号。如果客户端的`znode`在第3行的序列号最小,则客户端将持有该锁。否则,客户端将等待下列两种 `znode`被删除: 持有 Lock 的 `znode`,将在此客户端的`znode` 之前获得锁的 `znode` 。通过仅查看客户端`znode`之前的`znode`,我们仅在释放锁或放弃锁请求时才唤醒一个进程,从而避免了惊群效应。客户端 watch 的znode消失后,客户端必须检查它现在是否持有该锁。(先前的 Lock 请求可能已被放弃,或者具有较低序号的`znode` 仍在等待或保持锁。) +在Lock的第1行中使用`SEQUENTIAL`标志,命令 client 尝试获取锁,并相对其它的尝试获得一个序列号。如果客户端的`znode`在第3行的序列号最小,则客户端将持有该锁。否则,客户端将等待下列两种 `znode` 被删除: 持有 Lock 的 `znode`,将在此客户端的 `znode` 之前获得锁的 `znode` 。通过仅查看客户端`znode`之前的`znode`,我们仅在释放锁或放弃锁请求时才唤醒一个进程,从而避免了惊群效应。客户端 watch 的 `znode` 消失后,客户端必须检查它现在是否持有该锁。(先前的 Lock 请求可能已被放弃,或者具有较低序号的`znode` 仍在等待或持有锁。) 释放锁就简单地直接删除代表 Lock 请求的`znode` *n*。通过在创建 `znode` 时使用`EPHEMERAL`标志,崩溃的进程将自动清除所有锁定请求或释放它们可能拥有的任何锁定。 @@ -162,7 +164,7 @@ Unlock 1. 删除一个`znode`只会导致一个客户端唤醒,因为每个`znode`都被另一个客户端 watch,因此我们没有惊群效应; 2. 没有轮询或超时; -3. 由于我们实现了锁的方式,因此通过浏览 ZooKeeper 数据可以看到 Lock 争用,中断锁定和调试锁定问题的进程的数量。 +3. 由于我们实现锁的方式,因此通过浏览 ZooKeeper 数据可以看到锁争用的数量,中断锁定以及调试锁相关的问题。 #### Read/Write Locks @@ -190,11 +192,11 @@ Read Lock 6: goto 3 ``` -该锁定过程与之前的锁定略有不同。 写锁仅在命名上有所不同。 由于读锁可能是 shared 的,因此第3行和第4行略有不同,因为只有较早的写锁`znode`会阻止客户端获得读锁。 当有多个客户端等待读锁时,当删除具有较低序号的`write-` znode时,我们可能会收到 “惊群效应”。 实际上,这是一种符合预期的行为,所有那些需要读的客户端都应被通知,因为它们现在可能已具有锁定。 +该上锁过程与之前上锁的过程略有不同。 写锁仅在命名上有所不同。 由于读锁可能是 shared 的,因此第3行和第4行略有不同,因为只有较早的写锁`znode`会阻止客户端获得读锁。 当有多个客户端等待读锁时,当删除具有较低序号的`write-` znode时,我们可能会收到 “惊群效应”。 实际上,这是一种符合预期的行为,所有那些需要读的客户端都应被通知,因为它们现在可能持有锁。 #### Double Barrier -double barriers 使 client 能够同步计算的开始和结束。当有 barrier 限制定义的足够多的进程加入 barrier 时,进程将会开始它们的计算,并在计算结束后离开 barrier。我们在 ZooKeeper 中用`znode` 表示一个 barrier,称为*b*。 每个进程p都会在进入时通过将`znode`创建为 *b* 的子节点来向 *b* 注册,并在准备离开时注销,即删除该子节点。 当b的子`znode`数量超过 barrier 阈值时,进程可以进入 barrier。 当所有进程都删除了其子进程时,进程可以离开 barrier。 我们使用 watch 来有效地等待进入和退出 barrier 条件得到满足。 要进入 barrier,流程会监视是否存在 *b* 的 ready 子 `znode` ,该子 `znode` 将由导致子节点数超过障碍阈值的进程创建。 要离开 barrier,进程会 watch 特定的子节点的消失,并且仅在这个`znode`被删除之后检查退出条件。 +double barriers 使 client 能够同步计算的开始和结束。当有足够多的进程(具体数量由 barrier 阈值决定)进入到 barrier 中时,进程将会开始它们的计算,并在计算结束后离开 barrier。我们在 ZooKeeper 中用`znode` 表示一个 barrier,称为*b*。 每个进程p都会在进入时通过将`znode`创建为 *b* 的子节点来向 *b* 注册,并在准备离开时注销,即删除该子节点。 当b的子`znode`数量超过 barrier 阈值时,进程可以进入 barrier。 当所有进程都删除了其子进程时,进程可以离开 barrier。 我们使用 watch 来有效地等待进入和退出 barrier 条件得到满足。 要进入 barrier,流程会监视是否存在 *b* 的 ready 子 `znode` ,该子 `znode` 将由导致子节点数超过障碍阈值的进程创建。 要离开 barrier,进程会 watch 特定的子节点的消失,并且仅在这个`znode`被删除之后检查退出条件。 ## 3 ZooKeeper Applications @@ -230,29 +232,29 @@ Figure 3显示了 YMB 的`znode`数据分布的一部分。 每个 broker 域都 ZooKeeper通过在组成服务的每台服务器上复制 ZooKeeper 数据来提供高可用性。 我们假设服务器因崩溃而失败,并且此类故障服务器稍后可能会恢复。 Figure 4显示了 ZooKeeper 服务的层次组件。 收到请求后,服务器会为执行做 prepare(request processor)。 如果这样的请求需要服务器之间的协调(是写请求),则它们使用 agreement protocol(原子广播的一种实现),最后服务器将更改提交到 ZooKeeper 数据库中,该更改已在整个集成服务器中完全复制。 对于读取请求,服务器仅从本地数据库读取状态并生成对该请求的响应。 -复制数据库是一个包含整个数据树的内存数据库。默认情况下,每个数据库中的 `znode` 最多存储 1MB 的数据,但是这个值是一个可配置的值,在特殊的情况下可以被更改。为了可恢复性,我们高效的把 log 更新在磁盘中,我们强制在写入内存数据库之前写入磁盘。实际上,与 Chubby 一样,我们对于提交的操作维护了一个重放日志 (在我们的例子中,是一个 write-ahead log),并周期性的为内存数据库生成快照。 +复制数据库是一个包含整个数据树的内存数据库。默认情况下,每个数据库中的 `znode` 最多存储 1MB 的数据,但是这个最大值是一个可配置的值,在特定情况下可以被更改。为了可恢复性,我们高效的把更新记录到磁盘中,并且强制在写入内存数据库之前写入磁盘。实际上,与 Chubby 一样,我们对于提交的操作维护了一个重放日志 (在我们的例子中,是一个 write-ahead log),并周期性的为内存数据库生成快照。 -每个 ZooKeeper 的服务器都可以连接客户端,客户端只要连接到一个服务器,来提交它的请求,正如我们之前提到的,读请求从每个服务器的本地数据库读取。更新服务器状态的写请求,会被一个 agreement protocol 处理。 +每个 ZooKeeper 的服务器都可以为客户端提供服务。客户端连接一台服务器来提交它的请求。正如我们之前提到的,读请求从每个服务器的本地数据库读取。更新服务器状态的写请求由 agreement protocol 处理。 -作为 agreement protocol 的一部分,写请求会被转发给单一的被称为 *leader* 的服务器,其他的 *ZooKeeper* 服务器被称为 *follower* ,它们从 leader 接受包含状态变更的 proposal, 并就状态的更改达成一致。 +作为 agreement protocol 的一部分,写请求会被转发给单独的服务器,该服务器被称为 *leader* ,其他的 *ZooKeeper* 服务器被称为 *follower* ,它们从 leader 接受包含状态变更的 proposal, 并就状态的更改达成一致。 ### 4.1 Request Processor -因为 message layer 是 atomic 的,我们保证副本不会出现分歧,尽管在有些时间点有些服务器可能应用的事务会更多。与客户端的请求不同,事务是幂等的。领导者收到写请求后,它将计算*应用*写操作时系统的状态,并将其转换为捕获该新状态的事务。 因为可能存在尚未应用到数据库的未完成事务,所以必须计算未来的状态。 例如,如果客户端执行 条件`setData`,并且请求中的版本号与正在更新的`znode`的未来的版本号匹配,则该服务将生成一个 `setDataTXN`,其中包含新数据,新版本号和更新的时间戳。 如果发生错误,例如版本号不匹配或要更新的`znode`不存在,则会生成`errorTXN`。 +因为 message layer 是 atomic 的,我们保证副本不会出现分歧,尽管在任意时刻,某些服务器可能会应用了比其他服务器更多的事务。与客户端的请求不同,事务是幂等的。领导者收到写请求后,它将计算*应用*写操作时系统的状态,并将其转换为捕获该新状态的事务。 因为可能存在尚未应用到数据库的未完成事务,所以必须计算未来的状态。 例如,如果客户端执行条件`setData`,并且请求中的版本号与正在更新的`znode`的未来的版本号匹配,则该服务将生成一个 `setDataTXN`,其中包含新数据,新版本号和更新的时间戳。 如果发生错误,例如版本号不匹配或要更新的`znode`不存在,则会生成`errorTXN`。 ### 4.2 Atomic Broadcast -所有更新 ZooKeeper 的请求都会被转发给 leader, leader 执行请求,并用 `Zab` ,一个原子广播协议,广播 ZooKeeper 的状态变更。Zab 默认使用简单的 majority quorum 来决定一个 proposal,所以 Zab 和 Zookeeper 只有在大多数机器正确相应的时候才能工作(*2f+1* 台服务器,我们可以容忍 *f* 个错误)。 +所有更新 ZooKeeper 的请求都会被转发给 leader, leader 执行请求,并用原子广播协议 `Zab` 广播 ZooKeeper 的状态变更。从客户端接收请求的服务器在收到对应的状态变更后响应客户端。Zab 默认使用简单的 majority quorum 来决定一个 proposal,所以 Zab 和 Zookeeper 只有在大多数机器正确相应的时候才能工作(*2f+1* 台服务器,我们可以容忍 *f* 个错误)。 -为了达到高吞吐量,ZooKeeper 尝试维护整个请求处理流水线都在运行。在整个处理流水线中可能有几千个请求。因为状态变更取决于上一个状态,Zab 提供了比一个比原子广播协议强的顺序保证。更确切地说, Zab 保证 leader 广播的变化按照发送的顺序,并且上一个 leader 的变更会在这个 leader 的变更之前发送。 +为了达到高吞吐量,ZooKeeper 尝试维护整个请求处理流水线都在运行。在整个处理流水线中可能有上千个请求。由于状态变更依赖于上一个状态的应用,Zab 提供了比一个比原子广播协议更强的顺序保证。更确切地说, Zab 保证 leader 广播的变化按照发送的顺序,并且上一个 leader 的变更会在这个 leader 的变更之前发送。 有一些实现细节可以简化我们的实现,并为我们提供出色的性能。 我们使用 TCP 进行传输,因此消息顺序由网络层保证,这使我们可以简化实现(译者:Raft 等协议可以用 UDP 等实现)。 我们使用 Zab 选择的 leader 作为 ZooKeeper leader,因此创建事务的过程也可以处理事务。 我们使用日志来跟踪 Zab 协议,将其作为内存数据库的 write-ahead log,这样就不必将消息两次写入磁盘。 -在正常操作期间,Zab确实按顺序准确地传递了所有消息,但是由于Zab不会永久记录所传递的每个消息的ID,因此Zab可能会在恢复期间重新传递消息。 因为我们使用幂等事务,所以只要按顺序 deliver,就可以接受多次 deliver。 实际上,ZooKeeper要求Zab重新传递至少上一个快照开始之后传递的所有消息。(译者:这段话我看了下一节才看懂)。 +在正常操作期间,Zab会按顺序准确无误地传递所有消息,但是由于Zab不会永久记录所传递的每个消息的ID,因此Zab可能会在恢复期间重新传递消息。 因为我们使用幂等事务,所以只要按顺序 deliver,就可以接受多次 deliver。 实际上,ZooKeeper要求Zab重新传递至少上一个快照开始之后传递的所有消息。(译者:这段话我看了下一节才看懂)。 ### 4.3 Replicated Database -每个副本对 ZooKeeper 状态在内存中都有一个拷贝。 当 ZooKeeper 服务器从崩溃中恢复时,它需要恢复该内部状态。 在运行服务器一段时间后,重放所有已发送的消息以恢复状态将花费很长时间,因此 ZooKeeper 使用周期定时快照,并且只需要发送快照之后的状态变更。我们将ZooKeeper 快照称为模糊快照\(fuzzy snapshot\), 因为我们没有 Lock ZooKeeper 状态并获取快照(译者:即不会对 ZooKeeper 进行写锁)。 取而代之的是,我们对树进行深度优先的扫描,以原子方式读取每个`znode`的数据和元数据,并将它们写入磁盘。 由于生成的模糊快照可能已应用了快照生成过程中接收到的的状态变化的某些子集,因此快照的结果生成一个与任何时间的 ZooKeeper 状态都不对应的结果。 但是,由于 ZooKeeper 状态变更是幂等的,因此只要按状态变更的顺序应用状态更改,我们就可以多次引用这些变更。 +每个副本对 ZooKeeper 状态在内存中都有一个拷贝。 当 ZooKeeper 服务器从崩溃中恢复时,它需要恢复该内部状态。 在运行服务器一段时间后,重放所有已发送的消息以恢复状态将花费很长时间,因此 ZooKeeper 使用周期定时快照,并且只需要发送快照之后的状态变更。我们将 ZooKeeper 快照称为模糊快照\(fuzzy snapshot\), 因为我们没有 Lock ZooKeeper 状态并获取快照(译者:即不会对 ZooKeeper 进行写锁)。 取而代之的是,我们对树进行深度优先的扫描,以原子方式读取每个`znode`的数据和元数据,并将它们写入磁盘。 由于生成的模糊快照可能已应用了快照生成过程中接收到的的状态变化的某些子集,因此快照的结果生成一个与任何时间的 ZooKeeper 状态都不对应的结果。 但是,由于 ZooKeeper 状态变更是幂等的,因此只要按状态变更的顺序应用状态更改,我们就可以多次引用这些变更。 举个例子,假设 ZooKeeper 的数据树中,在模糊快照开始的时候,两个节点 `/foo` `/goo` 分别有 `f1` 和 `g1`的值,并且都是版本`1`,接下来到达的状态变更流以形式 `<事务类型, 路径, 值, 新版本>` 表示: @@ -262,7 +264,7 @@ ZooKeeper通过在组成服务的每台服务器上复制 ZooKeeper 数据来提 ``` -在处理完这些状态变更后,`/foo` 和 `/goo` 会有 `f3` 和 `g2` 的值,并且版本是 `3` 和 `2`。然而,模糊快照可能记录 `/foo` 和 `/goo` 会有 `f3` 和 `g1` 的值,并且版本是 `3` 和 `1`,这并非 ZooKeeper 数据树中出现的一个合法版本。如果服务器宕机并且接收到 Zab 广播的这个快照和日志,结果状态与崩溃前服务的状态相对应。 +在处理完这些状态变更后,`/foo` 和 `/goo` 会有 `f3` 和 `g2` 的值,并且版本是 `3` 和 `2`。然而,模糊快照可能记录 `/foo` 和 `/goo` 会有 `f3` 和 `g1` 的值,并且版本是 `3` 和 `1`,这并非 ZooKeeper 数据树中出现的一个合法版本。如果服务器崩溃并通过该快照恢复,Zab重新发送状态变化,则最终的状态与崩溃前服务的状态相对应。 ### 4.4 Client-Server Interactions @@ -270,9 +272,9 @@ ZooKeeper通过在组成服务的每台服务器上复制 ZooKeeper 数据来提 读请求由每个服务器在本地处理,每个读取请求都经过处理并用 zxid 标记,该 zxid 对应于服务器看到的最后一个事务。 该 zxid 定义了读取请求相对于写入请求的偏序。 通过本地处理读取,我们获得了出色的读取性能,因为它只是本地服务器上的内存中操作,并且没有要运行的磁盘操作或者广播协议。 这种设计选择是我们获得读为主的负载中高性能的关键。 -使用快速读取的一个缺点是不能保证读取操作的优先顺序。 也就是说,即使对同一`znode`的更新已经 commit,读取操作也可能返回陈旧的值。 并非我们所有的应用程序都需要(读的)优先级顺序,但是对于确实需要优先级的应用程序,我们已经实现了`sync`。这个原语是异步执行的,它由 leader 进行排序,保证`sync` 之前的 pending writes 全部写入它的副本。为了保证一个给定的读操作返回最新的值,客户端可以在 `sync` 之后调用读。客户端的 FIFO 顺序保证和 `sync` 的全局保证让读的结果反映了所有`sync` 之前的写入。在我们的实现中,因为我们使用 基于 leader 的协议,我们不用广播,简单把 `sync` 放在请求的队列末尾,让服务器执行 `sync`。为了使它起作用,follower 必须确保 leader 仍然是 leader。 如果有提交的 pending 事务,则服务器不会怀疑该领导者。 如果待处理队列为空,则领导者需要发出一个空事务来提交并把 `sync` 置于该事务之后。 它具有很好的属性,即当领导者处于负载状态时,不会生成额外的广播流量。 在我们的实现中,设置 timeout 是为了使 leader 在 follower 放弃他们之前意识到他们不再是 leader,因此它们就不会发布空事务。 +使用快速读取的一个缺点是不能保证读取操作的优先顺序。 也就是说,即使对同一`znode`的更新已经 commit,读取操作也可能返回陈旧的值。 并非我们所有的应用程序都需要(读的)优先级顺序,但是对于确实需要优先级的应用程序,我们已经实现了`sync`。这个原语是异步执行的,它由 leader 进行排序,保证 `sync` 之前的 pending writes 全部写入它的副本。为了保证一个给定的读操作返回最新的值,客户端可以在 `sync` 之后调用读。客户端的 FIFO 顺序保证和 `sync` 的全局保证让读的结果反映了所有`sync` 之前的写入。在我们的实现中,因为我们使用基于 leader 的协议,因此不需要广播 `sync`,只是简单的把 `sync` 放在请求的队列末尾,让服务器执行 `sync`。为了使该机制正常工作,follower 必须确保 leader 仍然是 leader。 如果有提交的 pending 事务,则服务器不会怀疑该领导者。 如果待处理队列为空,则领导者需要发出一个空事务来提交并把 `sync` 置于该事务之后。 它具有很好的属性,即当领导者处于负载状态时,不会生成额外的广播流量。 在我们的实现中,设置 timeout 是为了使 leader 在 follower 放弃他们之前意识到他们不再是 leader,因此它们就不会发布空事务。 -ZooKeeper 按照 FIFO 的顺序处理客户端的请求。返回值包括对应的 `zxid` 。即使是没有请求中间的心跳,服务端也会带上服务端对应的 zxid 给客户端。当一个客户端连接到一个新的服务器时,新服务器通过检查客户端的最后一个 zxid 保证 ZooKeeper 的 zxid 至少和客户端的 zxid 一样新。如果客户端的 zxid 和服务端的一样新,服务器在 zxid 赶上客户端之前不会建立 session。客户端保证能找到一台具有最新系统视图的服务器,因为客户端只能看到已经被复制到大多数 ZooKeeper 服务器的更改。 此行为对于保证 durability 很重要。 +ZooKeeper 按照 FIFO 的顺序处理客户端的请求。返回值包括对应的 `zxid` 。即使是没有请求中间的心跳,服务端也会带上服务端对应的 zxid 给客户端。当一个客户端连接到一个新的服务器时,新服务器通过检查客户端的最后一个 zxid 和自己的最后一个 zxid 来确保它的 ZooKeeper 数据视图至少和客户端的数据视图一样新。如果客户端的数据视图比服务器的数据视图要新,服务器在赶上客户端之前不会与客户端建立 session。客户端保证能找到一台具有最新系统视图的服务器,因为客户端只能看到已经被复制到大多数 ZooKeeper 服务器的更改。 此行为对于保证 durability 很重要。 为了检测客户端 session 的 failure, ZooKeeper 使用了 timeout。如果在 timeout 时间内,没有其他服务器收到一个 client session 对应的消息,即判定为 failure。如果客户端足够频繁地发送请求,则无需发送任何其他消息。 否则,客户端会在活动不足时发送心跳消息。 如果客户端无法与服务器通信以发送请求或心跳,则它将连接到其他ZooKeeper服务器以重新建立其会话。 为了防止会话超时,ZooKeeper客户端库在会话闲置了*s/3* ms后发送了心跳信号,如果在*2s/3* ms内未收到服务器的消息,则切换到新服务器,其中*s*是 session timeout(以毫秒为单位)。