引言
这个文档包含关于ZK内部工作的信息。目前为止,它讨论了这些主题:
- 原子广播
- 日志
原子传播
ZK的核心是一个原子的通信系统,它使所有的服务端保持同步。
保证、属性和定义
通过使用ZooKeeper的通讯系统提供具体保证如下:
可靠传递
如果一个消息,m,被一个服务端传递,它将最终被所有的服务端传递。
总序
如果一个消息被一个服务端在消息b之前传递,a将被所有的服务端在b之前被传递。如果a和b是被传递的消息,要么a在b之前被传递,要么b在a之前被传递。
因果顺序
如果一个消息b在被一个b的发送者已经传递a之后被发送,a必须被排在b前面。如果一个发送者发送c在发送b之后,c必须排在b之后。
ZK的通信系统也需要是高效的,可靠的,和容易实现和维护。我们大量使用通信,所以我们需要这个系统每秒可以处理成千上万的请求。尽管我们可以要求到少k+1个正确的服务端发送新消息,我们必须能够从例如停电的相关的失败中恢复出来。当我们实现这个系统我们很少的时间和很少的工程资源,所以我们需要一个对工程师来说可用的协议并且很容易实现。我们发现我们的协议满足所有的目标。
我们的协议假设我们可以构造在服务端之间点到点的FIFO通道。同时类似的服务端通常认为消息传递可能丢失或重排序消息,我们的FIFO通道的假设是很实用的假定我们使用TCP来通信。特别地我们依赖TCP如下的属性:
有序的传递
数据以它被发送的顺序传递并且一个消息m被传递只有在m之前发送的所有消息已经被传递之后。(这个推论是如果消息m丢失所有在m之后的消息将被丢失)。
在关闭之后没有消息
一旦一个FIFO通道被关闭,从它那里就没有消息被接受。
FLP证明共识不能够实现在异步的分布式系统,如果失败是可能的。为了确保我们实现共识在失败出现的时候我们使用超时。然而,我们依赖时间为了生存而不是为了共识。所以,如果超时停止工作(时钟故障的例子)通信系统可能挂起,但是它将不会违反这个保证。
当我们描述ZK消息协议,我们将会讨论数据包,提议,和消息:
数据包
通过FIFO通道发送的字节序列
提议
协议单位。提议通过在ZK服务端的法定人数之间交换数据包来达成一致。大部分提议包含消息,然而NEW_LEADER提议是一个提议的样例它不对应一个消息。
消息
将要原子传播到所有ZK服务端的的字节序列。消息放入一个提议当中并且在被传递之前同意。
如上所述,ZK确保一个消息的总顺序,并且它也确保提议的总顺序。ZK暴露总顺序使用一个ZK事务id(zxid)。所有提议将被标识一个zxid当它被提出并且确切地反应总顺序。提议被发送到所有ZK服务端并且当他们的法定人数确认了这一提议将被提交。如果一个提议包含一个消息,这个消息将被传递当提议被提交的时候。确认意为着服务端已经记录这个提议到永久存储。Our quorums have the requirement that any pair of quorum must have at least one server in common(意思就是法定人数有多数可用,至少是n/2+1个法定人数)。我们确保这个通过要求所有法定人数有(n/2+1)个,这里的n是组成ZK服务的服务端数量。
zxid有两部分:时期和计数器。在我们的实现中zxid是一个64位的数字。我们使用高32位来表示时期,低32位来表示计数器。因为它有两部分表示zxid,两部分都是数字并且作为一对整数(时期,计数器)。时期数字代表领导者改变。每当一个新的领导人上台,它将有它自己的时期数字。我们有一个简单的逻辑来给每一个提议分配一个唯一的zxid:领导者简单地增加zxid来为每一个提议获取一个唯一的zxid。领导者激活将确保只有一个领导者使用一个给定的时期,所以我们简单的逻辑确保每一个提议将有一个唯一的id。
ZK通信包含两阶段:
领导者激活
在这个阶段一个领导者建立这个系统的正在状态并且准备开始提出建议。
消息传递
在这个阶段一个领导者接受提议的消息并且协调消息传递。
ZK是一个整体协议。我们不关注个别的提议,而是把提议的流作为一个整体。我们严格的顺序性使我们能够有效地做到这一点并且大大地简化了我们的协议。领导者激活体现了这个整体概念。一个领导者变得活跃只当一个追随者(领导者也算是一个追随者,你可以总是投票给自己)已经和领导者同步过,他们拥有相同的状态。这个状态包含所有领导者相信已经被提交的提议和追随领导者的提议,也就是NEW_LEADER提议。(希望你也正在思考,真的领导者相信已经被提议的提议包括所有真的已经被提交的提议?答案是是的。下面,我们弄清楚为什么)。
领导者激活
领导者渡海包括领导者选举。我们现在在ZK中有两个领导者选举逻辑:LeaderElection和FastLeaderElection(AuthFastLeaderElection 是一个FastLeaderElection的变种,它使用UDP和允许 服务端执行简单形式的谁来避免IP欺骗)。ZK通信不关心选举领导者的具体方法只要以下拥有:
- 领导看到所有追随者的最高zxid
- 服务端法定人数致力于追随领导者
在这两个要求中只有第一个,在追随者中的最大zxid需要保持正确的操作。第二个要求,追随者的法定人数,只需要保持高概率。我们正要重新检测第二个要求,所以如果在领导者选择之间或在领导者选举之后发生一次失败并且法定人数丢失,我们将恢复通过取消领导者激活并且运行另一个选举。
在领导者选举之后一个服务端将被分配为领导者并且开始等待追随者连接。剩余的服务端将试图连接到领导者。领导者将同步追随者通过发送任何他们丢失的,或者如果一个追随者错误太多提议,它将发送一个完整的快照给追随者。
有一种情况一个追随者的提议,U,没有被领导者看到。提议有序地被看到,所以人民提议U将有一个比领导者看到的zxid更高的zxid。追随者必须在领导者到达之后到来,否则追随者将被选举成领导者因为它已经看到一个更高的zxid。因为提交的提议必须被服务端的法定人数看到,并且选举了领导者的服务端的法定人数没有看到U,你的提议没有被提交,所有他们将被丢弃。当追随者连接到领导者,领导者将告诉追随者丢掉U。
一个新的领导者通过获取时期(epoch)建立一个为了新的提议开始使用的zxid。也就是他已经看到的最高的zxid并且设置下一个使用的zxid为(e+1,0),在领导者和追随者同步之后,它将发出一个NEW_LEADER提议。一旦NEW_LEADER提议已经被提交,领导者将激活并且开始接收和发起提议。
它们听起来都很复杂,但是这里是领导者激活过程中的操作的基本规则:
- 追随者将响应NEW_LEADER提议工它已经和领导者同步之后。
- 追随者将只响应来自一个服务端带着给定的zxid的NEW_LEADER提议。
- 新的领导者将提交NEW_LEADER提议当追随者的法定人数已经响应它。
- 当NEW_LEADER提议被提交之后追随者将提交任何它从领导者收到的状态
- 新的领导将不会接受新提议直到NEW_LEADER提议已经被提交。
如果领导者选举错误地终止,我们不会有任何问题因为NEW_LEADER提议将不会被提交因为领导者没有法定人数。当这个发生时,领导者和任何任何剩余的追随者将超时并且重新开始领导者选举。
消息传递
领导者激活所有的沉重的起重(Leader Activation does all the heavy lifting)。一旦领导者被选举它可以开始发起提议。只要它是领导者就没有其它其它领导可以出现,因为没有其它领导者将可以有追随者的法定人数。如果一个新的领导者确实出现,它意为着这个领导者已经失去了法定人数,并且新的领导者将清理任何遗留的混乱在他的领导者激活的过程中。
ZK的消息操作和一个典型的两段式提交相似。
所有的通信通道是FIFO,所以所有事被有序地完成。特别地可以观察到如下的操作约束。
- 领导者发送提议到所有的追随者使用相同的顺序。甚至,这个序列按照请求被接受的顺序。因为我们使用FIFO通道这意为着追随者也是有序地收到提议
- 追随者处理消息以他们收到的顺序。这意为着消息将被有序地回应并且领导者有序地从追随者收到回应,因为FIFO通信。这也意为着如果消息$m$已经被写入到一个持久的存储里面,所有在$m$之前被发起的消息也已经被写入到持久存储里面。
- 领导者将发起一个COMMIT给所有的追随者一旦追随者的法定人数 已经回应了一个消息。因为消息被有序地回应,COMMIT将被领导者将以追随者收到的顺序发出。
- COMMIT被有序地处理。追随者传递一个提议消息当提议已经被提交的时候。
总结
所以到些为止。为什么它工作?特别地,为什么被新的领导者相信的一组提议总是包含任何已经被真正提交的提议?首先,所有提议有一个唯一的zxid,所以不像其它提议,我们永远不担心两个不同的值被提议带着相同的zxid;追随者(领导者也是一个追随者)有序地看到和记录提议;提议被有序地提议;一个时间有一个活跃的领导者追随者只追随一个领导者在一个时间;一个新的领导者已经看到所有被提交的提议从先前的纪元(epoch)开始因为它已经 看到服务端的法定人数中最高的zxid;任何被新的领导者在先前的纪元(epoch)看到的没被提交的提议将被提交被这个领导者在它变活跃之前。
比较
这不就是Multi-Paxos么?不是的,Multi-Paxos要求 假设只有一个协调者。我们不依赖这样的保证。相反地我们使用领导者激活来从领导者改变在恢复或者老的领导者相信它仍然活跃。
这是不是Paxos?你的消息传递阶段看起来像一个Paxos的2阶段?事实上,对我们来说消息传递看起来像一个不需要处理取消的2阶段提交。消息传递有整个提议顺序性的要求,在这个意义上说它和这两者都不同。如果我们不维护所有数据包严格的FIFO顺序,这一切都崩溃了。同时,我们的领导者激活阶段也和他们两个不同。告别地,我们的纪元的使用允许我们跳过没有提交提议的块并且不用担心对于一个zxid会有重复的提议。
法定人数
原子传播和领导者选举使用法定人数的概念来保证系统的一致性视图。默认地,ZK使用多数法定人数,这意为着每一个在这些协议中发生的投票需要一个大多数来投票。一个例子是承认一个领导者的建议:领导者可以提交一旦它从服务端的法定人数收到一个确认。
如果我们提取这一性质,我们真的需要使用大多数,我们实现这一点,我们只需要保证一组使用的进程通过投票来检测一个操作(也就是说,确认一个领导者提议)。使用大多数保证这样一个属性。然而有砣不同与大多数的的方法来构建法定人数。例如,我们可以分配权重给服务端的投票,来说明一些服务端的投票更重要。为了获取一个法定人数,我们得到足够的投票结果所有投票的权重的总全比所有权重的总全的一半要大。
一个不同的构造使用权重并且在wide-area(co-locations)部署是有用的是一个分层次法定人数。对于这个构造,我们把服务端分为不相交的组并且分配权重来处理。为了组成一个法定人数,我们不得不从一个一个组G的大多数获取足够的服务端,对于G中的每一个组g,g中的投票的总数比g中的权重的总数的一半要大。有趣的是,这个构造允许更小的法定人数。例如,如果我们有9个服务端,我们把他们分为三组,并且给每一个服务端分配一个权重1,那么我们可以组成一个大小为4的法定人数。注意,处理的两个字集从每一个组的大多数组成的每一个大多数服务器必需有一个非空的交集。这是合理的期望一个co-locations的大多数将有很大的概率有大多数可用的服务端。
对于ZK,我们提供给用户配置服务端来使用大多数法定人数,权重,或者分层次的组的能力。
日志
ZK使用slf4j作为日志的抽象层。现在版本1.2的log4j被选择作为最终的日志实现。为了更好的嵌入支持,在未来计划把最终日志实现的选择扔给用户。因些总是在代码里使用slf4jAPI来写日志语句,但是在运行时配置log4j怎么记录日志。注意slf4j没有FATAl级别,在以前的FATAl级别的消息已经被移到ERROR级别。对于ZK关于配置log4j的信息参考ZooKeeper Administrator's Guide的Logging部分。
开发者指南
请按照slf4j manual当在代码中创建日志语句的时候。当创建日志语句的时候同时读FAQ on performance,补丁的审稿人将寻找下面:
在正确的级别上记录日志
在slf4j中有几个日志级别。选择正确的一个非常重要。为了更高的较低的严重程度:
- ERROR级别指定错误事件可能仍然允许应用继续运行。
- WARN级别指示潜在的有害的情况。
- INFO级别指示信息的消息突出程序过处理以粗粒度地。
- DEBUG级别指示细粒度的信息事件对调试应用程序是最有用的。
- TRACE级别指示比DEBUG更详细的信息事件。
ZK通常以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 }