阅读时间:约 45 分钟 | 难度:★★★★☆ | 前置知识:Kafka 基础架构、消费者组模型、分区机制
1. 问题定义:什么是消息积压,为什么它是"一号警情"?

Kafka 消息积压(Consumer Lag),是指生产者写入的 Offset 与消费者提交的 Offset 之间的差值持续增长,导致消息在 Broker 端堆积,消费延迟不断恶化。
在 Kafka 的存储模型中,消息按分区(Partition)以 Offset 严格有序排列。每个消费者组在每个分区上维护一个消费位移(Offset),积压量就是这个位移与分区最新写入位置之间的"鸿沟":
积压量(Lag) = Log End Offset(LEO) - Consumer Offset
为什么积压是"一号警情"?
线上系统对消息积压的容忍度极低,因为它会引起连锁反应:
- 业务延迟放大:用户下单后 30 秒没收到确认,体验已经很难接受;积压意味着这条链路可能延长到分钟甚至小时级别。
- 资源耗尽:积压消息占用 Broker 磁盘,Kafka 的磁盘默认按时间(
retention.ms)或大小(retention.bytes)清理,但积压期间清理策略可能失效,导致 Broker 磁盘打满。 - 消费者 OOM:消费端拉取大批量积压消息时,如果处理能力跟不上,内存中堆积的未处理消息可能导致 Full GC 甚至 OOM。
- 级联故障:消费链路下游(数据库、缓存、下游服务)被积压释放的流量冲垮,形成雪崩。
2. 积压发现:如何第一时间感知并定位积压?

积压治理的第一步是可观测性——你无法治理你看不见的问题。
2.1 消费 Lag 的监控维度
Kafka 消费 Lag 的监控需要从三个层次覆盖:
| 层次 | 指标 | 工具 | 用途 |
|---|---|---|---|
| Broker 层 | kafka.consumer_groups:max_lag |
JMX + Prometheus | 全局视角,发现哪个消费者组积压最高 |
| 分区层 | 每个 Partition 的 Lag 值 | kafka-consumer-groups.sh |
定位积压集中在哪些分区 |
| 应用层 | 消费延迟(端到端时间戳差值) | 应用埋点 + 业务时间戳 | 从业务视角度量消息从生产到消费的真实耗时 |
2.2 命令行工具:kafka-consumer-groups
最直接的排查手段:
# 查看所有消费者组的 Lag 概况
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
# 查看特定消费者组的详细 Lag(含每个分区)
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group order-consumer-group --describe
# 输出示例:
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# order-topic 0 1523400 1525890 2490
# order-topic 1 1521000 1525890 4890 ← 分区1积压严重
# order-topic 2 1525800 1525890 90
2.3 编程式监控:JMX 与 Metrics
在生产环境中,建议通过 JMX 暴露的 MBean 指标接入 Prometheus:
kafka.consumer:type=consumer-fetch-manager-metrics,client-id=*
→ records-lag-max (最大积压)
→ records-lag-avg (平均积压)
配合 Prometheus + Grafana 面板,设置分级告警:
- Warning:Lag > 5000 持续 2 分钟
- Critical:Lag > 50000 持续 1 分钟
- Emergency:Lag > 200000 或增长率 > 1000/min
2.4 业务层埋点:消息时间戳差值
Broker 层的 Lag 只能告诉你"消息有没有被消费",但无法告诉你"消费结果有没有生效"。更精准的做法是在消息体中携带生产时间戳,消费端处理完成后计算差值并上报:
long produceTime = record.timestamp(); // Kafka 消息自带的 timestamp
long consumeTime = System.currentTimeMillis();
metricsCollector.recordLag(consumeTime - produceTime);
3. 积压根因分析:不是所有积压都是"消费太慢"

