基于开源组件打造Kafka自治集群

700 阅读13分钟

本文介绍了Slack如何基于Chef、Terraform、CruiseControl、CMAK等开源工具打造自动化的Kafka基础设施,实现Kafka集群的自动化运维管理。原文:Building Self-driving Kafka clusters using open source components[1]

本文讨论Slack如何使用Kafka,并且介绍了在过去四年中是怎样由一个小而精干的团队构建并规模化运行Kafka自治集群。

Slack利用Kafka构建发布/订阅系统,这一系统在所有重要的作业队列(Job Queue)[2]中都扮演重要角色,为Slack用户的几乎所有操作(例如在channel中展开链接,发送通知,通知机器人,更新搜索指数以及运行安全检查)提供异步作业执行框架。此外,Kafka还充当了在Slack上传输关键任务数据的神经系统,为日志管道[3]、跟踪数据[4]、计费管道、企业分析[5]和安全分析数据提供赋能。


Slack的Kafka之旅

早在2018年,几个团队就在各自的用例中采用了Kafka,并各自运行独立的Kafka集群。结果,不同团队部署了不同版本的Kafka,并且都做了重复的部署、运维以及管理Kafka集群的工作。

因此我们接手了将所有Kafka集群标准化到一个版本下的项目,并由一个团队进行管理。然而,由于我们团队规模比较小,所以希望能够尽可能让运维自动化。

今天,Slack在10个Kafka集群中管理着大约0.7 PB的数据,运行在数百个节点上,每秒处理数百个topic的数百万条消息,最高总吞吐量为6.5 Gbps。我们的Kafka基础设施成本不仅由硬件决定,也由网络决定,而消费者包括了从繁重的批处理作业到像Job Queue[2]这样对延迟非常敏感的应用。


什么是Kafka自治集群?

Kafka是一款优秀的软件,可以在Slack的数百个节点上运行。然而,如果你曾经尝试过部署或管理Kafka,就会知道这不是一件容易的事。我们经常会因为broker速度慢、时不时出现的故障或容量管理问题而被要求提供支持。

Kafka运维自动化的目标是消除日常管理Kafka的运维开销。

为此,我们为Kafka集群确定了一些常见的运维任务,包括:

  • Kafka常规管理操作,如创建topic,更改分区计数以及重新分配分区给broker
  • 容量规划操作,比如向集群添加/删除broker
  • 运维问题,如替换broker或部署新版本的软件
  • 诊断Kafka集群问题的on-call工作
  • 解释Kafka消费者的消费速度是否足够的客户支持工作

因此,当我们迁移到新版本的Kafka集群时,决定将运维方面的操作自动化,或者为用户提供自服务。


Kafka 2 项目

我们统一了努力的方向,基于2.0.1版本实现了一个更加自动化的Kafka。我们的Kafka设置由以下组件组成:

构建、发布、配置和部署:Chef和Terraform

我们使用Chef来管理基本操作系统,在主机上部署和配置Kafka软件。我们的每个Kafka集群都运行在不同的角色下,有自己的自定义配置,但共享相同的基础配置。我们用Terraform模块为AWS中的Chef角色创建ASG,自动管理节点的开通和关闭。

以前部署Kafka主要是通过部署Debian Kafka包来管理。然而,我们发现部署预构建的包是很痛苦的,因为配置并不总是可靠。此外,由于我们无论如何都要重写默认配置,所以Chef配置非常复杂。为了解决这些问题,我们在内部fork了Kafka仓库,并建立了CI/CD流水线来构建Kafka并将静态二进制文件发布到S3。然后,Chef将从S3中提取二进制文件并部署,这一过程可重复执行。

Apache Zookeeper 3.4集群一般来说是手动配置的,我们没有一种自动的方式来确保每个Zookeeper节点都有唯一的ID,并且没法在不重启整个集群的情况下重新分配ID。手动配置Zookeeper节点不仅冗长乏味(我们经常在普通的节点故障发生时被要求提供支持),而且很容易出错,我们有可能会意外的在同一个AWS可用域启动多个Zookeeper节点,从而增加影响半径。为了减少乏味和错误,我们通过升级到Zookeeper 3.6来自动化这个过程,它在替换broker时不需要重启集群。当配置Zookeeper节点时,通过Consul KV自动分配唯一的ID。有了这两个变化,就可以使用Terraform通过ASG开通Zookeeper集群。

