使用Apache Kafka时的5个常见陷阱

334 阅读16分钟

无论你是一个经验丰富的Apache Kafka®开发人员,还是刚刚开始,你都有可能在某些时候遇到障碍,无论是在配置和了解你的客户,还是在设置和监控经纪人方面的事情。由于涉及到所有的移动部件,有时很难确切知道如何保持你的Kafka管道顺利运行。为了帮助你解决这个问题,我们编制了一份清单,列出了五个常见的陷阱和如何避免这些陷阱的提示--从客户端和代理配置到设计和监控方面的考虑--这肯定会节省你的时间和精力。

我们将讨论的内容

从客户方面来看。

1.request.timeout.ms 设置得太低
2.误解生产者重试和可重试异常

从经纪人方面看。

3.不监控关键的经纪人指标
4.过度使用分区
5.segment.ms 设置得太低

让我们先来看看用户在客户端的一些常见配置错误。

request.timeout.ms 是一个客户端的配置,它定义了客户端(包括生产者和消费者)将等待多长时间来接收来自代理的响应。这个配置的默认值是设置为30秒。

如果在超时之前没有收到响应,那么将发生两种情况之一:要么客户端将尝试重新发送请求(如果重试被启用并且尚未用尽,关于这一点的更多细节,见下文),要么将直接失败。

request.timeout.ms 设置为一个较低的值可能是很诱人的。毕竟,有了更短的超时时间,客户端可以更快地做出反应,不管这是否意味着重新连接或甚至失败。然而,虽然这听起来很直观,但它并不总是一件好事。如果你不小心,你可能会加剧经纪人方面的任何问题,导致你的应用程序的性能下降。

例如,如果一个经纪商需要很长时间来处理其传入的请求,跨客户应用程序的较低的request.timeout.ms ,可能导致请求压力增加,因为额外的重试被添加到经纪商的请求队列中。然后这就加剧了对经纪商的持续性能影响,增加了它的压力。

建议将request.timeout.ms ,默认值为30秒。正如上面所讨论的,一个较低的值实际上会增加服务器端请求的处理时间。根据你的应用需求,如果你经常看到这些超时,将request.timeout.ms 设置为一个较高的值可能实际上是有用的。

2.对生产者重试和可重试异常的误解

当执行producer.send() ,希望记录能通过并成功地存储在一个主题中。现实是,由于某种原因,生产者的请求可能会失败。在某些情况下,失败是短暂的和可恢复的(即,如果有足够的时间和客户端重试请求,失败是可以恢复的),而其他的将是永久性的(即,在请求成功之前需要修复一些东西)。

例如,在集群滚动期间,客户可能会遇到以下一些可恢复的异常。

  • UNKNOWN_TOPIC_OR_PARTITION
  • LEADER_NOT_AVAILABLE
  • NOT_LEADER_FOR_PARTITION
  • NOT_ENOUGH_REPLICAS
  • NOT_ENOUGH_REPLICAS_AFTER_APPEND

如果重试和重试时间配置不当,所有这些异常都会被记录为错误,这有可能会扰乱你的客户端,导致消息丢失。

