Skip to content

Kafka 消息积压怎么处理

1. 积压本质

1.1 积压定义

Kafka 消息积压,本质上是 生产速度持续高于消费速度,导致 Consumer GroupLag 持续上升。
只要积压增长时间足够长,就会进一步引发消费延迟、数据过期、重平衡放大、下游雪崩等连锁问题。

积压本身不一定等于故障。
真正需要处理的是“积压是否还在增长、业务是否还能在可接受时间内追平”

1.2 核心指标

1.2.1 必看指标

判断积压是否严重,至少要看 4 个指标:

  • Lag:当前未消费消息数。

  • Lag TrendLag 是否持续增长。

  • Consume TPS:消费者每秒处理消息数。

  • Produce TPS:生产者每秒写入消息数。

其中,Lag 的绝对值不是唯一标准,增长趋势更关键
例如,积压 100000 条但 3 分钟可追平,通常比积压 10000 条但持续扩大更安全。

1.2.2 追平时间

可以用一个简单公式估算恢复时间:

追平时间 = 当前积压量 ÷ (消费速率 - 生产速率)

消费速率 <= 生产速率 时,系统 永远无法自然追平,必须立即扩容、限流或降级。

1.3 常见根因

根因类别 典型现象 本质问题
消费能力不足 Lag 持续上涨,消费者 CPU 不高 分区数不足或实例数不足
单条处理过慢 消费线程正常拉取,但业务执行慢 下游接口、数据库、计算逻辑成为瓶颈
批量参数不合理 单次 poll 太小,吞吐偏低 默认参数偏保守
重平衡频繁 消费者频繁离组、重入组 心跳超时、处理阻塞、实例抖动
坏消息卡死 同一分区 offset 长时间不推进 反序列化失败、业务异常重试死循环
集群侧瓶颈 Broker、磁盘、网络异常 Kafka 服务端负载过高
上游突发流量 短时流量飙升后出现堆积 峰值容量设计不足

2. 排查路径

2.1 先确认影响范围

第一步不是改参数,而是先确认 影响的是单个 Topic、单个 Consumer Group,还是整个集群
如果多个组同时积压,更可能是 Broker、网络、磁盘或共享下游的问题。

建议先执行以下命令查看消费组状态:

kafka-consumer-groups.sh \
  --bootstrap-server kafka1:9092 \
  --group order-consumer-group \
  --describe

重点看以下字段:

  • CURRENT-OFFSET

  • LOG-END-OFFSET

  • LAG

  • CONSUMER-ID

  • HOST

  • CLIENT-ID

如果发现 只有少数分区积压很高,通常说明存在数据倾斜或某个分区被慢消息拖住。
如果 所有分区都高,通常说明整体消费能力不足。

2.2 再定位消费端瓶颈

2.2.1 判断是“拉不动”还是“处理不动”

可以把消费过程拆成两段:

  1. Kafka 拉取消息。

  2. 执行业务处理并提交 offset

排查时要先区分瓶颈在哪一段。
如果 poll 很快但提交慢,问题一般在业务侧。
如果 poll 本身拿不到足够数据,则更可能是参数、分区、网络或 Broker 问题。

2.2.2 重点检查业务链路

业务处理慢,常见有以下几类:

  • 单条消息触发远程调用,且串行执行。

  • 写数据库存在慢 SQL、锁等待、连接池耗尽。

  • 大对象反序列化耗时高。

  • 消费后又同步调用其他系统,链路过长。

  • 异常重试没有上限,导致线程被坏消息长期占住。

这类问题的关键不是继续扩容消费者,而是 先降低单条消息处理成本
否则实例越多,下游越容易被打挂。

2.3 再看集群与 Topic 设计

Kafka 积压并不总是消费者代码问题。
还要检查 Topic 分区数、Broker 负载、磁盘使用率、网络吞吐、ISR 抖动等集群指标。

可以查看 Topic 元数据:

kafka-topics.sh \
  --bootstrap-server kafka1:9092 \
  --topic order-topic \
  --describe

重点观察:

  • 分区数是否足够。

  • 副本分布是否均衡。

  • 是否存在热点分区。

  • 是否频繁出现 UnderReplicatedPartitions

3. 处理策略

3.1 优先止血

3.1.1 限流生产端

Produce TPS 明显高于当前可承载的 Consume TPS 时,最有效的止血手段通常不是先调消费者,而是先限流生产者
否则积压会继续扩大,恢复窗口会越来越长。

适用场景:

  • 上游突发洪峰。

  • 下游数据库或外部接口已接近极限。

  • 消费端扩容需要时间。

3.1.2 暂停非核心链路

如果同一集群承载多个业务,且资源紧张,可以临时降低低优先级任务的消费频率。
核心思路是 把有限资源优先让给关键 Topic 和关键 Consumer Group

这种方式常见于:

  • 日志类、埋点类、统计类消费。

  • 离线任务与实时交易共用资源。

  • 多个消费者共享数据库连接池。

3.1.3 跳过坏消息

坏消息导致某个分区长期卡住时,必须尽快隔离。
否则一个分区的 offset 不推进,会持续放大积压,并拖慢整体追平速度。

建议处理方式:

  • 失败次数超过阈值后写入死信队列 DLQ

  • 记录原始消息体、异常原因、业务主键。

  • 人工或离线任务补偿,不要在线死循环重试。

不要让单条脏数据长期阻塞整个分区。

3.2 提升消费能力

3.2.1 增加消费者实例

最直接的扩容方式,是增加消费者实例数量。
但要注意,消费者实例数的上限受分区数限制

规则如下:

  • 实例数 < 分区数:仍有提升空间。

  • 实例数 = 分区数:基本打满并行度。

  • 实例数 > 分区数:多出来的实例不会消费数据。

因此,扩容前一定要先确认分区数。
否则只是增加机器成本,不会提升吞吐。

3.2.2 增加分区数

当现有分区数不足以支撑并行消费时,需要扩容 Topic 分区。
这通常是提升消费上限的关键动作。

但要注意两点:

  1. 增加分区后,消息顺序只在 单分区内 保证。

  2. 分区扩容可能改变 key 到分区的映射,影响局部有序性和热点分布。

因此,有顺序要求的业务不能只看吞吐,必须同时评估顺序语义是否可接受

3.2.3 批量消费

如果当前是“拉一条,处理一条,提交一次”,吞吐通常会很差。
应尽量改为 批量拉取、批量处理、批量提交,降低网络和提交开销。

典型收益包括:

  • 减少 poll 次数。

  • 减少 commit 次数。

  • 提升数据库批量写入效率。

  • 降低线程切换与对象创建成本。

3.2.4 异步解耦

当消息处理逻辑较重时,可以把消费线程与业务线程解耦。
消费线程只负责拉取和投递到内存队列,业务线程池异步处理。

但要特别注意:

  • 队列容量必须有限制。

  • 必须实现背压。

  • offset 提交必须基于“已处理完成”的边界,而不是“已拉取”。

异步化能提升吞吐,但也最容易引入重复消费和乱序提交问题。

3.3 降低单条处理成本

3.3.1 批量写库替代单条写库

很多积压问题,根本原因不是 Kafka 慢,而是下游数据库被单条写入拖垮。
把单条 insert 改成批量 batch insert,往往比扩容消费者更有效。

3.3.2 去掉同步远程调用

如果消费一条消息要同步调用多个外部服务,吞吐会被 RT 直接锁死。
这时应考虑:

  • 改为本地落库后异步补偿。

  • 合并远程调用。

  • 增加缓存。

  • 缩短事务边界。

3.3.3 缩短事务

长事务会显著拉低消费速度,并放大锁冲突和回滚成本。
建议将 Kafka 消费、数据库写入、外部调用拆分,避免一个大事务包住整条链路。

3.4 参数调优

3.4.1 关键参数解析