Kafka集群稳定性优化

虽然上述配置有助于减轻自动化配置主机的痛苦,但我们仍然不得不负责集群运维,比如将分区迁移到新的broker,并为负载重新平衡broker。此外,集群运维操作影响到了客户,导致他们需要上线支持或者达不到SLO。

经过分析,我们发现Kafka集群中的热点导致了不稳定性,不同的问题会引发热点。

我们注意到,集群中有几百个Kafka topic,每个topic基于不同的负载有不同的分区计数。在常规运维过程中,一些broker会比其他broker处理更多数据。在集群运维操作(比如添加/删除broker)过程中,这些热点又会加剧,从而导致数据消费延时。

*为了解决热点问题,我们希望均匀利用集群中的所有broker。*我们将所有分区计数更改为broker计数的倍数,从而消除写热点。我们通过在所有节点上选择均匀的消费者数量来平滑读取热点。只要所有分区都均匀分布在集群中,就可以在整个集群中获得可靠的利用率,并平滑读写速率。此外,当扩展broker或消费者时,将更新topic的分区计数,这样分区计数仍将是代理计数的倍数,以确保均匀的利用率。

Kafka集群热点发现的另一个原因是分区再平衡事件期间的复制带宽消耗。我们发现,复制带宽不足的生产者或消费者消耗了大部分资源,尤其是在高峰时段。因此,我们限制了集群可以使用的复制带宽。然而,限制复制带宽会导致集群运维操作变慢,为此我们还修改了操作,每次只移动少量分区,这让我们能够持续做出许多小的改变。

尽管做出了这些努力,Kafka集群仍然会由于部分故障失去平衡。为了将这些操作自动化,我们使用了出色的Cruise Control[6]自动化套件(由LinkedIn构建)来自动化集群平衡操作,并确保集群中所有节点的平均利用率。总体来说,这些调优为集群稳定运行提供了帮助。

混沌工程

考虑到将流量从旧集群切换到新集群的影响很大,我们使用暗流量对新集群进行了混沌实验。

测试涉及到集群负载下各种资源。此外,我们能够在受控的条件下终止broker,这有助于我们更好的理解代理失效的模式及其对生产者和消费者的影响。

在这些测试中,我们发现集群恢复操作大多受到主机每秒发送的数据包数量的限制。为了支持更快的恢复,我们在这些主机上启用了jumbo frame。现在我们的Kafka实例拥有Slack基础设施团队中最高的每秒数据包利用率。

此外,这也帮助我们识别到使用Go Sarama库的用户在一些边缘情况下的bug。在某些用例下,我们已经将这些客户端迁移到Confluent Go,这也帮助我们标准化了跨语言的客户端配置。在我们无法升级使用者的情况下,我们添加了适当的工作区和告警来监视这些用例。

在这些测试中,我们也意识到Zookeeper的问题会迅速演变成更大的Kafka问题。所以,我们为每个Kafka集群配置了单独的Zookeeper集群来减少Zookeeper故障的影响半径,虽然这么做会稍微增加一些成本。

混沌实验还帮助我们理解在实际故障期间可能出现的操作问题,并帮助我们对集群做出更好的调优。

自服务Kafka集群

在很多情况下,消费者团队会向我们询问或者提出增加/减少集群容量的问题。其中一类问题是关于常规操作的,比如容量规划,另一类是了解流水线的健康状况。

此外,使用CLI工具来理解Kafka发生了什么是很乏味的。所以,我们部署了kafka manager[7],让每个人都能看到kafka集群的元数据,比如broker列表和topic列表。Kafka Manager还帮助我们简化了日常操作,比如创建新的主题和增加主题的分区数量。

为了提供对Kafka消费者健康状况的操作可见性,我们部署了Kafka offset exporter[8]的一个分支,将消费者的偏移信息导出为Prometheus度量指标。我们在这些数据的基础上构建了仪表盘,向消费者实时提供每个topic、每个消费者的聚合消费指标。

为了减少知识孤岛,我们将各种执行手册标准化为单一的执行手册,有助于将所有Kafka知识聚集到一个地方。此外,我们将多个Kafka仪表板整合到一个全局仪表板中,用于所有Kafka集群。

