ZooKeeper Internals

Introduction

本文档包含有关 ZooKeeper 内部工作的信息。到目前为止,它讨论了以下主题:

Atomic Broadcast

ZooKeeper 的核心是原子消息系统,该系统使所有服务器保持同步。

保证,属性和定义

ZooKeeper 使用的消息传递系统提供的特定保证如下:

  • 可靠的传送 :如果消息 m 由一台服务器传送,则最终将由所有服务器传送。

  • 总订单 :如果一台服务器在消息 b 之前传递了一条消息,那么所有服务器将在 b 之前传递 a。如果 a 和 b 是传递的消息,则 a 将在 b 之前传递,或者 b 将在 a 之前传递。

  • 因果 Sequences :如果消息 b 在 b 的发送者发送了消息 a 之后发送,则必须在 b 之前对 a 进行排序。如果发送方在发送 b 之后发送 c,则必须在 b 之后 Orderc。

ZooKeeper 消息传递系统还需要高效,可靠并且易于实施和维护。我们大量使用消息传递,因此我们需要该系统每秒能够处理数千个请求。尽管我们可能至少需要 k 1 台正确的服务器才能发送新消息,但我们必须能够从相关故障(例如断电)中恢复。当我们实施该系统时,我们的时间很少,工程资源也很少,因此我们需要一种易于工程师使用的协议。我们发现我们的协议满足了所有这些目标。

我们的协议假设我们可以在服务器之间构造点对点 FIFO 通道。尽管类似的服务通常会假设消息传递会丢失或重新排序消息,但考虑到我们使用 TCP 进行通信,我们对 FIFO 通道的假设非常实用。具体来说,我们依赖于 TCP 的以下属性:

  • 有序传递 :仅按照在 m 传递之前发送的所有消息发送后的相同 Sequences 传递数据,并传递消息 m。 (这必然导致如果消息 m 丢失,则 m 之后的所有消息都将丢失.)

  • 关闭后无消息 :关闭 FIFO 通道后,将不会收到任何消息。

FLP 证明,如果可能发生故障,则无法在异步分布式系统中达成共识。为了确保在出现故障时我们达成共识,我们使用超时。但是,我们依靠时间来保持活力而不是为了正确。因此,如果超时停止工作(例如时钟故障),则消息系统可能会挂起,但不会违反其保证。

在描述 ZooKeeper 消息传递协议时,我们将讨论数据包,提议和消息:

  • 包**:通过 FIFO 通道发送的字节序列

  • 提案 :协议单位。通过与法定的 ZooKeeper 服务器交换数据包来达成协议。大多数投标都包含消息,但是 NEW_LEADER 投标是与消息不对应的投标的示例。

  • 消息 :将以原子方式 Broadcast 到所有 ZooKeeper 服务器的字节序列。传递到提案中并已达成共识的消息。

如上所述,ZooKeeper 保证消息的总 Sequences,也保证建议的总 Sequences。 ZooKeeper 使用 ZooKeeper 事务 ID(* zxid )公开总排序。提出提案时,所有提案都将盖上 zxid,并准确反映总排序。提案将发送到所有 ZooKeeper 服务器,并在法定人数确认提案后提交。如果提案中包含一条消息,则在提交该提案时将传递该消息。确认表示服务器已将建议记录到持久性存储中。我们的法定人数要求任何一对法定人数必须至少有一个共同的服务器。我们通过要求所有仲裁具有大小( n/2 1 *)来确保这一点,其中 n 是组成 ZooKeeper 服务的服务器数量。

zxid 有两个部分:时期和计数器。在我们的实现中,zxid 是一个 64 位数字。我们将高阶 32 位用于纪元,将低阶 32 位用于计数器。因为它有两个部分,所以 zxid 既可以表示为数字,也可以表示为一对整数(* epoch,count *)。纪元数代表领导层的变化。每次新领导人上台时,都会有自己的时代号。我们有一个简单的算法为提案分配唯一的 zxid:组长只需增加 zxid 即可为每个提案获得唯一的 zxid。 领导力激活将确保只有一名领导者使用给定的纪元,因此我们的简单算法可确保每个提案都具有唯一的 ID.

ZooKeeper 消息传递包括两个阶段:

  • 领导激活 :在此阶段,领导构建正确的系统状态,并准备开始提出建议。

  • 主动消息传递 :在此阶段,领导者接受消息以提议和协调消息传递。

ZooKeeper 是一个整体协议。我们不关注单个提案,而是整体来看提案流。我们严格的 Order 使我们能够有效地做到这一点,并大大简化了我们的协议。领导力的激活体现了这一整体概念。仅当法定人数的追随者(领导者也视为追随者.您可以随时为自己投票)与领导者同步时,领导者才会处于活动状态。此状态包含领导者认为已落实的所有提案以及跟随领导者的提案 NEW_LEADER 提案。 (希望您正在考虑自己,领导者认为已落实的建议集是否包括所有实际已落实的建议?答案是.在下面,我们明确了原因.)