面对积压,最常见的反应是"加机器、加消费者"。但这很可能是在用战术上的勤奋掩盖战略上的懒惰。积压的根因需要系统化排查:
3.1 根因分类
| 根因类型 | 现象 | 排查方向 |
|---|---|---|
| 生产端突增 | 上游流量短时间内暴增(如大促、秒杀) | 检查生产端 TPS 变化曲线 |
| 消费端变慢 | 消费 TPS 下降,但生产 TPS 正常 | 检查消费逻辑中的慢调用(DB、RPC、缓存) |
| 分区倾斜 | 总 Lag 高,但只有少数分区有 Lag | 检查分区键(Key)是否导致数据倾斜 |
| Rebalance 风暴 | 消费者频繁加入/退出,Lag 波动剧烈 | 检查 max.poll.interval.ms、心跳超时配置 |
| 下游阻塞 | 消费者处理能力正常,但下游(DB/Redis)响应慢 | 检查下游 RT、连接池、锁竞争 |
| 资源瓶颈 | 消费者 CPU/内存/网络打满 | 检查消费者实例资源使用情况 |
3.2 分区倾斜——最隐蔽的积压原因
Kafka 的消息按 Key 的哈希分配分区,默认策略是 murmur2(key) % partition_count。如果业务 Key 分布不均(如某个热点商家、某个大用户),会导致部分分区积压严重,其他分区消费正常。
排查方法:
# 查看每个分区的 Lag 分布
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group my-group --describe | awk '{if(NR>1) print $3, $6}' | sort -k2 -rn
如果发现标准差(StdDev)极大,说明存在分区倾斜。
解决思路:
- 重新设计分区 Key,加入更多维度打散(如
userId + timestamp) - 在生产者端使用自定义 Partitioner
- 增加分区数(但要注意分区数只能增不能减)
4. 扩容策略:从"加机器"到"精准扩容"

4.1 消费者扩容的约束
Kafka 消费者组的扩容受限于分区数——一个分区在同一消费者组内只能被一个消费者实例消费。因此:
最大有效消费者数 = 分区数
举例:某 Topic 有 8 个分区,消费者组有 3 个实例,每个实例分配 2-3 个分区。如果你把消费者实例加到 10 个,多余的 2 个实例将处于空闲状态,不会消费任何消息。
4.2 扩容决策树
积压发生
│
├─ 分区数 > 当前消费者数?
│ ├─ 是 → 增加消费者实例(最快速)
│ └─ 否 → 继续
│
├─ 消费逻辑有优化空间?
│ ├─ 是 → 优化消费逻辑(批量处理、异步化、去重)
│ └─ 否 → 继续
│
├─ 可以增加分区数?
│ ├─ 是 → 增加分区 + 增加消费者(注意:有 Key 的消息会重新分配)
│ └─ 否 → 继续
│
└─ 紧急 → 启动应急消费程序(见第 7 节)
4.3 增加分区的注意事项
增加分区是一个不可逆操作(Kafka 不支持减少分区),且对有 Key 的消息有副作用:
- 无 Key 消息:消息会重新均匀分布到新分区,无业务影响
- 有 Key 消息:同一个 Key 的消息可能被分配到不同分区,破坏同一 Key 的顺序性保证
因此,对有顺序性要求的 Topic,增加分区前必须评估业务影响。
# 增加分区(从 8 → 16)
kafka-topics.sh --bootstrap-server localhost:9092 \
--alter --topic order-topic --partitions 16
4.4 消费者扩容的最佳实践
// 消费者配置要点
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "order-consumer-group");
props.put("enable.auto.commit", "false"); // 手动提交,避免重复消费
props.put("max.poll.records", "500"); // 单次拉取记录数,根据处理能力调优
props.put("max.poll.interval.ms", "300000"); // 两次 poll 最大间隔,超时触发 Rebalance
props.put("session.timeout.ms", "30000"); // 心跳超时
props.put("heartbeat.interval.ms", "10000"); // 心跳间隔,应小于 session.timeout.ms 的 1/3
5. 并行度调优:在单消费者内部榨取更多性能

