最近发布的Confluent Cloud和Confluent Platform 7.0引入了轻松移除Apache Kafka® Broker的功能,只需一个命令就能缩小你的Confluent Server集群。
从集群中移除Kafka代理,乍一看很简单--从我们用户的角度来看,这是一个有意的设计决定,但在引擎盖下,它被证明具有惊人的微妙性和复杂性。在Confluent,我们努力使Kafka的大规模使用尽可能简单,从你的生活中去除复杂性。
Confluent服务器注意驱动幕后的一切,包括移动分区,防止在分区被移走时放置新的分区,甚至在最后关闭经纪商
这篇博文详细介绍了从集群中安全删除几个Kafka Broker所需要的东西,以及实现这一目标的相关挑战。
为什么要删除多个Kafka Broker?
在讨论这个新功能的细节之前,首先值得澄清的是我们试图解决的问题空间。
Confluent博客的普通读者可能会注意到,删除代理的功能在Confluent平台7.0中并不完全是新的,它最早是在6.0版本中通过kafka-remove-brokers命令引入的。
在Confluent Cloud中缩减集群的同时,需要提供更好、更实用的集群缩减体验,这促使我们加强了移除操作的功能。Confluent Cloud的规模单位是CKU(Confluent Unit for Kafka),而这些单位是由多个broker组成。因此,为了提供一个无缝的降级选项,一次删除多个经纪商成为了一种必要。
不幸的是,6.0中发布的移除功能有两个明显的缺点。第一个问题是,移除功能会导致集群中的分区间歇性重复不足,因为在分区被移出之前,经纪商首先被关闭了。
第二个问题源于第一个问题--你一次只能删除一个代理,而且必须等待负载被重新平衡。否则,根据Confluent Cloud的配置值2,你会有同步复制(ISRs)不足的风险。
从技术上讲,你可以用旧的API以安全的方式删除多个经纪商,只需逐个发出删除指令。这种方法的问题是,在你已经删除一个经纪商之后,你才会对是否能删除它有一个很好的概念。例如,假设你想从一个集群中删除五个经纪商。直到你删除了第四个经纪人,你才发现你没有任何能力再去删除。更糟糕的是,删除第四个经纪人可能导致你的集群容量不足,造成复制不足的分区和无法提供数据。预先计算你可以安全删除的容量可以使你避免这种情况。
现在我们已经确定了我们想要从集群中移除的东西,让我们深入了解一下这到底是如何做到的
智能地重新分配分区
有更好的方法来从集群中移除经纪商。第一步是减少它们的负载,直到它们不再托管任何分区。
Apache Kafka在Controller Broker上公开了AlterPartitionReassignments API,允许你传入一个分区及其新的目标副本集(应该托管该分区的经纪人ID集)。一旦API被调用,Kafka控制器就会启动数据移动,在所有新的副本加入同步的副本集后,删除旧的副本--这时,重新分配就被认为是完成了。
Kafka已经提供了一个名为kafka-reassign-partitions的底层工具来帮助使用这个API,但它的使用非常棘手。用户需要手动拼接一个大的JSON文件,其中包括每个分区及其相关副本。
此外,它不能帮助用户解决非常困难的bin-packing问题,即把副本移到哪个broker。让用户手动协调他们的集群缩减操作并不理想--有很多手动错误的空间。值得庆幸的是,Confluent平台提供了自平衡集群。
在一个理想的世界里,会有一种解决方案,可以将集群的重新平衡操作从用户的手中自动化。这可以通过经纪人中的一些长期运行的进程来实现,这些进程可以跟踪集群的资源,了解它们的负载,并可以想出一个智能的方法来重新分配要删除的经纪人的分区。这个集群再平衡器将在整个集群中重新分配分区,以尽量减少对剩余经纪商的负载影响。
Confluent的自平衡集群功能正是帮助实现这一目的。平衡器是一项服务,负责执行各项功能以确保自平衡集群。它在Kafka控制器内运行,负责任何与平衡有关的功能,如bin打包和分区重新分配,以及实际的代理删除操作。通过使用每个分区的指标,它计算出一个由分区重新分配组成的计划,将将要移除的经纪人的负载均匀地分配到集群的其余部分。
然后,这些重新分配被逐步分配和解决,以避免立即用成千上万的重新分配来压倒集群。
操作持久性和状态机
在经纪商之间重新平衡分区所需的时间差异很大,而且在很大程度上受到经纪商磁盘上的数据量的影响。在一个支持Tiered Storage的集群中,如果磁盘上的数据只有100G,这可能短到一个小时。在规模的另一端,一个没有分层存储的集群,如果磁盘上包含许多TB的数据,可能需要超过一两天。
为了确保重新平衡操作保持不间断,它必须对节点故障和重新启动有弹性;代理删除操作将其进度持续到磁盘,并在任何平衡器组件故障时从最后一步重新启动。
为了让平衡器知道从哪一步开始重启操作,我们在状态机中明确地模拟了经纪人删除操作的不同阶段。下图是经纪人移除操作的快乐路径所经历的状态。
移除经纪人操作的快乐路径
在操作开始时,以及每次操作进入一个新的阶段时,平衡器组件都会保存删除操作所处的最新状态。虽然用户被要求重试经纪商移除操作的任何意外失败,但平衡器优雅地处理已知的失败情况,如组件本身被重新启动。在这种情况下,它只是从最后坚持的同一操作开始。
平衡器使用一个复制系数为3的Kafka主题来持久化操作的状态。主题内的Protobuf格式的记录使平衡器能够在系统故障时重建其整个状态,最大限度地减少恢复时间和对任何正在进行的再平衡的影响。当你正在开发的组件是像Kafka这样可靠的数据存储的一部分时,这就非常方便了!
如果平衡器由于经纪商的重启而中断,它将在启动时消耗其主题的所有内容,检测到有一个正在进行的移除操作,并简单地从它离开的地方恢复。
什么会出错?
当然,强大的软件从来都不是简单的设计。你需要考虑到各种验证和竞赛条件,在这样一个长期运行的操作中,可能会出现很多问题
初步验证
最明显和最直接的问题是容量问题--有可能在移除N个经纪商后,集群不再有必要的容量来容纳整个集群的资源。这种情况的一个更极端的版本是,删除后的集群规模小于给定分区的复制因子--例如,如果一个分区的复制因子是5,那么将集群缩减到4个经纪人的规模是不成立的。
这些明显的容量问题很容易在前期验证--在启动移除操作之前,我们首先明确地验证这些可能性,并通过计算后来被抛弃的重新分配计划来验证。
这种验证的目的是运行所有的资源分配逻辑,并确保我们能拿出一个能平均分配资源的解决方案。这也间接地确保了平衡器组件已经收集了它所需要的所有必要指标。
竞赛条件
我们想要设计的比较棘手的问题是,在经纪人移除操作的过程中,用户能够创建新的主题。
因为在我们能够安全地关闭被移除的经纪商之前,有许多动作需要发生,所以总是有这样的风险:集群的用户创建了一个新的主题,而这个新主题的一些分区副本最终出现在我们试图移除的经纪商上!这就是我们的设计。
下面是一个图表,其中列出了缩减请求的步骤,以及新的主题创建将使我们之前计算的计划无效的区域。
这可能发生在删除经纪商操作的任何时候,考虑到删除经纪商可能需要的时间长度,我们不希望把我们的用户从创建新的主题中围起来。
人们可能会想到一个天真的解决方案,即在关闭一个经纪商之前,定期检查它是否有副本,并在必要时重复分区重新分配的步骤。但这种解决方案只是缩小了可能发生竞赛条件的窗口,因为在最后一次检查之后,在代理服务器被终止之前,仍然可以立即创建一个分区。需要有一个更强有力的保证。
复制的排除法
经过彻底的设计过程,旨在以一致和可靠的方式解决上述的副本放置问题,该团队开发了一个新的功能,称为经纪人副本排除。
这是一个控制面API,允许用户将一个经纪商标记为排除在任何新的副本放置之外,基本上禁止任何新的副本被放置在上面。
在新主题或新分区创建过程中,作为自动复制分配的一部分,任何被标记为排除在外的经纪商将不会有任何新的复制放在它上面--在用户要求的包含排除在外的经纪商的显式分配情况下,会抛出一个异常。
一旦被标记,排除就会被持续存在,并且不会被移除,直到API被再次调用,并且明确地打算移除所述排除。只有在确认经纪商被关闭后,该操作才会通过删除副本排除来清理自己。
此外,为了帮助经纪商移除层的简单性并推动良好的API设计,排除API在其设计中是原子性的和空转的。
排除请求被原子化地处理。要么应用整套排除法,要么不应用--例如在试图删除一个不存在的排除法的情况下。
关机或不关机
如果你还记得本博文前面的内容,旧的删除代理API的第二个缺点是,它在重新分配开始前关闭了代理。在Confluent Cloud的背景下,同样的缺点变成了一个普遍的问题--问题变成了命令本身做了关闭。
Confluent Cloud在Kubernetes上运行时,利用了一个由多个pod组成的StatefulSet来表示Kafka集群。在这个模型中,一个Kubernetes pod就是一个Kafka代理。
由于Confluent Cloud配置的默认容器重启策略为Always ,Kubernetes会立即重启刚刚完成受控关闭过程的Kafka pod。这就不允许我们从应用层本身永远关闭Kafka pod--Kafka pod会直接启动!这与运营商模式相悖。
此外,这也违背了Kubernetes世界中的运营商模式惯例。Confluent已经通过Confluent Operator组件遵循这一模式,该组件负责管理Kafka的生命周期。
我们想要的解决方案是,让代理移除操作将所有副本从代理上移走,并确保它在放置新副本时被排除。然后,Confluent Operator将通过永久关闭代理并将其从状态集中删除来重新拾起集群收缩操作。
不幸的是,这个解决方案并不像改变移除操作那样简单,因为它永远不会发出关闭。因为移除操作已经和Confluent Platform 6.0一起发布了,而且该版本确实关闭了代理,恢复该行为将是一个向后不兼容的改变,并破坏了API的契约。
我们所做的向后兼容的改变引入了一个新的布尔标志,叫做toShutdown ,它表示删除操作是否应该继续关闭代理。这个必要的条件性行为使我们得到了最终的状态机模型,现在有两个可能的路径,因为它包含了这个分叉:shouldShutdown=true和shouldShutdown=false。
现在分叉的经纪人移除操作状态机的版本
闲置性
按照Kubernetes运营商的最佳工作实践,我们让经纪人移除操作成为empotent的。如果作为移除请求一部分的所有经纪商都已经在移除过程中,或者已经作为先前移除请求的一部分从集群中移除,那么该请求将成为一个无用的操作,并立即返回成功状态。
另一种方法--当用户试图移除正在被移除或已经被移除的经纪商时抛出异常--将无助于我们将空闲子程序作为操作员的Reconcile循环的一部分。
把它放在一起
有了这些,我们就有了在Confluent服务器内提供良好工作的多经纪商移除功能的核心构件。
简而言之,删除经纪人的操作包括以下步骤。
- 计算一个重新分配的计划作为初步验证。
- 排除被标记为移除的经纪商,使其不再有新的副本分配给他们。
- 计算一个重新分配的计划,这次要实际用于重新分配。
- 通过反复向Kafka控制器经纪商发布计划中的分区重新分配来执行重新分配计划。
- 一旦计划完成,经纪商被确认是空的,就关闭那些被移除分区的经纪商。这一步是可选的,取决于
toShutdown标志,其默认值为true。 - 清理现在关闭的经纪商上的经纪商副本排除。这一步是可选的,取决于
toShutdown标志,其默认值为true。
总结
通过这六个步骤,Confluent在Confluent平台上提供了完整的多经纪商移除功能!这个功能也是Confluent平台的支柱。
这个功能也是Confluent云中集群收缩的支柱。其结果是一个可以安全和弹性地收缩和扩展的集群,以满足任何现实世界使用案例的不同需求。在接下来的几周里,请继续关注Confluent博客,看看更多关于Confluent云如何实现这一功能的细节。
同时,考虑更新到Confluent Platform 7.1或打开Confluent Cloud用户界面,尝试今天就收缩您的集群。
如果你觉得这里描述的任何错综复杂的问题都很有趣--在幕后还有很多事情要做如果你想成为一个充满活力的团队的一部分,不乏有趣的挑战需要解决--我们正在招聘!
Stanislav Kozlovski是一名软件工程师,是Confluent的Kafka核心团队的一员。他是一个自学成才的开发者,在他短暂的职业生涯中一直为高增长的创业公司工作,你可以在推特上看到他的名字@BdKozlovski。