Leader Activation

领导者激活包括领导者选举。目前,ZooKeeper 中有两种领导者选举算法:LeaderElection 和 FastLeaderElection(AuthFastLeaderElection 是 FastLeaderElection 的变体,它使用 UDP 并允许服务器执行一种简单的身份验证形式以避免 IP 欺骗)。只要满足以下条件,ZooKeeper 消息传递并不关心选举领导者的确切方法:

  • 领导者在所有关注者中看到了最高的 zxid。

  • 法定人数的服务器已承诺跟随领导者。

在这两个要求中,只有跟随者需要保持最高的 zxid,才能正确操作。第二个要求是一定数量的追随者,只需要极高的把握即可。我们将重新检查第二个要求,因此,如果在领导者选举期间或之后发生故障并且法定人数丢失,我们将通过放弃领导者激活并再次进行选举来恢复。

选举领导者后,会将一台服务器指定为领导者,并开始 await 关注者进行连接。其余服务器将尝试连接到领导者。领导者将通过发送追随者丢失的任何提案来与关注者同步,或者如果追随者缺少太多提议,则它将状态的完整快照发送给追随者。

在一个极端的情况下,有领导者看不到的提议 U 的跟随者到达。提案按 Sequences 显示,因此 U 的提案的 zxid 高于领导者看到的 zxid。跟随者一定是在领导人选举之后到达的,否则跟随者将被视为领导者,因为它具有较高的 zxid。由于必须通过一定数量的服务器来查看已提交的建议,并且当选领导者的服务器的法定数量没有看到 U,因此您的建议尚未被提交,因此可以将其丢弃。当跟随者连接到领导者时,领导者将告诉跟随者丢弃 U。

新领导者通过获取领导者与关注者同步后获得的最高 zxid 的时期 e 并将下一个要使用的 zxid 设置为(e 1,0),来构建 zxid 以开始用于新提案。它将提出一个 NEW_LEADER 提议。提交 NEW_LEADER 提案后,领导者将激活并开始接收和发布提案。

听起来很复杂,但是这是领导者激活期间的基本操作规则:

  • 追随者与领导者同步后,其追随者将对 NEW_LEADER 提议进行确认。

  • 关注者只能从单个服务器使用给定的 zxid 来确认 NEW_LEADER 提议。

  • 当达到一定数量的关注者确认后,新领导者将提交 NEW_LEADER 提议。

  • 当 NEW_LEADER 提议为 COMMIT 时,关注者将落实从领导者收到的任何状态。

  • 在提交 NEW_LEADER 提案之前,新领导者将不接受新提案。

如果领导者选举错误地终止,那么我们就不会有问题,因为将不会提交 NEW_LEADER 提案,因为领导者将不会达到法定人数。发生这种情况时,领导者和所有其他跟随者将超时,并返回到领导者选举。

Active Messaging

领导者激活完成了所有繁重的工作。领导加冕后,他就可以开始提出建议了。只要他仍然是领导者,就没有其他领导者可以出现,因为没有其他领导者能够获得一定数量的追随者。如果确实出现了新的领导者,则意味着该领导者已失去仲裁人数,并且该新领导者将清除激活她的领导者过程中留下的任何混乱情况。

ZooKeeper 消息传递的操作类似于经典的两阶段提交。

两阶段提交

所有通信通道均为 FIFO,因此所有操作均按 Sequences 进行。具体而言,遵守以下操作限制:

  • 领导者使用相同的 Sequences 将提案发送给所有关注者。此外,该 Sequences 遵循已接收请求的 Sequences。因为我们使用 FIFO 通道,所以这意味着关注者还会按 Sequences 接收投标。

  • 关注者按照收到消息的 Sequences 处理消息。这意味着,由于具有 FIFO 通道,消息将按 SequencesACK,并且领导者将按 Sequences 从跟随者接收 ACK。这也意味着,如果已将消息$ m $写入非易失性存储,则在$ m $之前建议的所有消息均已写入非易失性存储。

  • 只要有一定数量的关注者确认了消息,领导者就会向所有关注者发出 COMMIT。由于消息是按 Sequences 确认的,因此领导者将按照跟随者的 Sequences 发送 COMMIT。

  • COMMIT 按 Sequences 处理。提交提案后,关注者会发送提案消息。

Summary

所以你去了。为什么行得通?具体来说,为什么新领导人相信的一组提议中总会包含实际上已经提交的任何提议?首先,所有提议都具有唯一的 zxid,因此与其他协议不同,我们不必担心为同一 zxid 提议两个不同的值;关注者(领导者也是关注者)按 Sequences 查看和记录建议;提案按 Sequences 提交;一次只有一名活跃的领导者,因为追随者一次只能跟随一名领导者;一位新的领导者看到了上一时期所有已提交的提案,因为它从一定数量的服务器中看到了最高的 zxid。新领导者看到的来自先前时期的任何未提交的建议将在该领导者开始活动之前提交。

Comparisons

这不只是 Multi-Paxos 吗?不,Multi-Paxos 需要某种方式来确保只有一个协调器。我们不指望这种保证。取而代之的是,我们使用领导者激活来从领导者更迭中恢复过来,或者使旧领导者相信他们仍然活跃。

这不只是 Paxos 吗?您的活动消息传递阶段类似于 Paxos 的阶段 2?实际上,对我们来说,主动消息传递看起来就像两阶段提交,而无需处理中止。活动消息在这方面具有跨提案 Order 要求,因此两者均不同。如果我们不对所有数据包都执行严格的 FIFO 排序,那么所有这些数据包都会崩溃。此外,我们的领导者激活阶段与这两个阶段都不相同。特别地,我们使用历元使我们可以跳过未提交提案的块,而不必担心给定 zxid 的重复提案。

Quorums

原子 Broadcast 和领导者选举使用法定概念来保证系统的一致视图。默认情况下,ZooKeeper 使用多数法定人数,这意味着在这些协议之一中进行的每次投票都需要多数投票。一个示例是确认领导者提议:领导者只有在收到来自法定服务器的确认后才能提交。

如果我们从多数使用中提取出我们 true 需要的属性,那么我们只需要保证用于通过投票(例如,确认组长提议)来验证操作的过程组在至少一台服务器中成对相交。使用多数保证了这种性质。但是,还有其他一些不同于多数的构建仲裁的方法。例如,我们可以为服务器的投票分配权重,并说某些服务器的投票更为重要。要获得法定人数,我们需要获得足够的选票,以便所有选票的权重之和大于所有权重之和的一半。

一种使用权重并且在广域部署(同一地点)中有用的不同构造是一种分层构造。通过这种构造,我们将服务器分成不相交的组,并为进程分配权重。为了形成法定人数,我们必须从大多数组 G 获得足够的服务器,这样对于 G 中的每个组 g,来自 g 的票数之和要大于 g 权重之和的一半。有趣的是,这种构造可以实现较小的仲裁。例如,如果我们有 9 台服务器,则将它们分成 3 组,并为每台服务器分配权重 1,那么我们就可以形成大小为 4 的仲裁。请注意,进程的两个子集构成了每个进程的多数。大多数组中的每个服务器都必须具有非空交集。可以合理地预期,大多数托管服务器将以很高的概率提供大多数服务器。

借助 ZooKeeper,我们为用户提供了配置服务器以使用多数仲裁,权重或组层次结构的能力。

Logging

Zookeeper 使用slf4j作为记录的抽象层。现在选择 1.2 版中的log4j作为最终的日志记录实现。为了获得更好的嵌入支持,将来计划将选择最终日志记录实现的决定留给最终用户。因此,请始终使用 slf4j api 在代码中编写日志语句,但请配置 log4j 以了解如何在运行时进行日志记录。请注意,slf4j 没有致命级别,以前处于致命级别的消息已移至错误级别。有关为 ZooKeeper 配置 log4j 的信息,请参见ZooKeeperManagement 员指南。Logging部分

Developer Guidelines

在代码中创建日志语句时,请遵循slf4j manual。创建日志语句时,还请阅读性能常见问题解答。修补程序审阅者将寻找以下内容:

以正确级别登录

slf4j 中有多个登录级别。

选择正确的一个很重要。按从高到低的 Sequences:

  • ERROR 级别指定可能仍允许应用程序 continue 运行的错误事件。

  • 警告级别表示潜在的有害情况。

  • INFO 级别指定信息性消息,以粗粒度级别突出显示应用程序的进度。

  • 调试级别指定对调试应用程序最有用的细粒度信息事件。

  • 与调试相比,TRACE Level 指定的信息事件更细化。

ZooKeeper 通常在生产环境中运行,以便将 INFO 级别和更高(更严重)级别的日志消息输出到日志。

使用标准 slf4j 成语

静态消息记录

LOG.debug("process completed successfully!");

但是,当需要创建参数化消息时,请使用格式锚。

LOG.debug("got {} messages in {} minutes",new Object[]{count,time});

Naming

Logger 应以使用它们的类命名。

public class Foo {
    private static final Logger LOG = LoggerFactory.getLogger(Foo.class);
    ....
    public Foo() {
        LOG.info("constructing Foo");

Exception handling

try {
    // code
} catch (XYZException e) {
    // do this
    LOG.error("Something bad happened", e);
    // don't do this (generally)
    // LOG.error(e);
    // why? because "don't do" case hides the stack trace

    // continue process here as you need... recover or (re)throw
}