阿帕奇·卡夫卡-上线前需要检查的8件事

2020-11-10 03:13:46

阿帕奇·卡夫卡是一个美丽的系统。它具有良好的可扩展性、稳定性和惊人的系统架构灵活性。在运行了5年的生产Kafka集群后,我收集了一系列提示和陷阱。如果你在一个将卡夫卡投入生产的小团队中工作,这些可能会被证明是有用的。这篇文章假设你熟悉卡夫卡的基本概念,如经纪人、主题、生产者和消费者。更重要的是,以下几点对于卡夫卡2.0版应该是有效的,不需要更多的麻烦。

Kafka主题由一个(可配置的)分区数组成。如果用户没有提供分区号,KafkaProducer将使用传递给它的ProducerRecord实例中的键为消息选择分区。

查看默认实现:它检查密钥是否为空,如果不为空,则计算以分区数为模的Murmu2散列。这是一致的;对于共享相同密钥的消息,它将产生相同的结果。但是,如果密钥为空,它将使用粘性分区,该分区在每批中随机选择一个分区。实际上,如果没有传递密钥,则生产者将随机选择分区。

这具有重要的意义,因为Kafka只提供分区内的递送顺序保证。同一分区中的消息将按照提交的顺序递送。不同分区中的消息将以不确定的顺序递送。如果消息之间有任何形式的因果关系,并且它们不在同一个分区中,那么任何下游消费者在处理它们之前都必须收集关键字的所有消息,否则可能会违反因果一致性;无论在无限流的上下文中意味着什么。

举一个简单的例子来说明上面的内容,想象一下如下所示的简单事件:

但是,如果不能保证每个电子邮件的因果顺序,我们需要保持足够的状态来知道我们看到的事件是否真的是最新的。否则,如果顺序颠倒,我们可能会向已取消订阅的用户发送电子邮件。例如:

如果我们使用电子邮件发送邮件,那么Kafka将按照邮件插入的顺序发送单个电子邮件的所有邮件。我们的消费者可以是完全无状态的;它可以从Kafka获取邮件并将其存储到数据存储中。如果密钥为空(未提供),我们的无国籍咨询人员将很乐意存储来自过去的邮件。为了缓解这一问题,我们至少需要为每封电子邮件维护最新的Created On Date。依赖挂钟判断因果关系是一个非常糟糕的主意。

假设每个键的排序是有保证的,那么下游的约简的界可以从半格降为半群。在实践中,利用这一性质,我们可以省去交换性的要求,从而解锁更容易的实现。如果为卡夫卡消费者设计约简听起来很有趣,让我知道,我会写下来。

仅提供密钥是不够的。分区(即函数f:(Key)=>;Partition)是可配置的。Kafka提供了几个分区,用户可以推出自己的分区。不要假设所有的制作人都使用相同的分区程序。

在一个复杂的系统中,围棋服务、Python服务、Spark和其他野生动物都共享同一个Kafka集群,可能存在各种不同的实现。如果不同的服务将数据推送到相同的主题,集成测试将非常有用。如果出现问题,交付给消费者的将是不确定的,调试可能是纯粹的地狱。

卡夫卡的美妙之处在于,数据可以根据需要多次重新处理,这可以原谅很多错误。为了说明这一点,让我们假设同样的虚拟模型:

现在,让我们假设消息在被推送到Kafka之前被序列化为JSON。由于JSON序列化错误,以下内容被推送到电子邮件订阅主题:

一种解决方案是为那些有缺陷的实例创建一个自定义的反序列化程序,并将其添加到所有使用者。这不是一种简单且容易出错的代码;它只对这个实例有用。

另一种解决方案是实现一个从电子邮件订阅-1读取、修复问题并写入电子邮件订阅-2的使用者。一旦两个主题的偏移量相同,生产者和消费者就可以从电子邮件订阅-1切换到电子邮件订阅-2,而无需更新任何代码。这样做的伟大之处在于,这些迁移可能会失败而不会产生重大后果。如果电子邮件订阅-2不起作用,我们可以再次运行并生成电子邮件订阅-3,依此类推。此技巧也适用于重要的架构更改、迁移和其他数据丰富。在某些情况下,Avro和Profobuf可以提供帮助,但会出现错误,需求会以不可预测的方式发展。无论如何,通过读取主题的数据并将其发布到主题来修复该主题的数据通常不是一个好主意。主题应该是不可变的和真实的。

至少在2.6.0之前,卡夫卡都依赖ZooKeeeper。失去与ZooKeeper的连接意味着没有ISR(同步复制,稍后会详细介绍),没有分区领导选举,最终会有经纪人关门。谢天谢地,@fpjuqueira和他创建ZooKeeper的团队是真正的专业人士,这不会无缘无故发生。事实上,ZooKeeper是最可靠的分布式系统之一(至少我见过)。

由于Provisioning Ansible脚本中的错误,一个集群的2/3个RD最终位于同一可用区,具有连续的IP(通常意味着在AWS上的同一机架中)。它们同时消失。没有共识,地狱爆发了。

QA环境运行的时间足够长,以至于所有节点都会耗尽磁盘空间(ZooKeeper会随着时间的推移创建事务日志的备份快照,而外部人员必须处理删除这些快照的问题)。同时。要让这个env恢复生机,需要手动编辑znode,并且静态数据会丢失。

为了清理ZooKeeper 3.4.x中较旧的事务日志快照,ZooKeeper提供了以下工具:

这只是两个例子。可能会出更多的差错。由于故障的后果,强烈建议使用适当的JMX度量监控和实时日志聚合,所有这些都与PagerDuty的形式挂钩。

这本质上是可用性和耐用性之间的权衡。让我们从不干净的选举开始吧。

让我们假设我们有一个只有一个分区和一个副本的主题。数据正在愉快地流入。如果副本是同步的(也就是与引导器和ZooKeeper中的ISR集相同),则如果引导器分区变得不可用(例如代理崩溃),则副本可以拾取、接受写入并继续,而不会停机。但如果副本滞后,则引导器将从ZooKeeper中的ISR中删除它。如果领导人倒台,有两种选择:

滞后的复制副本拾取并接受写入,旧领导者的任何多余写入都会丢失。从本质上讲,副本在没有同步的情况下被选举为领导者。这是不干净的部分。

这完全取决于主题所包含的数据类型。如果主题包含系统指标,则最新的数据可能更有价值,因此丢失一些较旧的写入可能是可以接受的。如果主题包含银行交易,则下降到人工干预可能是更好的选择。这是可以覆盖每个主题的代理级配置。

此等式的第二部分是min.insync.plicas,它表示为完成写入而必须同步的最小副本数。这可以在代理级、主题级甚至在生产者级(即ACK)进行配置。与上面相同的考虑因素是,如果主题包含付款,那么只有一个包含所有数据的副本可能会有风险。

传奇的分布式系统研究员凯尔·金斯伯里,又名阿菲,在大约7年前对卡夫卡的复制机制做了一次出色的分析。如果你想更深入地研究这种权衡,强烈推荐阅读阿菲的这篇文章。据我所知,讨论的基本权衡在今天仍然适用。

卡夫卡用了很多这样的东西。耗尽这些分区会导致致命的运行时异常,从而杀死代理。如果使用操作系统缺省值,则极有可能在群集的每个代理包含数万个分区时就会达到这些缺省值。更糟糕的是,在一个均衡的集群中,代理拥有相似数量的分区,这些故障将大致同时发生。让我们更仔细地了解一下:

MAX_MAP_COUNT:该文件包含一个进程可以拥有的最大内存映射区数量。内存映射区被用作调用Malloc的副作用,直接由mmap、mProtection和mise使用,并且在加载共享库时也是如此。虽然大多数应用程序只需要不到一千个映射表,但某些程序,特别是Malloc调试器,可能会消耗大量的映射表,例如,每次分配最多一个或两个映射表。默认值为65536。

每个日志段需要一个索引文件和一个时间索引文件;每个文件都需要一个映射区。每个分区包含多个日志段。Kafka关闭一个段并打开一个新段的频率取决于Segment.bytes和Segment.ms,默认为1 GB和7天。例如,如果保留时间设置为一年,数据段设置为每天滚动,则一个分区一年后可能有365个日志数据段。每个分区都需要2个映射区。如果一个代理有1,000个这样的分区,它将需要365x2x1000=730000个映射区域。

这听起来可能很高,但如果卡夫卡充当一个科技组织的中枢神经系统,拥有数千个分区和数万个日志段并不少见。考虑到卡夫卡的PID,要查看一个经纪人目前正在消费多少地图:

注意:这可能不是启动进程的限制。要增加当前会话的限制,请执行以下操作:

快速解决方案是增加最大映射区域并重新启动代理。实际解决方案是检查数据段大小、保留策略和数据段滚动计划,以发现效率低下的地方。

文件描述符限制:Kafka对日志段和开放连接使用文件描述符。如果一个代理托管了许多分区,考虑到该代理除了跟踪代理建立的连接数量外,至少需要跟踪所有日志段。我们建议至少100000个允许的文件描述符作为代理进程的起点。

我见过的生产经纪人消耗了大约40万台这样的设备。在有大量消费者和细分市场的情况下,100K可能是不够的。无论如何:

注意:映射区和文件描述符都与操作系统相关,并且根据Kafka运行的位置而有所不同。如果Kafka部署在Kubernetes或Mesos上,容器编排层可能会引入额外的约束。一个好的方法是在测试环境中将Segment.bytes和Segment.ms设置为小值,然后通过编程创建主题和消费者,直到Kafka崩溃。

日志压缩的目的是保留分区中每个键的最后已知值。此过程保留了最有价值的数据,节省了空间,并加快了主题的重新处理。要使日志压缩正常工作,需要配置键和分区(请参见(1))。在实践中,对于a)我们需要永久保留一个实体并且b)此实体经常更新的用例来说,它是一个很好的候选方案。例如,从用户配置文件到帐户余额再到统计数据聚合。要使日志压缩起作用,首先要确保在server.properties上启用了它(这是0.9.0以后的默认设置):

根据经验,如果过度使用压缩,128MB的大小有点太小。最重要的是,Log Cleaner管理器线程可以在不杀死代理的情况下终止。如果代理依赖Log压缩,则确保监视log-leaner.log的错误。如果这些线程死了,那么代理可能会坐在一个定时器磁盘炸弹上。

如果上面有什么不对劲的地方,请让我知道。同样,你也可以得到一般的反馈,或者要求对任何事情进行扩展。