总之,这些自助式工具帮助客户更好的理解数据,同时减少了团队的运维开销。这些工具还通过减少SSH到Kafka broker的需要改善了安全状况。

升级Kafka集群

为了升级Kafka集群,我们决定不进行本地集群升级。原因是我们没有信心确保在升级窗口期间做到不停机,特别是在同时升级多个版本时。此外,我们没有办法验证新集群是否存在问题,特别是在更改底层硬件类型时。

为了解决这些问题,我们制定了一个新的集群切换的升级策略。切换过程如下:

  • 启动新集群
  • 在新集群上使用暗流量运行所有验证测试
  • 停止向旧集群生产数据
  • 开始向新集群生产数据
  • 在保留窗口过期后关闭旧集群

虽然这种策略有协调截断消费者的缺点,但这是一种标准的运维过程,也适用于其他场景,如跨集群移动topic以及测试新的EC2实例类型。


分裂Kafka主集群

在投入时间让Kafka可以自我维持以及提升了可靠性之后,我们可以有时间去做其他重要的功能,比如tracing[4]。然而,即使我们已经做了所有工作,当系统达到临界点时,仍然需要重新审视假设和容量。

我们在2021年初接近了这个点,90个broker集群在网络吞吐量上达到了临界点,最高达到40,000 pps。网络饱和导致下游管道延时,Kafka在正常工作负载下都难以跟上用户,更不用说处理大流量峰值了。依赖日志管道来调试问题的开发者每天都会受到Kafka网络饱和的影响。

为了减少Kafka主集群的负载,我们利用工具和自动化将大型topic分割到更小、更高性能的集群中(从旧的d2[9]实例升级到现代的启用了nitro的d3en实例[10])。比较两个集群之间类似的工作负载,新的集群能够在20个broker上实现类似的性能(每1,000 pps),大约提高了2.5倍的效率。

在将三个最大的topic移出主集群后,问题立即获得了缓解。下面是一些当时的图表,用来说明这项工作的影响。

这些峰值代表了消费者从最大的topic之一消费数据产生的滞后,每当滞后超过5亿,就会因为日志刷新而破坏SLA。

主题迁移完成后,消费者延迟大大改善。我们的日志延迟从最坏情况下的1.5小时减少到最坏情况下的3-4分钟。此外,我们的日志记录流水线的on-call数量从一个月71个告警减少到一个月9个告警。相当大的进步!

更小的专用Kafka集群也更容易管理,集群运维操作可以完成得更快,而且由于干扰产生的噪音问题更少。


结论

使用Cruise Control、Kafka Manager、Chef和Terraform等开源组件,可以大规模运行自愈的Kafka集群。此外,使用标准的SRE原则和适当的工具,如Kafka Manager和Kafka offset exporter,可以构建可靠、自助式和自治的Kafka。

我们从其他人的Kafka配置中受益匪浅,本着分享传递的精神,可以在Gibhub上找到我们的Kafka配置[11]

过去几年来,这一架构一直在Slack成功运行。展望未来,Kafka将在Slack扮演更重要的角色,它是新的变更数据捕获(CDC,Change Data Capture)项目的一部分。新的CDC功能将支持Slack的权限服务(Permission Service)的缓存需求,该服务用于授权Slack中的操作,也将支持数据仓库的近实时更新。为此,我们在Slack成立了一个新的数据流团队来处理所有当前和未来的Kafka用例,并维护和支持Slack的所有Kafka集群。

References:
[1] Building Self-driving Kafka clusters using open source components: slack.engineering/building-se…
[2] Scaling Slack's Job Queue: slack.engineering/scaling-sla…
[3] Data Wrangling at Slack: slack.engineering/data-wrangl…
[4] Tracing at Slack Thinking in Causal Graphs: slack.engineering/tracing-at-…
[5] Understand the data in your Slack analytics dashboard: slack.com/help/articl…
[6] Cruise Control: github.com/linkedin/cr…
[7] Cluster Manager for Apache Kafka: github.com/yahoo/CMAK
[8] Kafka offset exporter: github.com/echojc/kafk…
[9] Now afailable d2 instance the latest generation of Amazon EC2 dense storage instances: aws.amazon.com/about-aws/w…
[10] D3: aws.amazon.com/ec2/instanc…
[11] gist.github.com/mansu/dfe52…

你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。
微信公众号:DeepNoMind