Kafka复制高级设计
在Kafka中添加复制的目的是为了更强的耐久性和更高的可用性。我们希望保证任何成功发布的消息不会丢失,并且可以被消费,即使有服务器故障。这种故障可能由机器错误,程序错误或更常见的软件升级引起。我们有以下高级目标:
- 可配置的持久性保证:例如,具有关键数据的应用程序可以选择更强的持久性,增加写入延迟,另一个产生大量软状态数据的应用程序可以选择更弱的持久性,但更好的写入响应时间。
- 自动复制管理:我们希望简化对代理服务器的副本分配,并能够增量式增长集群。
这里主要有两个问题需要解决:
- 如何将分区的副本均匀分配给代理服务器?
- 对于给定的分区,如何将每条消息传播到所有副本?
副本展示位置
初始展示位置
(仅创建主题,根据当前活动代理(手动创建主题命令)进行决策;重新平衡命令(基于新群集拓扑的重新计算分配,每个分区都移动)并进行移动)
我们首先使用管理api创建初始套经纪人:
然后,我们使用另一个管理api创建一个新主题:
之后,以下信息将在zookeeper中注册:
- 经纪人名单;
- 主题列表和每个主题的分区列表。
为了更好的负载均衡,我们要过度分区一个主题。通常,将有比服务器更多的分区。对于每个主题,我们希望在所有代理之间平均分配分区。我们对代理和分区列表的列表进行排序。如果有n个代理,我们将第i个分区分配给第(i mod n)个代理。此分区的第一个副本将驻留在此分配的代理上,并被称为此分区的首选副本。我们要放置其他副本,使得如果代理关闭,它的负载被均匀地分布到所有幸存的代理,而不是一个单一的。为了实现这一点,假设有分配给代理i的m个分区。分区k的第j个副本将被分配给broker(i + j + k)mod n。下图说明了代理broker-0到broker-4上分区p0到p14的副本分配。在此示例中,如果代理0关闭,则可以从所有剩余的4个代理提供分区p0,p1和p2。我们将关于ach分区的副本分配的信息存储在Zookeeper中。
在线增量添加代理
我们希望能够使用像下面这样的管理命令来递增地增加代理集。
当添加新代理时,我们将自动将一些分区从现有代理移动到新代理。Out目标是最小化数据移动量,同时在每个代理上保持平衡负载。我们使用独立的协调器进程来进行重新平衡,算法如下。
离线经纪人
我们还希望通过使用像下面这样的管理命令使现有的代理集离线来支持缩减集群。
这将为当前托管在broker-1上的分区启动重新分配过程。一旦完成,代理1将离线。此命令还将删除代理1的状态更改路径
数据复制
我们希望允许客户端选择异步或同步复制。在前一种情况下,一旦到达1个副本,就会确认要发布的消息。在后一种情况下,我们将尽最大努力确保消息仅在到达多个副本后才被确认。当客户端尝试将消息发布到主题的分区时,我们需要将消息传播到所有副本。我们必须决定:
- 如何传播消息;
- 在我们向客户端确认之前有多少个副本接收到该消息;
- 当副本崩溃时会发生什么?
- 当失败的副本再次回来时会发生什么。
我们在第2.1节中介绍现有的复制策略。然后我们分别在2.2节和2.3节描述我们的同步和异步复制。
相关工作
有两种常见策略用于保持副本同步,主备份复制和基于仲裁的复制。在这两种情况下,一个副本被指定为领导者,其余的副本被称为跟随者。所有写请求通过领导者,领导者将写入传播给跟随者。
在主要备份复制中,领导等待,直到在确认客户端之前在组中的每个副本上完成写入。如果其中一个副本已关闭,则主管将其从当前组中删除,并继续写入剩余的副本。如果失败的副本返回并赶上领导者,则允许其重新加入该组。使用f副本,主 - 备份复制可以容忍f-1故障。
在基于法定人数的方法中,领导等待,直到大多数副本上的写入完成。即使某些副本已关闭,副本组的大小也不会更改。如果有2f + 1个副本,基于仲裁的复制可以容忍f个副本故障。如果领导失败,则需要至少f + 1个副本来选择新的领导。
在两种方法之间存在权衡:
- 基于仲裁的方法具有比主要 - 备份更好的写入延迟。任何副本中的延迟(例如,长GC)会增加后者的写入延迟,但不会延长前者。
- 给定相同数量的副本,主要 - 备份方法容忍更多的并发故障。
- 复制因子2对于主要备份方法很好。在基于仲裁的复制中,两个副本都必须启动才能使系统可用。
我们选择Kafka中的主要备份复制,因为它容忍更多的故障,并与2个副本很好地工作。当一个副本崩溃或变慢时,可能会出现打嗝。然而,这些是相对罕见的事件,并且通过调整各种超时参数可以减少打嗝时间。
同步复制
我们的同步复制遵循典型的主要备份方法。每个分区具有n个副本,并且可以容忍n-1个副本故障。其中一个副本被选为领导者,其余的副本是追随者。领导者维护一组同步副本(ISR):一组完全赶上领导者的副本。对于每个分区,我们在Zookeeper中存储当前的leader和当前的ISR。
每个副本将消息存储在本地日志中,并在日志中维护几个重要的偏移位置(如图1所示)。日志末尾偏移(LEO)表示日志的尾部。高水印(HW)是最后提交的消息的偏移。每个日志会定期同步到磁盘。刷新偏移之前的数据被保证保留在磁盘上。正如我们将看到的,刷新偏移可以在HW之前或之后。
写
要将消息发布到分区,客户端首先从Zookeeper找到分区的leader,并将消息发送到leader。领导者将消息写入其本地日志。每个追随者经常使用单个套接字通道从领导者拉出新消息。这样,追随者接收与写在领导者中的相同的顺序的所有消息。跟随器将每个接收到的消息写入其自己的日志,并将确认发送回领导者。一旦领导从ISR中的所有副本接收到确认,则该消息被提交。领导者提前HW并向客户端发送确认。为了更好的性能,每个跟随器在消息写入存储器之后发送确认。因此,对于每个提交的消息,我们保证消息存储在内存中的多个副本中。然而,但是不能保证任何副本都将提交消息持久化到磁盘。鉴于相关的故障相对较少,这种方法使我们在响应时间和耐久性之间有一个很好的平衡。在未来,我们可能会考虑增加提供更有力保证的选项。领导者还定期向所有追随者广播HW。广播可以捎带在来自跟随者的获取请求的返回值上。有时,每个副本将其HW检查到其磁盘。领导者还定期向所有追随者广播HW。广播可以捎带在来自跟随者的获取请求的返回值上。有时,每个副本将其HW检查到其磁盘。领导者还定期向所有追随者广播HW。广播可以捎带在来自跟随者的获取请求的返回值上。有时,每个副本将其HW检查到其磁盘。
阅读
为简单起见,读取总是由领导提供。只有到HW的消息才会暴露给读取器。
故障场景
跟随器故障
在配置的超时时间之后,领导者将从其ISR中丢弃失败的跟随者,并且写入将在ISR中的剩余副本上继续。如果失败的跟随者回来,它首先将其日志截断到最后一个检查点的HW。然后它开始赶上所有的消息,在其HW之后的领导。当跟随者完全赶上时,领导者将其添加回当前的ISR。
领导失败
有3例领导失败,应该考虑 -
- 领导者在将消息写入其本地日志之前崩溃。在这种情况下,客户端将超时并重新发送消息到新的领导。
- 领导者在将消息写入其本地日志之后,但在将响应发送回客户端之前崩溃
- 原子性必须得到保证:所有副本都写消息,或者没有
- 客户端将重试发送消息。在这种情况下,系统应该理想地确保消息不被写入两次。也许,其中一个副本已经将消息写入其本地日志,承诺它,它被选为新的领导者。
- 发送响应后,引导程序崩溃。在这种情况下,将选择新的领导者并开始接收请求。
当发生这种情况时,我们需要执行以下步骤来选择新的领导者。
- ISR中的每个存活的副本在Zookeeper中注册它自己。
- 首先注册的副本成为新的领导者。新领导者选择其LEO作为新硬件。
- 每个副本在Zookeeper中注册一个监听器,以便它被通知任何领导更改。每当一个副本被通知一个新的领导:
- 如果副本不是新的领导者(它必须是追随者),它会将其日志截断到其HW,然后开始赶上新领导者。
- 领导等待,直到ISR中的所有幸存的副本已经赶上或配置的时间已经过去。领导者将当前的ISR写入Zookeeper,并为读取和写入打开自身。
(注意,在ISR为空时的初始启动期间,任何副本都可以成为领导者。
异步复制
为了支持异步复制,领导者可以在完成将消息写入其本地日志后确认客户端。唯一的警告是,在追赶阶段,追随者可能必须在其HW之前截断数据。由于复制是异步的,因此不能保证提交消息能够承受任何代理失败。
开放式问题
- 在第二类领导者失败中如何保证原子性?
- 我们如何避免同一分区的多个领导者的问题?
- 如果代理在多个机架,如何保证至少一个副本到不同的机架?
Kafka复制详细设计
以下是详细实施的不同建议。在0.8中,实现是基于v3提议。