凌晨告警响起:Kafka 消息积压(Consumer Lag)持续飙升,下游订单、风控、日志链路开始延迟。消息积压本质上不是 Kafka 本身的故障,而是「生产速度长期大于消费速度」的直观结果。能否在几分钟内判断是消费慢、分区不够还是上游洪峰,决定了你今晚要不要熬夜救火。
一、先搞清楚:什么是消息积压(消费滞后)
Kafka 中每个分区是一个有序的日志文件,生产者往末尾追加,消费者按位移(offset)顺序读取。所谓消息积压,就是「分区最新位移」与「消费者已提交位移」之间的差值,这个差值被称为 Consumer Lag(消费滞后)。Lag 不为零是正常的(消费天然略滞后于生产),但当 Lag 持续、快速、单向增长时,就说明消费跟不上生产,积压开始形成。
一个常见误区是盯着「消息总量」看。真正该盯的是每个分区的 Lag 曲线:总量不涨但 Lag 涨,说明消费线程卡死;Lag 阶梯式上涨又回落,可能是频繁的 Rebalance;Lag 平稳线性增长,多半是消费吞吐不足。判断积压的第一步,永远是拿到这张分区分维度的 Lag 表。
二、确认积压规模:用 kafka-consumer-groups 看 Lag
最常用、零依赖的命令是官方自带的消费组诊断工具。它能列出某个 Group 订阅的所有 Topic 分区、最新位移、已提交位移以及差值:
# 查看指定消费组的滞后情况
kafka-consumer-groups.sh \
--bootstrap-server broker1:9092 \
--describe \
--group order-consumer-group
# 输出示例(关键看 LAG 列)
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID
# orders 0 1002301 1002580 279 consumer-1-xxx
# orders 1 998812 1002600 37888 consumer-2-xxx
# orders 2 1002400 1002405 5 consumer-3-xxx
三列的含义要记牢:LOG-END-OFFSET 是分区最新消息位移(生产端水位),CURRENT-OFFSET 是消费者已提交位移(消费端水位),两者相减就是 LAG。上面的例子里,分区 1 的 Lag 高达 3.7 万且还在涨,而分区 0/2 几乎追上,这种「个别分区严重滞后」的分布,往往指向数据倾斜或该分区对应的消费线程卡死,而不是全局吞吐问题。
三、定位根因:四类最常见的积压原因
拿到 Lag 分布后,按下面四类逐一排查。错把「分区不够」当成「消费慢」去调线程,往往越调越乱。
1. 消费侧处理慢(单条/单批耗时过高)
这是最常见的原因。消费者里同步调了慢接口、写了未批量的数据库、或在循环里访问了远程服务,导致每条消息处理几十毫秒甚至几百毫秒。表现是所有分区 Lag 均匀上涨,且消费 TPS 上不去。用 Arthas 或 pprof 抓一下方法耗时即可确认。
2. 分区数不足 / 消费并发度不够
Kafka 的并行度天花板是「分区数 = 消费者实例数」。如果 Topic 只有 3 个分区,你起 10 个消费者也只有一个能干活。表现是整个 Topic 的 Lag 均匀上涨,且 CPU/IO 都没打满——典型的「线程在等分区」。此时需要增加分区或提升单消费者吞吐。
3. 消费者宕机或 Rebalance 风暴
消费者频繁上下线、GC 停顿触发会话超时、或 max.poll.interval.ms 设置过小导致被踢出组,都会引发消费组再平衡(Rebalance)。Rebalance 期间整个组暂停消费,Lag 呈阶梯式跳跃。这与单纯的「消费慢」不同,根因是稳定性而非吞吐,需要先稳住消费者。
4. 上游生产突增 / 洪峰
大促、批量回放、上游 Bug 导致的消息重发,会让生产 TPS 瞬间翻几倍。此时消费端没变,但相对生产能力瞬时不足。这类积压通常是短暂的,优先扩容消费侧即可,不必大改代码。
| 现象特征 | 最可能的根因 | 止血动作 |
|---|---|---|
| 所有分区 Lag 均匀涨,CPU/IO 空闲 | 分区/并发不足 | 扩分区或扩消费者 |
| 个别分区严重滞后 | 数据倾斜 / 消费线程卡死 | 查热点 Key、查慢处理 |
| Lag 阶梯式跳跃式上涨 | Rebalance 风暴 | 稳消费者、调超时参数 |
| 消费 TPS 上不去,单条耗时长 | 消费逻辑慢 | 批处理、异步化、降耗时 |
| 突发短时积压后追平 | 上游洪峰 | 临时扩容消费实例 |
四、紧急止血:先让 Lag 降下来
定位清楚后,第一步永远是止血:在业务能接受的前提下,先把 Lag 压下去,再谈根治。两个立竿见影的手段:
# 1) 临时调大单次拉取条数(减少网络往返,提升吞吐)
# Spring Kafka 配置示例
spring.kafka.consumer.max-poll-records=500
spring.kafka.listener.type=batch
# 2) 紧急增加消费者实例(前提是分区数 ≥ 实例数)
# 用 Docker 快速起一个同 Group 的消费者副本
docker run -d --name consumer-extra \
-e GROUP=order-consumer-group \
my-consumer-image:latest
# 3) 若分区确实太少,在线增加分区(注意:仅对新消息生效,老消息仍按旧分区分布)
kafka-topics.sh --bootstrap-server broker1:9092 \
--alter --topic orders --partitions 12
需要特别提醒:增加分区不会重新分布已存在的消息,老消息仍躺在原分区里。如果积压集中在老分区,扩分区只能让新消息更均匀,对存量积压帮助有限,此时更该做的是给老分区对应的消费线程减负(比如临时把老分区的消息路由到独立消费池)。
五、根治:提升消费吞吐的五种手段
手段一:批量消费 + 批量写库
把「一条一条处理、一条一条入库」改成「攒一批、批量落库」,能将数据库往返从 N 次降到 1 次,吞吐往往翻几倍。下面是 Spring Kafka 批量监听的最小示例:
@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>>
batchFactory(ConsumerFactory<String, String> cf) {
ConcurrentKafkaListenerContainerFactory<String, String> f =
new ConcurrentKafkaListenerContainerFactory<>();
f.setConsumerFactory(cf);
f.setBatchListener(true); // 开启批量
return f;
}
@KafkaListener(topics = "orders", groupId = "order-consumer-group",
containerFactory = "batchFactory")
public void consume(List<ConsumerRecord<String, String>> records) {
List<Order> batch = records.stream()
.map(r -> parse(r.value())).collect(Collectors.toList());
orderMapper.batchInsert(batch); // 一次 INSERT 多行
}
手段二:异步化与线程池隔离
把非核心、耗时的动作(如发通知、写审计日志、调外部接口)从消费主链路摘出来,提交到独立线程池异步执行,主链路只做「落库 + 确认」。注意要保证最终一致性,避免消息在异步阶段丢失。
手段三:优化单条处理耗时
用缓存挡掉重复查询、合并远程调用、给慢 SQL 加索引。很多时候消费慢不是 Kafka 的问题,而是消费逻辑里藏着一条没走索引的查询。先 profiling 再动手,别盲目堆机器。
手段四:合理增加分区与消费者
分区数是并行度的硬上限。若消费逻辑已无可优化空间,就按「分区数 = 消费者数」同比例扩容。在 Kubernetes 上用 HPA 按 Lag 指标自动扩缩消费 Pod,能让扩容自动化,不必半夜手动起副本。
手段五:降级与限流非核心链路
大促期间,对可延迟处理的非核心消息(如用户行为埋点)做降级或直接丢弃并补偿,把消费能力集中到订单、支付等核心链路。积压缓解后再回放补偿数据。
| 手段 | 适用根因 | 收益 | 注意点 |
|---|---|---|---|
| 批量消费+批写 | 消费逻辑慢 | 高(降 DB 往返) | 事务边界要设计 |
| 异步化隔离 | 主链路混入慢动作 | 中高 | 保证最终一致 |
| 优化单条耗时 | 慢查询/慢接口 | 视情况 | 先 profiling |
| 扩分区+扩消费者 | 并发不足 | 高 | 分区上限、老消息不重分布 |
| 降级非核心 | 洪峰/资源紧张 | 中 | 需补偿回放 |
六、监控前置:用 Lag 告警避免半夜救火
最好的排查是不让它发生。把 Lag 接入监控,超过阈值就告警,远比等用户投诉强。最常见的方案是用 kafka-exporter 暴露指标,再用 Grafana 配置可视化面板与告警:
# Prometheus 查询:按消费组统计最大分区 Lag
max(kafka_consumer_group_lag{topic="orders",group="order-consumer-group"}) by (partition)
# 告警规则:任意分区 Lag 持续 5 分钟 > 100000 即触发
- alert: KafkaLagTooHigh
expr: kafka_consumer_group_lag{topic="orders"} > 100000
for: 5m
labels:
severity: warning
annotations:
summary: "orders 消费组 Lag 过高"
告警阈值不要拍脑袋,建议取「正常峰值的 3~5 倍」作为预警线,并区分 Warning 与 Critical 两档,避免频繁误报让人麻木。同时把 Rebalance 次数也纳入监控——频繁的再平衡本身就是隐患,可参考 Kafka 再平衡风暴排查 提前规避。
七、避坑清单(30 秒速查)
- 先看 Lag 分布,再下结论:均匀涨、个别涨、阶梯涨,对应完全不同的根因。
- 并行度天花板 = 分区数:消费者多于分区是浪费,分区少于并发需求是瓶颈。
- 扩分区不重分布老消息:别指望加分区能立刻消化存量积压。
- Rebalance 要优先稳住:调大
session.timeout.ms与max.poll.interval.ms,避免被误踢。 - 止血优先于根治:先临时扩容压住 Lag,再回头做批处理、异步化改造。
- 监控前置:Lag + Rebalance 次数都要告警,别等积压成山才反应。
- 消费要幂等:重放、补数据时必须能去重,否则会引入脏数据。
消息积压不可怕,可怕的是没有 Lag 视图、没有预案、每次都靠直觉救火。把上面的诊断流程固化成 SOP,再配上监控告警,Kafka 消息积压就能从「半夜惊魂」变成「五分钟定位」的常规操作。相关稳定性问题也可结合 Docker 磁盘爆满排查 一并纳入你的线上排障手册。




