Zookeeper内部构件


简介

本文档包含Zookeeper内部运作的资料。到目前为止,它讨论这些主题:

  • 原子广播
  • 日志

原子广播

Zookeeper的核心是一个保持所有服务同步的原子消息系统。

保证,属性和定义

Zookeeper通过下面的内容使用消息系统提高特殊保证:

可靠的交付

如果消息m由一个服务器投递,它最终将由所有服务投递。

总序

如果a消息在b消息之前由一个服务器投递,所有服务器对a消息的投递都在b之前。如果a和b是传递消息,a将在b之前投递或者b在a之前投递。

因果顺序

如果消息b在消息a之后发送,a由b的发送者投递,a的顺序一定在b之前。如果发送者在发送b之后发送c,c的顺序一定在b之后。

Zookeeper消息系统还需要是高效、可靠和易于实现和维护的。我们大量使用消息,所以我们需要系统能够处理每秒数以千计的请求。虽然我们可能需要至少K+1个正确的服务发送新消息,我们必须能够从相关故障恢复,如断电。当我们实现系统时我们只有很少的时间和稀缺的工程资源,所以我们需要一个易接近的工程和易于实现的协议。我们发现我们的协议满足所有的这些目标。

我们的协议假定我们可以在服务器间构建点对点FIFO(先入先出)通道。而类似的服务通常假定消息投递可以丢失和重排序消息。我们FIFO通道的设想是对使用TCP通讯非常实用的。特别的是我们依靠下面的TCP性质:

有序传递

数据在相同顺序里投递并发送,消息m在所有在m之前投递的消息发送之后投递。(这个推论是如果消息m丢失,m消息以后的所有消息都会丢失)

关闭之后无消息

一旦FIFO通道关闭,将不会再从它接收消息。

FLP证明在异步分布式系统里如果故障不能取得共识。为了确保在面对故障时取得共识我们使用超时时间。然而,我们依靠时间判断活性而不是正确性。所以,如果超时时间停止消息系统可能会挂,但是它不会违反它的保证。

描述Zookeeper消息协议时我们会谈论数据包、建议和信息:

数据包

通过FIFO通道发送的字节序列

建议

一个单位的协议。建议通过交换数据包获得一致。多数的建议包含信息,然而NEW_LEADER建议是不对应消息的建议实例。

信息

序列字节是原子广播到所有服务。信息放入一个提议并在投递之前同意。

如上所述,Zookeeper保证消息的总序,并且他还保证提议的总序。Zookeeper通过zxid暴露总序。所有的提议将会有一个zxid标记当它被提议并准确的反应总排序。提议被发送到所有Zookeeper服务并提交当法定人数承认提议的时候。如果提议包含一个信息,当提交提议时信息将被投递。确认的意思是服务器记录了这个提议道持久化存储。我们的法定人数有要求,任何一对法定人数必须只是有一个服务。我们通过要求所有法定人数的大小(n/2+1)确保,这里的n是组成Zookeeper服务的服务器数量。

zxid有两个部分:时代和一个计数器。在我们的实现中zxid是一个64位的数字。我们使用高位32为记录时代,低位32位记录计数器。因为它有两个部分代表zxid,一个数字和一堆证书,(epoch,count)。epoch数字代表领导阶层的变化。每当一个新领导产生,它会获得自己的epoch数字。我们有一个简单的算法分配唯一的zxid:领导者简单的增加zxid去为每个提议获得唯一的zxid。领导活动将保证只有一个领导使用指定的epoch,所有我们简单的算法保证每个提议将由一个唯一id。

Zookeeper消息由两个阶段组成:

领袖激活

在这个阶段一个领袖建立系统的正确状态并准备开始提出建议。

活动消息

在这个阶段一个领袖接收信息到建议并协调消息投递。

Zookeeper是一个全盘协议。我们不关注个人建议,而把协议流看作一个整体。我们严格的顺序可让我们有效的并大大简化我们的协议。领袖激活体现这个整体概念。一个领袖变为活动的只在法定追随者已经同步了领导者,他们有相同的状态。这个状态有所有的提议构成,领袖认为已经提交了并且提议追溯领袖,NEW_LEADER提议。

领袖激活

领袖激活包括领导者选举。当前在Zookeeper我们有两个领导者选举算法:LeaderElection和FastLeaderElection(AuthFastLeaderElection是FastLeaderElection的一个变种,它用UDP可让服务器之心给一个简单的授权形式避免IP欺骗)。Zookeeper消息传递不关心选举的精确算法,只有以下的持有:

领导者见过所有追随着的最高zxid。

  • 服务器的法定人数已经提交到后续的领导者。
  • 只有这个两个要求,第一,追随者中最高的zxid需要坚持正确的操作。第二个要求,追随者的法定人数只需要保持高概率。我们将重检查第二个要求,所以如果在领导者选举或之后发生故障且法定人数丢失,我们将通过放弃领袖激活恢复并运行另一个选举。

领导者选举之后将指定一个单独的服务作为领导者并开始等待追随者连接它。其余的服务将尝试连接到领导者。领导者将通过发送他们丢失的提议同步追随者,或者如果追随者丢失太多提议,它将发送一个完整的状态快照到追随者。

有一个极端状况,一个有提议的追随者,U,领导者没有看到。提议在顺序中看到,所有U的提议将由一个比领导者更大的zkid。因为提交的提议一定要让服务器的法定人数看到,并且服务器法定人数选举领导者看不到U,你的提议还没提交,所有他们可以被丢弃。当追随者连接的领导者,领导者会告诉追随者丢弃U。

新的领导者建立一个zxid开始使用新的提议,e,最大的zxid看到并设置下一个zxid使用(e+1,0),领导者同步追随者之后,它提出NEW_LEADER提议。一旦NEW_LEADER提议提交,领导者将激活并开始接受和发行提议。

这一切听起来复杂,但这是领导者激活过程中的基本规则:

  • 追随者确认NEW_LEADER提议在它同步leader之后。
  • 追随者只从单个服务确认给定的zxid的NEW_LEADER提议。
  • 新领导者将提交NEW_LEADER提议,当法定人数的追随者确认时。
  • 追随者将提交从领导者接收到的任何状态,当NEW_LEADER提议提交时。
  • 新领导者将不接收新提议,直到NEW_LEADER提议完成提交。

如果领导者选举异常终止,我们没有问题因为NEW_LEADER提议不会提交,因为领导者没有法定人数。当这个发生时,领导者和剩下的追随者会超时并回到领导者选举。

活动通知

领导者激活做了所有的重担。一旦领导者加冕它可以启动爆破提议。只要他保持领导者没有其他领导者可以出现,因为没有其他领导者能够获得追随者法定人数。如果新领导者出现,意味着领导者丢失了法定人数,并且新的领导者将清理任何混乱的东西。

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

所有的通讯通道都是先进先出的,所有每件事都是有序的。特别的是下面的操作约束:

  • 领导者使用相同的顺序发送提议所有的追随者。此外,这个顺序遵循请求接收的顺序。因为我们使用FIFO通道,意味着追随者也在顺序中接收提议。
  • 追随者有序的处理他们接收到的消息。这意味着消息将在顺序中确认并且领导者将从追随者有序的接收确认,由于FIFO通道。它还意味着如果消息$m$已经写入到非易失的存储,所有在$m$消息之前消息也已经写入到非易失性的存储。
  • 领导者将发行一个COMMIT到所有的追随者,一旦法定的追随者确认消息。因为消息在顺序中确认,COMMIT将被领导者接收的顺序发送。
  • COMMITs被有序的处理。追随者投递一个提议消息当提议被提交时

总结

它为什么工作?特别的是,为什么提议被新领导者赞成总数包含任何已经提交的提议?首先,所有提议有一个唯一zxid,所有不像其他的协议,我们从没必要担心出现两个相同的zxid;追随者有序的看到并记录提议;提议有序的提交;在一个时间点只有一个活动的领导者因为在特定时间追随者只追随单独的领导者;新领导者从之前的epoch看到所有提交的提议因为它从法定数量的服务已经看到最高的zxid;上一个时代任何没有提交的提议将由新领导者提交在它活动之前。

比较

这不仅仅是Multi-Paxos吗?不是,Multi-Paxos需要某种程度上的保证,只有一个单独的协调者。我们不指望这样的保证。反而我们使用领导者激活去恢复。

这不只是Paxos吗?你的活动通知阶段看上去仅仅是Paxos的第二阶段?实际上,到我们的活动消息看起来像两阶段提交而不需要处理终止。活动通知从某种意义上看是不同的。如果我们不维持严格的数据包FIFO顺序,这一切面临崩溃。另外,我们的领导者激活阶段从这方面看也是不同的。特别的,我们使用的时期可让我们跳过没提交的提议块并不关心给定zxid的重复提议。

法定人数

原子性广播和领导者选举使用法定人数的概念保证系统的一致性视图。默认的,Zookeeper使用大多数的法定人数,这意味着每个协议投票的发生要求多数人投票。一个实例是确认领导者提议:领导者值可以提交一旦接收到法定人数的服务确认。

日志

Zookeeper使用slf4j作为日志抽象层。版本1.2开始选择log4j作为最终的日志实现。为了更好的嵌入支持,计划在将来放弃决定选择,最终的日志实现交给用户。因此,在代码里总是使用slf4j api写日志生命,而不是在运行是配置log4j。注意slf4j没有FATAL级别,前面的消息在FATAL级别已经移动到ERROR级别。Zookeeper的log4j配置信息,查看Zookeeper管理员指南的日志章节。

开发者指引

请跟随slf4j手册当在代码中建立日志声明时。还要阅读FAQ on perfomance,创建日志声明时。补丁审核员将寻找下面的:

正确级别的日志

slf4j日志有若干个级别。正确的选择非常重要。为了更好的降低严重程度:

  • ERROR级别指定可让程序仍然继续运行的错误事件
  • WARN级别表示潜在的有害情况
  • INFO级别表示报告的消息,强调在粗粒度级别应用程序的进展。
  • DEBUG级别表示细粒度的报告事件,有利于调试应用。
  • TRACE级别表示比DEBUG更细粒度的报告事件。

Zookeeper运行在生产环境通常使用INFO级别或更高级别的日志消息输出到日志。

使用标准的slf4j风格

静态消息日志

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

需要创建参数化的消息时,使用格式化锚。

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

命名

记录器应该在使用它们的类后命名。

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

异常处理

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
}

 

马军伟
关于作者 马军伟
写的不错,支持一下

先给自己定个小目标,日更一新。