扩容消费者实例是最简单的方法,但受限于分区数。如果分区数已经等于消费者数,继续提升消费能力的唯一途径是提高单消费者内部的并行度。
5.1 单线程串行消费模型
Kafka 原生 Consumer API 是单线程模型——poll() 之后逐条处理消息。这种模型下,消费 TPS 等于单条消息处理时间的倒数:
TPS = 1 / 平均处理时间(秒)
如果每条消息处理耗时 50ms(含 DB 写入、RPC 调用),单线程 TPS 只有 20。
5.2 多线程消费模型
在单消费者实例内,可以有多种并行化策略:
方案 A:消费者拉取 + 线程池处理(推荐)
ExecutorService executor = Executors.newFixedThreadPool(20);
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
List<Future<?>> futures = new ArrayList<>();
for (ConsumerRecord<String, String> record : records) {
futures.add(executor.submit(() -> {
processMessage(record); // 业务处理
}));
}
// 等待所有任务完成后再提交 Offset
for (Future<?> future : futures) {
future.get(); // 阻塞等待
}
consumer.commitSync(); // 全部成功后才提交
}
关键点:
- 必须在所有异步任务完成后才能提交 Offset,否则消费者崩溃会导致消息丢失
- 线程池大小需根据 CPU 核数和 I/O 等待时间综合计算
方案 B:分区级别并行(每个分区一个线程)
// 手动 assign 分区,每个分区一个处理线程
consumer.assign(Arrays.asList(
new TopicPartition("order-topic", 0),
new TopicPartition("order-topic", 1)
));
// 每个分区使用独立线程处理
for (TopicPartition tp : consumer.assignment()) {
executor.submit(() -> {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records.records(tp)) {
processMessage(record);
}
}
});
}
适用场景:分区数较少,需要保证每个分区内的顺序消费,且不需要消费者组的自动 Rebalance。
方案 C:批量消费 + 批量写入
如果你的消费逻辑是写入数据库,单条写入效率极低。改为批量聚合:
List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
buffer.add(record);
if (buffer.size() >= 200) {
batchInsertToDB(buffer); // 批量写入
buffer.clear();
consumer.commitSync();
}
}
}
效果:批量写入可以将数据库写入吞吐量从 20 TPS 提升到 1000+ TPS。
5.3 并行度公式
合理的线程数可以通过以下公式估算:
线程数 = CPU核数 × (1 + 平均等待时间 / 平均计算时间)
对于 I/O 密集型消费(如 DB 写入),等待时间 >> 计算时间,线程数可以设置较大(如 20-50)。但要注意下游资源的承受能力——不要为了消积压把下游打垮。
6. 限流策略:给消费降速,保护下游

消积压时,一个常见的错误是"用力过猛"——消费者全力消费,结果把下游数据库、缓存、下游服务全部打垮,引发更大的故障。
6.1 为什么消积压需要限流?
积压状态下,消费者的消费速度远超平时。假设平时消费 TPS 是 100,积压后消费者扩容到 10 个实例,单实例内部又开了 20 个线程,消费 TPS 可能飙到 2000+。下游数据库平时承担的写入 TPS 是 100,突然承受 2000 TPS,大概率直接打挂。
限流的本质是:在消积压的同时,保护下游不被冲垮。
6.2 消费端限流方案
方案 A:Guava RateLimiter(单机限流)
RateLimiter rateLimiter = RateLimiter.create(500.0); // 每秒 500 条
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
rateLimiter.acquire(); // 阻塞直到获取到令牌
processMessage(record);
}
consumer.commitSync();
}
优点:简单直接,Guava 开箱即用。 缺点:单机限流,无法跨实例感知全局消费速率。
方案 B:Sentinel / Resilience4j(分布式限流)
// Sentinel 流控规则
FlowRule rule = new FlowRule();
rule.setResource("kafka-consume");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(500); // QPS 上限
FlowRuleManager.loadRules(Collections.singletonList(rule));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
Entry entry = null;
try {
entry = SphU.entry("kafka-consume");
processMessage(record);
} catch (BlockException e) {
// 被限流,可以选择等待后重试
Thread.sleep(10);
} finally {
if (entry != null) entry.exit();
}
}
}
方案 C:动态限流(基于下游负载自适应)
更高级的做法是根据下游数据库的实时负载动态调整消费速度:
// 伪代码
while (true) {
double dbLoad = getDatabaseLoad(); // 查询数据库 CPU 使用率、连接数等
double rate = calculateRate(dbLoad); // 根据负载计算合适的消费速率
rateLimiter.setRate(rate); // 动态调整限流速率
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
// ... 限流消费
}
6.3 限流不应该"一刀切"
在限流时,需要区分消息的优先级:
- 核心业务消息(如订单支付):给予较高限流阈值,优先消费
- 非核心消息(如日志、通知):给予较低限流阈值,必要时可丢弃
这需要在 Topic 设计层面做隔离——核心业务和非核心业务使用不同的 Topic。
7. 应急消费:当一切常规手段都失效时

