问题背景
公司订单处理中心采用 Kafka 作为消息中间件,上游订单服务将新订单写入 Topic order-events(16 分区),下游 3 个消费者服务(订单处理、库存扣减、通知推送)共用同一个消费者组 order-processor-group。日均消息量约 50 万条,正常 lag < 200。
故障现象
10 月 11 日下午 4:00,运维收到订单积压告警:超过 500 笔订单创建超过 30 分钟但未进入"处理中"状态。登录 Kafka 监控发现:
1
2
3
4
5
6
7
8
9
|
$ kafka-consumer-groups --bootstrap-server kafka01:9092 \
--group order-processor-group --describe
GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
order-processor-group order-events 0 14520321 14552350 32029
order-processor-group order-events 1 13890234 13945021 54787
...
order-processor-group order-events 15 15230874 15283456 52582
# 总lag: 856,234条
|
消费者组 lag 从正常的 200 以下飙升至 85 万条,所有 16 个分区全线积压。但 Kafka broker 的 CPU、磁盘 IO 均正常,说明问题出在消费端。
排查过程
第一步:检查消费者服务状态
1
2
3
4
5
|
$ kubectl get pods -l app=order-processor
NAME READY STATUS RESTARTS AGE
order-processor-6d7f8b9c4-abc12 1/1 Running 23 (1m ago) 2m
order-processor-6d7f8b9c4-def34 1/1 Running 18 (2m ago) 2m
order-processor-6d7f8b9c4-ghi56 1/1 Running 25 (30s ago) 2m
|
容器频繁重启(累计 23、18、25 次),每次存活不到 2 分钟。这就是 lag 爆炸的原因:消费者不断崩溃重启,根本没有在消费。
第二步:查看消费者日志
1
2
3
4
5
6
|
2024-10-11 16:03:21.456 ERROR [pool-2-thread-1] c.c.order.consumer.OrderConsumer -
Failed to deserialize message, partition=3, offset=13981234
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException:
Unrecognized field "paymentChannel" (class com.company.order.dto.OrderMessage),
not marked as ignorable (5 known properties: "orderId", "userId", "amount",
"createTime", "payMethod"])
|
消费者在反序列化 Kafka 消息时抛出了 UnrecognizedPropertyException——消息体中有字段 paymentChannel,但消费者代码中的 DTO 类没有定义这个字段。
第三步:追溯变更
检查 Git 提交记录:
1
2
3
4
5
|
commit a1b2c3d (2 hours ago)
Author: dev-zhang
feat: 订单DTO增加paymentChannel字段支持多渠道支付
commit f4e5g6h (1 day ago)
|
上游订单服务在下午 3:45 发布了新版本,在 OrderMessage DTO 中新增了 paymentChannel 字段。消息写入 Kafka 时该字段被序列化,但 消费者服务并未同步更新——消费者代码中的 DTO 没有 paymentChannel 字段,默认 Jackson 反序列化策略是 FAIL_ON_UNKNOWN_PROPERTIES。
第四步:为什么没有触发消费者重试/跳过?
1
2
3
4
5
6
|
# consumer配置
spring.kafka.consumer:
max-poll-records: 500
enable-auto-commit: false # 手动提交
max-attempts: -1 # 无限重试!
backoff-delay: 3000
|
消费者配置了 max-attempts: -1(无限重试),且使用手动提交 enable-auto-commit: false。每批 poll 到 500 条消息中只要包含一条新格式的消息,反序列化失败 → 抛出异常 → 不提交 offset → 下次 poll 到同一批消息 → 再次失败 → 死循环。
解决方案
紧急处理:重启消费者 + 跳过问题消息
1
2
3
4
5
6
|
# 1. 临时将消费者组的offset跳过问题分区中的问题消息
kafka-consumer-groups --bootstrap-server kafka01:9092 \
--group order-processor-group --topic order-events \
--partition 3 --reset-offsets --to-latest --execute
# 对每个有问题的分区重复操作(约12万条消息被丢弃,但业务可补单)
|
修复消费者代码
1
2
3
4
5
6
|
// 方案一:忽略未知字段(最快)
@JsonIgnoreProperties(ignoreUnknown = true)
public class OrderMessage { ... }
// 方案二:配置全局Jackson策略
spring.jackson.deserialization.FAIL_ON_UNKNOWN_PROPERTIES=false
|
修复重试策略
1
2
3
4
|
spring.kafka.consumer:
max-attempts: 3 # 最多重试3次后跳过
backoff-delay: 3000
retry-topic: order-events-dlq # 失败消息进死信队列
|
根因分析
直接原因:上游服务新增序列化字段后未同步更新消费者 DTO,Jackson 默认拒绝未知字段,消费者反复失败无法提交 offset,形成消息积压雪崩。
深层原因:
- 上下游服务未遵循消费者优先的变更发布策略(先更新消费者兼容新字段,再更新生产者)
- Jackson 默认的 FAIL_ON_UNKNOWN_PROPERTIES 策略对 Kafka 消费者过于严格
- 无限重试 + 手动提交 = 单条坏消息可以永久阻塞整个分区
- 缺少死信队列(DLQ)机制隔离坏消息
预防措施
- 消费者兼容性优先:所有新字段上线前,消费者必须先配置
ignorableUnknownProperties
- 消息 Schema 版本管理:引入 Avro + Schema Registry,在写入阶段就能校验兼容性
- 死信队列:所有消费者都配置 DLQ,三次重试失败自动转入死信队列
- 重试上限:禁止
max-attempts: -1,强制设置上限(推荐 3-10 次)
- lag 梯度告警:lag > 1000 告警(目前有),lag 增长率 > 100/分钟 紧急告警(需新增)
- 变更通知机制:任何涉及消息格式的变更,需在 Kafka 管理群中通知所有消费者负责方
总结
Kafka 消息积压的根因往往不在 broker,而在消费端——不是消费者挂了,就是消费者处理不了某条消息。这次故障的教训是双重的:技术层面,Jackson 的 FAIL_ON_UNKNOWN_PROPERTIES 应该作为 Kafka 消费者的"强制关闭项";流程层面,消息格式变更必须有跨团队的兼容性审查。当一个字段上线导致 12 万条消息被丢弃,这笔账要算在整个发布流程上,而不只是写 bug 的开发者。