参数 作用 调优方向 风险点
max.poll.records 单次拉取返回的最大记录数 适当调大,提升批量处理能力 过大可能导致单批处理时间过长
fetch.min.bytes 服务端最少返回字节数 调大可提升吞吐 低流量时可能增加等待
fetch.max.wait.ms 等待凑批的最长时间 fetch.min.bytes 配合调优 过大可能增加延迟
enable.auto.commit 是否自动提交位点 建议关闭,手动提交 自动提交易产生消息丢失或重复
max.poll.interval.ms 两次 poll 最大间隔 处理耗时长时需要调大 过小会频繁触发重平衡
session.timeout.ms 会话超时时间 网络抖动场景可适当调大 过大影响故障发现速度
heartbeat.interval.ms 心跳间隔 一般小于 session.timeout.ms / 3 配置不当导致误判离组

3.4.2 推荐调优顺序

参数调优不要一把梭。
建议按以下顺序推进:

  1. 先确认瓶颈是不是业务处理。

  2. 再调整 max.poll.records

  3. 再看是否需要增大 max.poll.interval.ms

  4. 最后再联合调整 fetch.min.bytesfetch.max.wait.ms

参数调优只能放大已有能力,不能替代架构修复。

3.5 极端场景处置

3.5.1 临时重置 offset

当消息积压已经超过业务可恢复窗口,且历史消息允许丢弃或离线补偿时,可以考虑重置 offset
这是高风险操作,只适合明确接受数据跳过的场景。

示例命令如下:

kafka-consumer-groups.sh \
  --bootstrap-server kafka1:9092 \
  --group order-consumer-group \
  --topic order-topic \
  --reset-offsets \
  --to-latest \
  --execute

该操作会直接跳过未消费消息。
执行前必须确认:

  • 业务是否允许丢数据。

  • 是否已完成数据备份。

  • 是否有补偿链路。

  • 是否已在预发环境验证命令效果。

3.5.2 数据迁移与旁路消费

极端积压下,也可以临时启动旁路消费集群,将历史消息转储或分流处理。
这种方式适合:

  • 正常消费者无法快速追平。

  • 历史消息需要保留。

  • 核心实时流必须先恢复。

4. 实战方案

4.1 标准处理流程

4.1.1 5 步闭环

遇到消息积压时,建议按以下顺序执行:

  1. 确认积压规模、增长趋势、预计追平时间。

  2. 判断瓶颈在生产端、消费端、下游还是集群侧。

  3. 先做止血动作,例如限流、隔离坏消息、暂停低优任务。

  4. 再做能力提升,例如扩容实例、增加分区、批量处理。

  5. 最后补监控、补限流、补 Runbook,避免再次发生。

这种顺序的核心,是 先阻止问题继续恶化,再处理根因
一上来改参数,往往只会延误恢复时间。

4.2 方案对比

方案 见效速度 适用场景 风险
限流生产端 上游流量突发 影响写入吞吐
增加消费者实例 分区数足够时 可能压垮下游
增加分区数 并行度受限 影响顺序与路由
批量消费 单条处理开销高 需要改代码
异步线程池 业务逻辑重 容易引入位点提交问题
死信队列 DLQ 坏消息阻塞 需要补偿机制
重置 offset 极快 接受跳过历史数据 高风险,可能丢数

5. 预防机制

5.1 监控体系

5.1.1 必配监控项

要避免“发现时已经积压很多”,必须对以下指标设告警:

  • Consumer Lag

  • Lag 增速

  • Consume TPS

  • Produce TPS

  • 单条处理耗时

  • 批处理耗时

  • 重平衡次数

  • 死信队列写入量

  • 数据库 RT、失败率、连接池使用率

其中,仅监控 Lag 绝对值是不够的
更有效的是监控“Lag 增速 + 追平时间”。

5.2 容量规划

容量评估至少要覆盖以下问题:

  • 峰值 TPS 是平时的多少倍。

  • 单条消息平均处理耗时是多少。

  • 当前分区数能提供多少并行度。

  • 下游数据库和外部服务最多能承受多少请求。

建议预留至少 2 倍以上的突发冗余。
没有容量冗余的系统,遇到活动、补数、故障恢复时几乎一定会积压。

5.3 数据模型优化

消息体过大、字段冗余、反序列化复杂,也会直接降低吞吐。
因此在设计消息协议时,应尽量做到:

  • 只放必要字段。

  • 避免深层嵌套对象。

  • 避免把大文本、大数组直接塞进消息体。

  • 能传主键就不要传整对象。