当积压量过大(百万级甚至千万级),常规手段(扩容、并行调优)的回血速度不够快时,需要启用应急消费模式。
7.1 应急消费的核心理念
应急消费的核心原则是:用最短时间将积压消除,即使牺牲部分非核心业务逻辑。 这不是一个优雅的解决方案,但它是一个有效的止损手段。
7.2 方案 A:跳过非核心逻辑
在消费代码中增加"应急模式"开关,跳过耗时但非关键的处理:
boolean emergencyMode = isEmergencyMode(); // 从配置中心读取
void processMessage(ConsumerRecord<String, String> record) {
// 核心逻辑:必须执行
saveOrderToDB(record);
if (!emergencyMode) {
// 非核心逻辑:应急模式下跳过
sendNotification(record); // 发送通知
updateSearchIndex(record); // 更新搜索索引
writeAuditLog(record); // 写审计日志
callAnalyticsService(record); // 调用分析服务
}
}
效果:将单条消息处理时间从 50ms 降到 10ms,消费 TPS 提升 5 倍。
7.3 方案 B:临时消费者组(影子消费)
启动一个独立的消费者组,将积压消息分发给这个临时组,利用其快速消费后直接丢弃或写入临时存储:
// 临时消费者组,从 earliest 开始消费所有积压消息
Properties props = new Properties();
props.put("group.id", "emergency-consumer-group-" + System.currentTimeMillis());
props.put("auto.offset.reset", "earliest"); // 从最早开始
// 消费后直接写入临时存储(如 Redis),后续由正常消费者组异步处理
KafkaConsumer<String, String> emergencyConsumer = new KafkaConsumer<>(props);
emergencyConsumer.subscribe(Arrays.asList("order-topic"));
while (lag > 0) {
ConsumerRecords<String, String> records = emergencyConsumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
// 快速写入 Redis,不做过多的业务逻辑
redis.setex("backlog:" + record.key(), 3600, record.value());
}
}
注意:这个方案需要确保正常消费者组和临时消费者组不会重复消费同一条消息,或者业务层做好幂等。
7.4 方案 C:重置 Offset(弃车保帅)
当积压消息已经失去时效性(如 30 分钟前的库存更新消息),消费它们已经没有业务价值,反而会浪费资源。此时可以选择直接跳过:
# 将消费者组的 Offset 重置到最新位置(丢弃所有积压)
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group order-consumer-group \
--topic order-topic \
--reset-offsets --to-latest --execute
这个操作不可逆,必须谨慎。使用前确认:
- 积压消息的业务时效性已过
- 有对账机制能够补偿丢失的消费
- 业务方已确认可以接受
7.5 方案 D:消息转储 + 离线消费
对于超大规模积压(千万级以上),可以使用 Kafka 自带工具或 Spark/Flink 将积压消息直接转储到 HDFS/S3,然后启动离线批处理任务消费:
# 使用 Kafka Connect 将积压消息导出到 HDFS
# 配置 connect-standalone.properties 和 quickstart-hdfs.properties
connect-standalone.sh connect-standalone.properties quickstart-hdfs.properties
8. 预防体系:从"救火"到"防火"