一些生产者的配置可以对是否启用重试以及如何处理重试产生影响。以下是需要检查的生产者配置,以确保重试被启用并按预期工作。

  • **[retries](https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_retries):**正如其名称所暗示的,这是尝试重试的次数。生产者重试的默认值是INT_MAX--根据你选择的编码语言的最大整数值--但你可以把它设置为0到INT_MAX 之间的任何值。请记住,虽然这是一个大值,但并不意味着客户端将永远重试--实际上,请求将在delivery.timeout.ms 的范围内尽可能多地被重试。

  • [**delivery.timeout.ms**](https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_delivery.timeout.ms)并且[**linger.ms**](https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_linger.ms)**:**生产者有能力在幕后对记录进行批处理,并在向同一分区发送消息的过程中更有效率。linger.ms ,控制生产者在批处理这些消息时的逗留时间。delivery.timeout.ms ,是生产者尝试交付记录的总时间的上限(默认为2分钟)。它必须等于或大于linger.msrequest.timeout.ms 的总和。

  • **[retry.backoff.ms](https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_retry.backoff.ms):**如果尝试重试,这是生产者在重新发送请求前等待的时间。请记住,如果你把这个值设置得太低,有可能导致瞬时异常的事件还没有被解决,这意味着故障可能继续发生。

  • **[request.timeout.ms](https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms):**如上所述,建议将request.timeout.ms ,默认值为30秒。

  • [max.in.flight.requests.per.connection](https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_max.in.flight.requests.per.connection) 和 :[enable.idempotence](https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_enable.idempotence) max.in.flight.requests.per.connection 定义了一个给定的客户端在传输过程中可以有的未被确认的请求的总数,同时也保持排序保证。当 被设置为 ,一个给定的消息只有一个副本会被写入主题;如果 ,重复的消息是可能的。enable.idempotence true false

    为什么这些很重要?一句话:它们可以影响主题上消息的排序,特别是当重试被启用,max.in.flight.requests.per.connection 大于1,以及enable.idempotence=false 。假设一个生产者向经纪人发送了两个包含同一分区消息的请求。第一批失败了,生产者试图重新发送,而第二批则成功了。这意味着第二组消息将第一批消息之前出现在分区上。如果排序对你的用例很重要,而且你想利用重试,请避免同时设置max.in.flight.requests.per.connection 大于1和enable.idempotence=false

  • **[acks](https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_acks):**如果你想完全禁止重试,你可以把这个值设置为0。这告诉生产者,无论是否有任何例外,都要 "发射并忘记 "这个请求。(要了解更多关于交付保证的信息,请看这篇博文:每个Kafka开发者都应该知道的前五件事)。另一方面,如果你想启用重试,请确保acks ,不要设置为0。

在开始使用上述生产者配置参数之前,请决定你希望你的代码是如何表现的。你是否想启用重试?检查retries 的值是否大于1,并且acks 被设置为1all 。 你是否希望你的生产者快速失败,并对所有可能遇到的问题抛出异常?通过设置retries=0 ,禁止重试。(注意,在这种情况下,你仍然有能力在客户端代码中自行处理异常)。

当客户端与集群进行交互时,问题很容易出现,但集群本身呢?意识到经纪人可能出错的事情以及如何缓解它们同样重要。

3.不对关键的经纪人指标进行监控

Kafka经纪人暴露了许多非常有用的JMX指标,让你对集群的整体健康状况有了更深入的了解。不幸的是,并不是所有的集群管理员都对这些指标给予足够的关注。

其中一些指标是显而易见的,很容易理解和利用,而另一些指标则可能有点难以操作,所以研究和理解你可以使用的指标是很重要的。如果你想专注于少数几个关键指标,以下五个指标是一个很好的开始。

  • **kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions:**每个分区在集群中都有一些跨经纪商的副本。数据首先被写入领导者副本,然后被复制到跟随者副本上。这个指标是对还没有在集群中完全复制的分区的计数。任何数量的复制不足的分区是一个不健康的集群的标志,因为它意味着你的数据没有像预期的那样被完全复制。
  • **kafka.network:type=SocketServer,name=NetworkProcessorAvgIdlePercent:**这个指标描述了你的网络处理线程闲置的时间百分比。所有的Kafka请求都是通过网络线程传输的,所以这个指标是相当关键的。0意味着所有资源都不可用,1意味着所有资源都可用。作为一个经验法则,这个值应该在30%以上,这样才不会持续耗尽你的集群资源。
  • **kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent:**请求处理程序从请求队列中获取请求,处理它们,并将响应输出到响应队列中。与上述类似,这个指标描述了你的经纪人请求处理线程空闲的时间百分比。0意味着请求处理线程完全不可用,而1意味着它们全部可用。尽量使这个值也保持在30%以上。
  • **kafka.network:type=RequestChannel,name=RequestQueueSize:**这描述了请求队列中的请求总数。较高的计数意味着队列是拥挤的,所以这个指标最好有一个较低的值。结合NetworkProcessorAvgIdlePercentRequestHandlerAvgIdlePercent ,你可以用这个指标来了解整个Kafka请求管道的繁忙程度。
  • **kafka.network:type=RequestMetrics,name=TotalTimeMs,request={Produce|FetchConsumer|FetchFollower}:**这是一系列的指标,描述了给定类型的请求所需的总时间(包括发送的时间)。它存在于每一种请求类型--生产者、获取消费者和获取跟随者。这个指标让你对系统的整体延迟有一个很好的概念。一个较低的时间意味着一个更健康的集群。

