Kafka 消费者组积压,订单为何延迟两小时才处理?

问题背景

公司订单处理中心采用 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)机制隔离坏消息

预防措施

  1. 消费者兼容性优先:所有新字段上线前,消费者必须先配置 ignorableUnknownProperties
  2. 消息 Schema 版本管理:引入 Avro + Schema Registry,在写入阶段就能校验兼容性
  3. 死信队列:所有消费者都配置 DLQ,三次重试失败自动转入死信队列
  4. 重试上限:禁止 max-attempts: -1,强制设置上限(推荐 3-10 次)
  5. lag 梯度告警:lag > 1000 告警(目前有),lag 增长率 > 100/分钟 紧急告警(需新增)
  6. 变更通知机制:任何涉及消息格式的变更,需在 Kafka 管理群中通知所有消费者负责方

总结

Kafka 消息积压的根因往往不在 broker,而在消费端——不是消费者挂了,就是消费者处理不了某条消息。这次故障的教训是双重的:技术层面,Jackson 的 FAIL_ON_UNKNOWN_PROPERTIES 应该作为 Kafka 消费者的"强制关闭项";流程层面,消息格式变更必须有跨团队的兼容性审查。当一个字段上线导致 12 万条消息被丢弃,这笔账要算在整个发布流程上,而不只是写 bug 的开发者。

使用 Hugo 构建
主题 StackJimmy 设计