治理积压的最高境界是不让积压发生。这需要建立一套完整的预防体系。
8.1 容量规划
根据历史峰值流量和业务增长预期,提前规划 Topic 的分区数和消费者实例数:
分区数 = 峰值生产 TPS / 单分区写入 TPS 上限 × 1.5 (冗余系数)
消费者数 = 分区数
单消费者线程数 = 目标消费 TPS / 单线程处理 TPS
8.2 弹性伸缩
基于 Lag 指标的自动扩缩容:
# Kubernetes HPA 示例(基于自定义指标)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: kafka-consumer-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-consumer
minReplicas: 3
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: kafka_consumer_lag
target:
type: AverageValue
averageValue: "1000"
8.3 消息生命周期管理
为 Topic 设置合理的保留策略,避免消息无限堆积:
# 保留 7 天或 100GB
kafka-configs.sh --bootstrap-server localhost:9092 \
--entity-type topics --entity-name order-topic \
--alter --add-config retention.ms=604800000,retention.bytes=107374182400
8.4 应急预案
每季度进行一次积压演练:
- 人工注入大量消息,制造积压
- 触发告警 → 值班响应 → 按预案执行
- 记录消积压耗时,持续优化预案
9. 核心要点总结
| 维度 | 关键结论 |
|---|---|
| 发现 | 三层监控(Broker/分区/业务)+ 分级告警,5 分钟内发现 |
| 根因 | 分区倾斜是最隐蔽的积压原因,不要只看总 Lag |
| 扩容 | 消费者数 ≤ 分区数;增加分区不可逆,注意有 Key 消息的顺序性 |
| 并行度 | 线程数 = CPU × (1 + 等待/计算);批量写入可提升 10 倍+ |
| 限流 | 消积压必须限流,保护下游;动态限流优于固定限流 |
| 应急 | 跳过非核心逻辑 → 影子消费 → 重置 Offset → 离线转储,逐级升级 |
| 预防 | 弹性伸缩 + 容量规划 + 演练 = 不让积压发生 |
面试官视角:追问与回答
以下问题从面试官视角提出,考察候选人对 Kafka 消息积压的深度理解和实战经验。请先尝试自己回答,再对照参考答案。
Q1:生产环境凌晨 3 点收到 Kafka Lag 告警,Lag 已经 200 万,你作为值班人员怎么处理?
参考答案:
第一步,止损——确认是否影响线上业务。如果影响,立刻启动应急模式(跳过非核心逻辑),同时通知业务方。
第二步,定位根因——先看是生产端突增还是消费端变慢:
kafka-consumer-groups.sh --describe看 Lag 分布,判断是全局积压还是分区倾斜- 检查消费者日志,看是否有下游超时、GC 停顿、Rebalance 日志
- 检查上游生产 TPS 监控,看是否有流量突增
第三步,执行方案:
- 如果是分区倾斜 → 紧急增加消费者实例,让空闲分区被消费
- 如果是消费变慢 → 扩容消费者 + 提高并行度
- 如果下游被打挂 → 先限流保护下游,同时扩容下游资源
第四步,事后复盘——为什么凌晨 3 点才告警?阈值设置是否合理?为什么没有提前发现?
Q2:一个 Topic 有 32 个分区,消费者组有 32 个实例,但 Lag 依然很高。你怀疑是消费逻辑太慢,但不允许加实例。怎么优化?
参考答案:
实例数已经等于分区数,无法再横向扩容。需要从单消费者内部并行度入手:
- 批量处理:将
max.poll.records从默认 500 调大到 1000-2000,并在消费逻辑中改为批量写入 DB(批量 insert 比逐条 insert 快 10-20 倍) - 异步化:使用线程池并行处理消息,注意 Offset 提交时机——必须在所有任务完成后提交
- 去除非核心逻辑:检查消费链路中每条操作的必要性,能否异步化或延迟处理
- 优化慢调用:使用 Arthas 或 APM 工具追踪消费链路中的耗时瓶颈,针对性优化
- 考虑消息优先级:如果消息有优先级之分,可以只消费高优先级消息,低优先级消息延迟处理
Q3:分区倾斜导致积压,但业务不允许改变分区 Key。怎么办?
参考答案:
分区倾斜的本质是数据分布不均。如果业务不允许改变分区 Key,可以从以下角度解决:
- 生产者端自定义 Partitioner:在保持业务 Key 的基础上,加入随机后缀或时间戳,打散数据分布。例如
hash(key + "_" + random(0, 10)),这样同一个业务 Key 的消息会被分散到 10 个分区,但需要在消费端做聚合处理。 - Topic 内部重分区:在消费端增加一个中间 Topic,消费者组 A 消费原始 Topic 后,按新的分区策略重新写入中间 Topic,消费者组 B 从中间 Topic 消费。这相当于在应用层做了一次 re-partition。
- 消费者端粘性分配:如果某个分区积压特别严重,可以临时将其他消费者实例通过
assign()手动指定去消费这个分区(但脱离了消费者组管理,需要自行处理故障转移)。
Q4:Kafka 的 max.poll.records 和 max.poll.interval.ms 这两个参数如何影响积压?如果设置不当会导致什么问题?
参考答案:
max.poll.records:单次 poll() 返回的最大消息数。调大可以提升吞吐量,但调太大会导致:
- 单次 poll 处理时间过长,超过
max.poll.interval.ms - 消费者内存中堆积大量消息,可能 OOM
max.poll.interval.ms:两次 poll() 之间的最大间隔。如果处理 max.poll.records 条消息的时间超过这个值,消费者会被踢出消费者组,触发 Rebalance。
典型故障场景:某团队将 max.poll.records 设为 5000,max.poll.interval.ms 保持默认 300000(5 分钟)。但单条消息处理需要 100ms,5000 条需要 500 秒,远超 5 分钟。结果消费者每隔 5 分钟就被踢出一次,不断 Rebalance,消费彻底停滞,积压越来越严重。
正确做法:根据单条消息处理时间反推合理的 max.poll.records:
max.poll.records ≤ max.poll.interval.ms / 单条消息处理时间 × 0.8 (安全系数)
Q5:如果让你设计一个"Kafka 积压自动治理平台",你会怎么设计?核心功能有哪些?
参考答案:
核心功能模块:
- 监控采集层:多维度采集 Lag 数据(Broker JMX、Consumer 埋点、业务时间戳),统一存储到 Prometheus + ClickHouse / Elasticsearch。
- 智能告警层:基于历史数据的动态阈值(而非固定阈值),避免大促期间误报。支持按消费者组、Topic、分区维度的告警聚合和降噪。
- 根因分析引擎:自动分析 Lag 产生的根因——比对生产 TPS 和消费 TPS 曲线,检测 Rebalance 事件,检查分区倾斜度,关联下游依赖的 RT 变化。
- 自动扩容引擎:在 K8s 环境下,基于 Lag 指标自动 HPA 扩容消费者 Pod。扩容策略需要考虑分区数上限,以及"扩容生效"的延迟(新 Pod 启动 + Rebalance)。
- 应急处理引擎:一键触发应急模式——跳过非核心逻辑、动态限流、临时消费者组、Offset 重置(需审批)。
- 演练与复盘:定期积压演练,自动生成复盘报告,持续优化治理策略。
延伸阅读:Kafka 官方文档 Consumer Group 章节、《Kafka: The Definitive Guide》第 8 章、Uber 的 uReplicator 项目文档。