外观
案例:Kafka 消息积压两百万
现象
下游对账系统数据延迟从 1 分钟涨到 4 小时。监控显示消费组 lag 持续增长。
bash
/opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server 10.0.0.31:9092 \
--describe --group order-synctext
GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST
order-sync order 0 1842000 2284000 442000 consumer-order-sync-1-8f3a /10.0.0.71
order-sync order 1 1798000 2248000 450000 consumer-order-sync-2-a1b2 /10.0.0.72
order-sync order 2 1805000 2255000 450000 consumer-order-sync-3-c3d4 /10.0.0.73
order-sync order 3 1810000 2260000 450000 consumer-order-sync-4-e5f6 /10.0.0.74总积压约 179 万,而且还在涨。
定位过程
第一步:消费者数量和分区数是否匹配
bash
/opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server 10.0.0.31:9092 \
--describe --group order-sync --memberstext
GROUP CONSUMER-ID HOST #PARTITIONS
order-sync consumer-order-sync-1-8f3a /10.0.0.71 1
order-sync consumer-order-sync-2-a1b2 /10.0.0.72 1
order-sync consumer-order-sync-3-c3d4 /10.0.0.73 1
order-sync consumer-order-sync-4-e5f6 /10.0.0.74 14 个消费者,topic 只有 4 个分区 —— 并行度已经打满,加实例也没用。
第二步:单条处理耗时多少
应用日志里加埋点后:
text
2026-10-28 03:20:11 INFO consume order=88213 cost=88ms
2026-10-28 03:20:12 INFO consume order=88214 cost=92ms
2026-10-28 03:20:13 INFO consume order=88215 cost=89ms每条 88~92ms。四台机器、每台一个线程,吞吐约:
text
4 线程 × (1000 / 90) ≈ 44 条/秒生产端 QPS 约 120,消费只有 44 —— 缺口 76 条/秒,积压必然增长。
第三步:90ms 花在哪
bash
grep -A 3 'consume order=88213' /var/log/order-sync/app.logjava
// 处理逻辑
Order o = parse(record.value());
Account acct = accountClient.get(o.getUserId()); // 同步 HTTP,60ms
BigDecimal rate = rateService.getRate(o.getCurrency()); // 同步 HTTP,20ms
repo.save(o, acct, rate); // 5ms两次同步 HTTP 调用占了 80ms。
第四步:有没有毒丸消息
bash
/opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server 10.0.0.31:9092 \
--topic order --partition 0 --offset 1842000 --max-messages 5 \
--property print.key=true --property print.timestamp=truetext
CreateTime:1761570000000 order-88213 {"id":88213,...}
CreateTime:1761570001000 order-88214 {"id":88214,...}数据本身没问题,排除毒丸。
处置
立即止血
- 加分区 + 加消费者(不要求同 key 严格顺序时可行):
bash
/opt/kafka/bin/kafka-topics.sh --bootstrap-server 10.0.0.31:9092 \
--alter --topic order --partitions 24text
WARNING: If partitions are increased for a topic that has a key, the partition logic or ordering of the messages will be affected
Adding partitions succeeded!bash
# 扩容消费实例到 12 个(每个 2 线程)
kubectl -n shop scale deploy/order-sync --replicas=12并行度从 4 提到 24,消费能力约 264 条/秒,可以用 3~4 小时追平积压。
- 临时提高批处理大小:
yaml
spring:
kafka:
consumer:
max-poll-records: 200扩分区会改变 key 的分区归属
如果业务要求同一订单严格顺序,扩分区会导致同一 key 之前和之后的消息落到不同分区,顺序被打乱。
必须扩的话,先确认下游能容忍乱序,或者临时切到"按订单 ID 分区数不变"的临时 topic。
根治
- 把同步 HTTP 改成批量/异步:
java
// 批量查
Map<Long, Account> accounts = accountClient.batchGet(userIds);
Map<String, BigDecimal> rates = rateService.batchGet(currencies);一个 100 条的批次只需要 2 次调用,而不是 200 次。
本地缓存 + 异步刷新:账户、汇率这类变化不频繁的数据,用 Caffeine 缓存 5 分钟。
处理与提交解耦:
yaml
spring:
kafka:
listener:
ack-mode: manual_immediate处理成功后立即提交,避免 rebalance 导致重复。
- 毒丸消息进 DLQ:
java
try {
handle(record);
} catch (Exception e) {
producer.send(new ProducerRecord<>("order.dlq", record.key(), record.value()));
log.error("poison message, sent to dlq", e);
}否则一条坏消息会让整个分区卡住(配合无限重试)。
复盘
| 问题 | 根因 | 改进 |
|---|---|---|
| 积压 200 万 | 消费 44/s,生产 120/s | 并行度 + 批量化 |
| 加实例无效 | 分区数 = 消费者数 | 扩分区 |
| 单条 90ms | 两次同步 HTTP | 批量查询 + 缓存 |
| 没有积压告警 | 只监控了 broker | 加 consumer lag 告警 |
| 追平慢 | 吞吐上限固定 | 临时扩容 + 峰值预案 |
bash
# lag 告警(每 5 分钟)
/opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server $B --all-groups --describe \
| awk 'NR>1 && $6 ~ /^[0-9]+$/ {s[$1]+=$6} END {for (g in s) if (s[g] > 100000) print g, s[g]}'积压时的标准顺序
- 消费者数 < 分区数?→ 直接加实例。
- 消费者数 = 分区数?→ 扩分区(注意顺序)。
- 单条处理慢?→ 批量化/异步化。
- 有坏消息反复重试?→ DLQ。
先看第 1、2 条,成本最低;改造代码是最后手段。