经纪人的请求延迟,或者说经纪人处理请求的速度,对整个集群的健康是非常重要的。更重要的是,除了启用请求记录(这是非常冗长的,可能会极大地影响性能)之外,真的没有任何其他代理请求健康的方法,所以指标真的是要走的路。底线是,忽视这些指标会产生影响。在其他方面,我想到了缓慢的响应时间。

如果你不知道JMX指标,或者没有过多地关注它们,你应该开始这样做。知道它们的存在是成功的一半,现在你可以开始对这些指标采取行动,使你的集群更健康、更有弹性。上面的指标例子只是冰山一角,我们鼓励你看一下这些额外的资源。

在读完这些之后,你可能会想。如果我不能看到一个流氓客户在滥用集群,那么经纪人指标有什么用?这当然很重要!我们看到了你的担忧,并向你提出了KIP 714(在写这篇文章的时候,它正在讨论中)。这个KIP提供了更高的客户端在经纪人内部的可观察性。这多酷啊?

如果监测所有这些指标的想法令人生畏,你应该知道Confluent Cloud,我们完全管理的Kafka服务,为你处理所有这些。延迟、利用率和一个简化的集群负载指标在默认情况下被暴露在专用集群中。

4.过度使用分区

分区是Kafka的并行单位--当然不包括其他因素,比如消费者的吞吐量--所以为了提高你的整体吞吐量,尽可能多的使用分区是有意义的,对吗?嗯,不一定。更多的分区数量可能会在集群中产生一些后果,包括但不限于。

  • 文件处理程序的数量增加,这可能会超过底层操作系统设定的限制。当一个消息被生产到一个主题时,该消息被装入一个特定的分区。在引擎盖下,数据实际上被附加到一个日志段文件中(在下一节中会有更多的介绍),并且伴随着索引文件被更新。数据文件和索引文件都有一个文件处理程序。因此,如果你有10个主题,每个主题有50个分区,在任何时候至少有2000个文件处理程序。从这个角度来看,Linux通常将每个进程的文件描述符的数量限制在1024个。

  • 当代理故障发生时,分区不可用的几率更高。在一个理想的、有弹性的集群中,每个分区都会在集群中的其他经纪商中复制若干次。选择一个经纪商来维护领导者副本,所有其他副本都是跟随者。在一个经纪商失败的情况下,它所拥有的任何领导者分区在一定时间内不可用,而控制器(集群中的另一个经纪商)会努力选出一个跟随者副本作为新的领导者。

    有三种不同类型的经纪商故障转移可能发生:清洁、不清洁和涉及故障控制器的故障。值得庆幸的是,在干净的关机中,控制器可以主动选择领导者复制,停机时间相对较短。也就是说,当关机是不干净的时候,控制器不能主动出击,在控制器确定领导者复制的时候,故障代理上的所有领导者分区都会一下子不可用。在控制器经纪商宕机的情况下,情况变得更加糟糕;不仅需要选举一个新的控制器,它还必须通过读取ZooKeeper内每个分区的元数据来初始化自己。在后两种情况下,这些分区的不可用时间与集群中的分区数量成正比。分区数越多意味着停机时间越长。

  • 增加的端到端延迟。消费者只有在消息被提交到所有同步的副本后,才会接触到主题上的消息。随着分区的增加,复制这些分区所需的带宽也会增加。在这一步骤中产生的额外延迟导致生产者写入消息和消费者可以读取该消息之间的时间增加。

另一个需要注意的要点是,如果你现在使用Kafka和ZooKeeper,你会发现每个代理的分区限制是4000个左右,每个集群是200000个。随着KIP-500的实施不再使用ZooKeeper,这将发生巨大的变化。由于集群将不再使用ZooKeeper来存储关于分区和经纪商的元数据,可扩展性将大大增加。测试表明,使用新的元数据法定人数控制器,一个集群可以轻松地包含超过200万个分区。

Timed shutdown operations in Apache Kafka with 2 million partitions

那么,你如何避免遇到这些问题呢?小心地设计主题分区,并在计划的吞吐量和使用量以及过度配置的缺点之间取得平衡。虽然有可能改变一个主题的分区数量,但并不总是可取的,因为这将对消息排序产生影响。

一个主题要使用多少个分区?

确定每个Kafka主题的分区数量的公式随着时间的推移已经得到了很好的探索。当在你的Kafka集群中创建一个新的主题时,你应该首先考虑你所期望的吞吐量(t),单位是MB/sec。接下来,考虑你在单个分区上能达到的生产者吞吐量(p)--这受生产者配置的影响,但一般来说,大概是10几MB/秒。最后,你需要确定你将拥有的消费者吞吐量(c)--这与应用有关,所以你必须自己测量它。你应该预计到至少max(t/p, t/c) 分区来处理这个问题。因此,如果你有一个250MB/秒的吞吐量要求,生产者和消费者的吞吐量分别为50MB/秒和25MB/秒。那么你应该为该主题至少使用max(250/50, 250/25) = max(5, 10) = 10 分区。

5.设置segment.ms 太低

虽然分区是生产者API的低级别,但当涉及到在磁盘上存储这些实际的字节时,Kafka将每个分区分割成段。每个段代表磁盘上的一个实际数据文件。了解段是如何工作和配置的,对于确保代理的最佳行为是很重要的。

当消息被写入主题时,数据被简单地附加到消息所属分区的最新打开的日志段文件中。当一个段文件保持开放时,它不能被认为是删除或压缩日志的候选文件。默认情况下,这些日志段文件将保持开放,直到它们完全充满(1GB),根据主题级segment.bytes 配置参数。相反,你可能希望强制段在特定的时间后滚动;这可以使用segment.ms ,另一个主题级配置来设置。

一些用户会尝试将segment.ms ,以帮助更频繁地触发日志压缩或删除,减少他们的集群在磁盘上占用的内存量。然而,如果segment.ms 被配置得太低(默认是七天),你的集群将产生大量的小段文件。由于这些小段文件太多,你的Kafka集群很容易遇到 "太多开放文件 "或 "内存不足 "的异常。此外,大量的小段文件或空段文件会对主题的消费者产生负面的性能影响。在获取过程中,消费者只能从每个分区接收最多一个段的数据。因此,如果段非常小,消费者在给定的时间内可以消费的东西就会受到限制,结果就不得不多跑几趟中介。

不幸的是,为了避免这种常见的陷阱,你在集群中创建和管理主题时,需要勤奋地进行配置。如果用户把segment.ms 设置成一个低值,一定要记得尽快把这个值设回默认值。这样做之后,请注意这些变化是不具有追溯力的,以前的段子不会受到影响。

总结

本文涉及的五个陷阱可以通过遵循这些提示来避免。

  • 避免将request.timeout.ms 设置得太低
  • 在你下次编写生产者客户端时,审查生产者重试和可重试异常情况
  • 监控代理指标,确保你的集群是健康的
  • 注意你的分区数量,特别是在你创建新主题的时候
  • 确保segment.ms ,不要设置得太低。

通过使用Confluent Cloud,你也可以轻松避免与代理有关的陷阱。有了这个完全管理的Kafka服务,你可以集中精力编写你的客户端应用程序,而不是担心保持经纪商的运行!使用代码CL60BLOG ,可额外获得60美元的免费使用权。*

开始使用

这里有一些很好的资源来了解更多关于Kafka的信息。

Danica Fine是Confluent的高级开发者倡导者,她帮助其他人从他们的事件驱动管道中获得最大利益。在此之前,她曾在彭博社的流媒体基础设施团队担任软件工程师,主要负责基于Kafka流和Kafka Connect的项目。

Hiro Kuwabara是Confluent全球技术支持工程组织的团队负责人。 在加入Confluent之前,Hiro在数据流领域的多家公司担任过客户工程职位,他从2016年开始就一直在使用Kafka。