一次Kafka性能问题分析

551 阅读4分钟

问题现象描述

生产上监控发现,kafka集群中有个topic集群出现了消费组消息堆积的情况。

image-20240626132707800.png

问题分析

由于项目匆忙,我们把同一个服务,所有的消息监听都配置成了一个消费者组。我们发现某一个topic的消息发送堆积后,其他topic的消息也一直无法被消费。

同一个group为什么不适宜订阅太多topic?
  • 每个消费者实例需要维护多个主题的偏移量、元数据和连接,这会增加内存和 CPU 的消耗。
  • Kafka 的分区(Partition)分配机制会根据主题和分区数量来分配消费者实例。如果一个消费组订阅了过多的主题,可能会导致某些消费者实例负载过重,而其他实例负载较轻,导致负载不均衡。
  • 订阅过多主题会增加每个消费者实例需要处理的数据量,从而增加处理延迟。
  • 当一个消费者实例出现故障时,Kafka 需要重新分配分区给其他消费者实例。如果订阅的主题过多,重新分配和恢复的过程会更加复杂和耗时。

鉴于以上原因,所以第一时间就判断可能是同一个group订阅了太多的topic,我们决定把消息堆积的topic订阅者独立出来,新建一个消费者组。

kafka消费者组订阅.png

应用重启部署后,发现除了消息量巨大产生堆积的topic外,其他的topic都可以被消费掉了,但是问题并没有完全解决,最繁忙的topic堆积还在持续增加,并且客户端Rebalance还是很频繁。

深入分析

此时spring-kafka客户端配置如下

spring:
  kafka:
    bootstrap-servers:xxxxx
    consumer:
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      enable-auto-commit: true
      auto-offset-reset: earliest
      auto-commit-interval: 1S
      heartbeat-interval: 3000
      properties:
        spring.json.trusted.packages: "*"
        session.timeout.ms: 100000
        max.poll.records: 200
        max.poll.interval.ms: 300000

先增加配置,打印kafka 日志,观察有没有什么有用的信息。

logging:
  level:
    org.springframework.kafka: DEBUG

kafka相关资料,我们知道,客户端发生Rebalance的原因大致会有两种情况:

  • 心跳参数设置不合理,导致Consumer心跳超时,引发Rebalance。
  • 客户端消费过慢,超过一定时间未进行poll拉取消息,会导致客户端主动离开队列,而引发Rebalance。

首先我增加了消费者服务的节点数,期待能提升消费速度,但是增加了2个pod后,发现消息堆积现象并无好转。

然后我又调整了heartbeat-interval 参数的值,从3000改为1000,保证其值远小于session.timeout.ms。重启应用后发现情况稍有缓解,但运行一段时间后,还是频繁触发Rebalance,并无解决问题。

排除了心跳参数问题后,就可以把原因聚焦在客户端消费速度上。

根据第一步增加了spring-kafka的日志输出后,从日志中发现了一些关键信息

E8E58D25-244C-416b-8555-6705D3285B87.png

B1EAC632-B7C2-4708-88BF-03C69688455E.png

其中关键信息如下 consumer poll timeout has expired. This means the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time processing messages. You can address this either by increasing max.poll.interval.ms or by reducing the maximum size of batches returned in poll() with max.poll.records. 可以判断是客户端耗时超过了拉取频率,导致处理超时,从而引发了Rebalance。

结合信息要调整 max.poll.recordsmax.poll.interval.ms 两个参数的值,每次拉取记录数测试了1000、200、30等数值,发现影响并不大,因此判断还是主要调整max.poll.interval.ms,具体应该调整多少,我们从arms上观察,发现groupB关联的消息处理耗时确实比较慢,就取最大耗时时间更长一些,

image-20240626145904006.png

最终spring-kafka设置如下:

spring:
  kafka:
    bootstrap-servers:xxxxx
    consumer:
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      enable-auto-commit: true
      auto-offset-reset: earliest
      auto-commit-interval: 1S
      heartbeat-interval: 1000
      properties:
        spring.json.trusted.packages: "*"
        session.timeout.ms: 100000
        max.poll.records: 200
        max.poll.interval.ms: 1800000

后续处理

至此,表象上不再发生Rebalance了,堆积消息开始逐步减少,但是还需要从根本上解决消费端耗时较久的问题。跟踪耗时较久的trace,发现基本都是堵在正则表达式处理文本上,因为业务诉求,需要通过正则表达式处理文本,当消息涉及大文本(几千甚至数万字)时,正则的效率极其低下。找到根本原因后,我们做了一些针对性优化:

  • 预编译正则,尽量提高正则表达式的复用性,而非每次编译正则,再去match文本
  • 缩短文本,根据业务特性,做一些文本截断,最大不超过3000字,提升match效率
  • 增加Consumer消费线程数,提升并发处理效率 ,根据topic的分区数和消费服务数,增加了KafkaListener的"concurrency"="n"

优化至此后,持续观察数日,再无Rebalance情况发生,同时消费速度有了提升(采样2天数据:6月19日平均耗时1.3s,6月25日平均耗时43ms)。