4. 消息队列采集异常排查:消息积压、重复消费、序列化失败问题

消息队列这东西,用好了是数据高速公路,用不好就是堵车现场。我在数据采集系统里跟 Kafka、RocketMQ 打交道好几年了,说实话,踩过的坑比吃过的盐还多。今天咱们就聊聊三个最让人头疼的问题:消息积压、重复消费、序列化失败。

4.1 消息积压:到底是谁在拖后腿?

消息积压,说白了就是生产速度大于消费速度。我遇到过最夸张的一次,Kafka 里积压了上亿条消息,消费者 lag 高达几百万。为什么会这样?

核心排查思路:

  • 消费者处理能力不足:单线程消费,或者业务逻辑太慢
  • 分区分配不均:某些分区消费者负载过高
  • 下游系统瓶颈:写入数据库太慢,或者调用外部接口超时
  • 消息体过大:一条消息几 MB,网络传输和反序列化都慢

我个人习惯,遇到积压先看消费者 lag 监控。Kafka 自带的 kafka-consumer-groups 命令就能搞定:

# 查看消费者组 lag
kafka-consumer-groups --bootstrap-server localhost:9092 \
  --group my-consumer-group --describe

# 输出示例
GROUP           TOPIC           PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
my-group        data-topic      0          1000            1500            500
my-group        data-topic      1          1200            1800            600

你看,每个分区的 lag 一目了然。如果某个分区 lag 特别高,那可能就是分区分配不均。

我的经验:曾经有个项目,消费者处理每条消息都要查一次数据库,结果数据库连接池太小,导致大量线程阻塞。后来改成批量消费 + 批量写入,积压问题直接解决了。

4.2 重复消费:数据怎么多了一份?

重复消费,这是消息队列的「原罪」之一。你想想看,为了保证消息不丢,很多系统都会在消费成功后手动提交 offset。但如果提交失败了,下次重启就会重新消费。

我遇到过最坑的一次:消费者处理完消息后,在提交 offset 之前进程挂了。重启后,同一批消息又被消费了一遍。结果数据库里多了好几万条重复数据。

避坑指南:我曾经以为「幂等性」是可有可无的,直到被重复数据搞到凌晨三点。现在我的每个消费者都会做幂等处理。

解决重复消费,常用的方法有:

  • 业务幂等:在数据库里加唯一索引,重复插入直接报错
  • 去重表:用 Redis 或数据库记录已消费的消息 ID
  • 事务消息:RocketMQ 支持,但性能会下降

代码示例,用 Redis 做去重:

// 消费前检查是否已处理
String messageId = record.key();
Boolean isProcessed = redisTemplate.opsForValue()
    .setIfAbsent("msg:" + messageId, "1", 1, TimeUnit.HOURS);

if (Boolean.FALSE.equals(isProcessed)) {
    // 已处理过,跳过
    log.warn("重复消息,跳过: {}", messageId);
    return;
}

// 处理业务逻辑
processMessage(record.value());

// 提交 offset
consumer.commitSync();

嗯,这里要注意:Redis 的过期时间要设置合理。太短可能漏掉重复,太长又浪费内存。我一般设 1 小时,够用了。

4.3 序列化失败:数据格式不对,解析不了

序列化失败,说白了就是生产者和消费者用的协议不一致。我见过最离谱的:生产者用 JSON 序列化,消费者却用 Avro 反序列化,结果一堆乱码。

常见的序列化问题:

问题类型 典型表现 排查方法
字段不匹配 反序列化时字段缺失或类型不对 对比生产者和消费者的 schema
编码问题 中文乱码,特殊字符解析失败 检查字符编码,统一用 UTF-8
版本兼容 升级了序列化库,但消费者没升级 检查依赖版本,做好兼容性测试
数据损坏 消息在传输过程中被截断或篡改 用工具解析原始字节流

我个人习惯,在生产者端加一个「自检」逻辑:

// 生产者发送前,先自己反序列化一次
byte[] data = serializer.serialize(message);
try {
    Object obj = deserializer.deserialize(data);
    // 自检通过,再发送
    producer.send(new ProducerRecord<>(topic, data));
} catch (Exception e) {
    log.error("序列化自检失败,消息丢弃: {}", message);
    // 记录到死信队列
    deadLetterQueue.send(message);
}

我的经验:曾经有个项目,生产者和消费者用的 JSON 库版本不同,导致日期格式解析失败。后来统一用 Jackson,并且定义了严格的日期格式规范。

4.4 知识体系总览

下面这张图,是我梳理的消息队列异常排查的核心逻辑。你看一眼,心里就有谱了。

消息队列采集异常排查核心逻辑 消息积压 重复消费 序列化失败 排查方法 • 查看消费者 lag 监控 • 检查分区分配是否均衡 • 分析下游系统瓶颈 解决方案 • 业务幂等设计 • 去重表(Redis/DB) • 事务消息 预防措施 • 统一序列化协议 • 生产者自检机制 • 版本兼容性测试 核心原则:监控先行 → 定位根因 → 幂等兜底 → 预防为主

你看,这三个问题其实是有内在联系的。消息积压可能导致重复消费(因为超时重试),序列化失败也可能引发积压(消费线程卡住)。所以排查的时候,别孤立地看问题。

总结一下我的经验:

  • 消息积压:先看 lag,再看消费者处理能力,最后看下游
  • 重复消费:幂等是底线,去重是手段,事务是备选
  • 序列化失败:统一协议,加自检,做好版本管理

嗯,这些坑我都踩过。希望你能少走弯路。

专注资料整理