当你发现 SkyWalking 的 Kafka 消费积压了 6 亿条消息,你的第一反应是什么?

A. 扩容机器 B. 加分区 C. 提桶跑路 D. 深呼吸,打开源码

如果你选了 C,说明你很有生活智慧。但作为一个有职业素养的工程师,我选了 B,然后发现不管用,最后被迫选了 D。

这篇文章,就是从 6 亿积压到源码级定位根因的完整复盘。


一、问题现象

1. 告警发现

某天早上打开监控大盘,发现 SkyWalking 对应的 Kafka 消费组积压量直线飙升,查看近 7 天和近 16 天的趋势图,积压量已经突破了 6 亿条

在这里插入图片描述

在这里插入图片描述

此时的心情可以用四个字形容:大事不妙

2. 直观感受

  • 日志查询严重延迟,研发同学反馈"查不到最新日志"
  • 链路追踪页面数据滞后严重,几乎等于"瞎了"
  • 积压量还在持续增长,完全没有收敛的趋势

6 亿条消息是什么概念?如果把每条消息当成一粒米,大概可以铺满 3 个篮球场。当然这个比喻没什么用,但至少说明——量很大


二、排查现场

1. 第一反应:扩 Kafka 分区

既然消费不过来,最直觉的想法就是——加分区,提升并行消费能力。

docker exec -it kafka-broker bash

# 进入容器后,先 unset JVM 参数,避免干扰命令行工具
unset JAVA_TOOL_OPTIONS

# 查看当前分区情况
kafka-topics.sh \
  --bootstrap-server 10.0.0.1:9092 --describe \
  --topic skywalking-segments

# 批量扩容所有 SkyWalking 相关 topic 到 12 分区
for t in skywalking-segments skywalking-logs skywalking-metrics \
         skywalking-meters skywalking-profilings skywalking-logs-json \
         skywalking-managements; do
  echo "Expanding topic: $t"
  kafka-topics.sh \
    --bootstrap-server 10.0.0.1:9092 \
    --alter \
    --topic "$t" \
    --partitions 12
done

扩完分区,满怀期待地等了一会儿……

2. 扩分区后:积压依旧,而且更扎心了

扩分区后继续观察,积压不仅没缓解,还暴露了一个更深层的问题——分区数据严重倾斜

在这里插入图片描述

12 个分区中,Partition-2、Partition-8、Partition-11 三个分区的数据量远远大于其他分区。其他分区基本在"摸鱼",这三个分区在"996"。

加分区就像给高速公路加车道,但如果所有车都只走最左边三条道,加再多车道也是白搭。

3. 查看 ES 写入情况

SkyWalking OAP 最终会将数据写入 Elasticsearch,看一下 ES 的写入线程池状态:

GET /_cat/thread_pool/write?v
节点 active queue rejected
node-03 8 118 0
node-02 8 15 0
node-01 0 0 0

ES 写入同样极度不均衡node-03 的写入队列堆积了 118 个任务,node-01 完全空闲。同时发现 node-03 的 CPU 使用率也明显偏高。

整个 ES 集群是 3 节点、8C16G 的配置。三个节点的工作状态可以形象地概括为:一个在拼命、一个在划水、一个在睡觉。

来看一眼 ES 机器的监控大盘,画面感更直接:

在这里插入图片描述

CPU、负载、写入压力全部集中在少数节点上,和 Kafka 分区倾斜的表现完全吻合——数据从源头就歪了,下游自然跟着歪

4. 紧急止血:清理积压消息

排查当天上午,积压已超过 6 亿条。先删除了积压最严重的 skywalking-segments topic 数据,但积压仍在持续增长。

下午,决定清理全部积压消息,清理后日志查询恢复正常。

在这里插入图片描述

但这只是"止血",不是"治病"。核心问题仍然存在——为什么数据会倾斜到少数几个分区?


三、原因分析

1. 定位瓶颈:segments 才是大头

从多张监控图中可以明确看到:积压主要集中在 skywalking-segments 这个 topic 上。

这里要科普一下:skywalking-segments 存储的是链路追踪数据(Trace Segments),并不是日志(Logs)。链路数据的量级通常远大于日志,因为每一次 RPC 调用、每一个 SQL 执行、每一次 HTTP 请求都会产生 Span。

2. 深入源码:真凶浮出水面

虽然通过降低采样率临时解决了问题(详见下方方案一),但工程师的"强迫症"不允许我就这样放过它。抽空翻了一下 SkyWalking Agent 的源码,直接定位了根因。

Agent 上报数据到 Kafka 依赖的是 kafka-reporter-plugin 这个插件,项目结构如下:

代码量不多,关键的发送类一眼就能锁定。我们看其中 KafkaMeterSender 的核心发送逻辑:

@OverrideImplementor(MeterSender.class)
public class KafkaMeterSender extends MeterSender
    implements KafkaConnectionStatusListener {

    private String topic;
    private KafkaProducer<String, Bytes> producer;

    @Override
    public void send(Map<MeterId, BaseMeter> meterMap,
                     MeterService meterService) {
        if (producer == null) {
            return;
        }
        MeterDataCollection.Builder builder =
            MeterDataCollection.newBuilder();
        transform(meterMap, meterData -> {
            builder.addMeterData(meterData);
        });

        // 注意看这里:key 固定为实例名称!
        producer.send(new ProducerRecord<>(
            topic,
            Config.Agent.INSTANCE_NAME,  // <-- 关键:key = 实例名
            Bytes.wrap(builder.build().toByteArray())
        ));
        producer.flush();
    }
}

看到问题了吗? ProducerRecord 的第二个参数是 key,而这里 key 被固定设置为 Config.Agent.INSTANCE_NAME(即服务实例名称)

3. 根因:Kafka 分区策略 + 固定 Key = 数据倾斜

Kafka 的分区路由机制是这样的:

场景 分区策略 结果
key = null 轮询(Round Robin) 数据均匀分布 ✅
key != null hash(key) % partitionCount 相同 key 始终路由到同一分区

由于 key = INSTANCE_NAME,同一个服务实例的所有数据会始终被路由到同一个分区

而在实际生产环境中,各服务实例的流量差异巨大——流量大的实例疯狂往同一个分区灌数据,流量小的实例对应的分区却很清闲。最终导致:

  • 少数分区承担了绝大部分数据 → 消费速度跟不上生产速度 → 积压
  • 大部分分区基本空闲 → 消费者线程在"带薪摸鱼"

这就好比食堂打饭,12 个窗口只有 3 个窗口排满了人,其他 9 个窗口的阿姨在聊天。你加再多窗口,排队的人还是往那 3 个窗口挤。


四、解决方案

方案一:降低 Agent 采样率(紧急止血)

研发同学最关心的是日志的实时性,链路追踪数据的优先级相对较低。在不扩容机器的前提下,先从采集端"做减法":

# 每 3 秒最多采样 100 条链路(默认值 -1,表示全量采集)
agent.sample_n_per_3_secs=${SW_AGENT_SAMPLE:100}

# 单个 Span 中最大 TraceSegmentRef 数量(默认 500 → 调整为 50)
agent.trace_segment_ref_limit_per_span=${SW_TRACE_SEGMENT_LIMIT:50}

# 单个 Segment 中最大 Span 数量(默认 300 → 调整为 30)
agent.span_limit_per_segment=${SW_AGENT_SPAN_LIMIT:30}

三个参数一调,链路数据的写入量直接降低了 90% 以上。简单粗暴,但有效。

调整后观察近 6 天的消费情况:

  • 消费积压趋势平稳,无新增积压
  • 分区间的数据分布也趋于均匀(因为整体量下来了)

截止 2 月份持续观察,未再出现新的积压

方案二:修改 Agent 源码,修复分区倾斜(根治方案)

既然知道了根因是 key = INSTANCE_NAME 导致的 hash 倾斜,解决思路就很清晰了:

  • 方案 A:将 key 设为 null,让 Kafka 使用轮询策略均匀分配
  • 方案 B:使用随机 key 或自定义分区器,实现更灵活的负载均衡

具体做法:Fork skywalking-java 仓库,修改 kafka-reporter-plugin 中的 ProducerRecord 构造逻辑,将 key 置为 null 或使用随机值,然后自行打包部署验证。

改动量其实很小,核心就是把 Config.Agent.INSTANCE_NAME 换成 null,一行代码的事儿。但从发现问题到定位根因,这个过程才是最有价值的。


五、总结与反思

1. 时间线回顾

时间 动作 效果
D+0 上午 发现 6 亿积压,尝试扩 Kafka 分区 无效 ❌
D+0 下午 清理全部积压消息 临时恢复 ✅
D+5 调整 Agent 采样率,降低 90% 写入量 积压消除 ✅
D+30 分析源码定位根因:固定 key 导致分区倾斜 找到根因 ✅
后续 修改源码打包验证(进行中,后续会补充结论) 根治方案 🔧

2. 经验教训

  1. 扩分区不是万能药:如果数据本身存在倾斜,扩分区只是增加了空闲分区的数量,对缓解热点分区毫无帮助
  2. Kafka 的 key 设计至关重要:key 决定了分区路由,不恰当的 key 选择会直接导致数据倾斜。除非有明确的顺序性需求,否则优先考虑 null key(轮询)
  3. 先止血,再治病:线上问题首先保证业务可用,然后再从容地追根溯源
  4. 源码面前,了无秘密:当文档和经验都解释不了问题时,打开源码往往能在几分钟内找到答案

3. 举一反三

这个问题不仅限于 SkyWalking,所有使用 Kafka 的场景都可能遇到类似的分区倾斜问题。建议大家对自己项目中的 Kafka Producer 做一次 key 策略审查:

  • key 是否固定?是否会导致数据集中到少数分区?
  • 是否真的需要按 key 保证顺序?如果不需要,考虑使用 null key
  • 分区数是否与消费者数量匹配?

如果这篇文章对你有帮助,或者你也踩过类似的坑,欢迎点赞、收藏,方便下次遇到 Kafka 积压时快速翻出来 “抄作业”。

毕竟,能从 6 亿积压里翻出一个 one-line fix 的文章,值得你一个小小的点赞 👍

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