Kafka 队列积压引发的问题
最近在生产环境的 Skywalking 监控统计数据中发现某个方法平均每分钟调用 5 万次,远远超出了预期。因为这个方法与 Kafka 消费者相关,于是排查 Kafka 的相关统计数据,发现该队列在这个时间段内有数据积压。
虽然说这段时间内 kafka 最多堆积了18万的数据,但是从业务量来说,每天也就二十几万次调用,觉得有点疑惑。每分钟调用几万次的统计数据是从哪里来的。
因为系统中实现了一个简单的重试机制,消费者在消费失败的时候,会立即重试 3 次,首先怀疑是不是与这个重试机制相关。但是从数据上来看,每分钟5万次的调用,即使除以 4,每分钟也有 1 万多次,数据也对不上。于是,担心是不是重试机制有问题。为了验证这个,在测试环境模拟失败重试的场景。如果仅调用一次接口,skywalking 的统计数据上面看到的数据是 4,在应用日志中看到异常的次数是 4 次,这个两个数字都符合预期。如果连续调用了 10 次,在 skywalking 的统计数据上看到的 76,在应用日志中看到的异常次数是 40。这个结果说明,重试机制没有问题,但是 76 这个数字不符合预期。
那 Skywalking 统计数据中的 76 这个数字是怎么来的呢?又去翻 Kafka 的监控数据,看看能不能有什么线索,就发现了 76 这个数字与 Sent Records 这个 Kafka 监控数据项匹配上了。看来这个数字与消费者从 Kafka 获取的记录数有关系。
但是明明只调用了 10 次接口,按理说队列中的数据只有 10 条,怎么就获取到 76 条数据?是不是队列中的有些记录被重复拉取?于是,在配置中开启 kafka 的日志,再测试一次。果然在应用日志中发现,同一个 offset 的数据从 Kafka 被拉取了多次。为了了解被重复拉取的原因,于是就是查了一些资料,并翻了源码。原来这个与 Kafka 消费者从服务端拉取数据的机制有关系。
Kafka 的消费者在每次 poll() 调用时会根据分配给它的分区拉取消息。如果一个分区有大量积压的消息,消费者会多次请求该分区的数据,导致重复的 poll() 调用。如果消费者没有及时提交偏移量,Kafka 可能会再次返回已经消费过的消息,导致重复的 poll() 调用。虽然 Kafka 会确保 每个消息只会被消费一次,但因为偏移量没有及时提交,消费者可能会重新请求之前拉取过的消息。
Kafka 集群和监控系统(例如 Prometheus、Kafka Manager 或 SkyWalking)通常会收集不同的统计数据,这些统计数据可能并不总是直接反映 队列中数据的数量,而是与消费者的 拉取行为 相关。它可能会根据时间粒度(例如每分钟、每小时)来记录 poll() 调用次数。如果有大量积压消息且消费者的处理速度较慢,监控数据可能会显示较高的 poll() 调用次数,甚至超过消息本身的数量。这种情况通常是由于延迟或频繁轮询造成的。
在目前系统的重试机制中,是调用失败后,在同一个线程立即以同步的方式重试 3 次。在当时那个时间段内,失败的调用太多,同时由于重试 3 次的原因,导致每一个消息的执行时间相对较长,导致队列中的数据无法即使处理,逐渐积压。这次的排查过程,还发现了另外一个问题,就是消息在队列中分布不均匀,这个也是导致队列积压的一个原因。
在这次排查与分析之后,做了一下分析与总结:
- 重试机制要根据业务场景来选择,包括是否要重试、用同步还是异步、重试次数等
- 在设计的时候,需要根据业务场景,也要根据生产环境的实际业务量来做分析,选用合适的值来作为 kafka 消息的 key,保证消息尽量均匀分布,避免发生积压。
参考: