背景是一条最常见的链路:
Kafka Source → 宽表 Join → Kafka Sink,RocksDB StateBackend。
整个排查过程,严格对应 Flink 的三层职责:JobManager 定界,TaskManager 给资源信号,火焰图做线程级归因。下面按这个顺序走完一次完整排障。
Flink 的火焰图,不是又一个花哨图表,而是 Runtime 留给你的唯一窗口,让你能问线程一句:“你这段时间到底在干嘛?”JobManager 说“order_join 红了”,Metrics 说“buffer 没了”,火焰图说“它在 RocksDB 里 park 着”。三句话连起来,才是一个完整的排障。以后看到反压,不要先加并行度,先点开火焰图,看一眼栈,答案往往就在那几个宽框里。
一、反压排查过程
1. JobManager 只说一句话:哪一段红了
早上看到告警:作业 Checkpoint Duration 超过 5 分钟。打开 Flink Web UI,Job Graph 颜色很直观:Source 绿,Map(parse) 黄,KeyedProcess(order_join) 红,Sink 绿。
JobManager 到这里已经完成了它的全部职责——它聚合反压信号,告诉你 order_join 写不出去,上游被它拖慢。
但它说不出“为什么写不出去”:是 Join 算太快,还是 Join 自己卡住?调度层只关心拓扑和状态,不关心栈,所以结论只能停在“order_join 反压 100%”。
2. 下到 TaskManager:看 SubTask 和 Buffer
点进 order_join 的 SubTasks 页,
- 第一个发现是倾斜:SubTask 3、7、11 反压 100%,其余只有 20%。说明不是 Kafka Sink 全局慢,而是 key 集中到了少数 Task。
- 再看 Metrics:outPoolUsage 100%(发送端 Buffer 池使用率),inPoolUsage 60%(接收端 Buffer 池使用率),SubTask 3 的吞吐是其他实例的 8 倍。
- outPoolUsage 高 → RecordWriter 侧堵
- inPoolUsage 高 → 本算子消费慢,
上游RecordWriter 堵 你那个 100% / 60% 组合,就是典型的:下游 Sink 不收,RecordWriter 等
Buffer,线程 park,上游被反压。
Runtime 层把问题收敛成更精确的一句话:“某个 SubTask 的 Network Buffer 被吃光,数据卡在 RecordWriter 侧。”
但 Metrics 仍然解释不了根因——buffer 用完,可能是产太快,也可能是单条 record 占着 buffer 太久不释放。
3. 火焰图切换视角:从指标变成栈
开 SubTask 3 所在 TM 的 Flame Graph,先选 Mixed。
最宽栈(用的做多) 是 Thread.park → LockSupport.park → MailboxProcessor.run,往下是 RecordWriterOutput.push → RecordWriter.emit → NetworkBufferPool.requestMemorySegment。
翻译成架构语义:Join 想发数据,申请不到 buffer,线程被 park。注意,这不是 Join 在疯狂生产,而是它处理得慢,导致已占用的 buffer 迟迟不释放。
- 切 On-CPU,看 RUNNABLE。processElement 很窄,说明 UDF 本身不烧 CPU,没有重计算。宽栈落在 RocksDBValueState.value → RocksDB.get → getJNI,线程醒着时主要在等 RocksDB 返回。
- 再切 Off-CPU,没有锁竞争,但能看到 native 层的 fdatasync、Compaction 相关栈,对应 RocksDB 读放大。因果链就此闭合:热点 key 集中 → RocksDB Get 慢 → 单条处理时间变长 → buffer 回收慢 → outPoolUsage 打满 → 上游被反压 → Checkpoint barrier 卡住。
4. 三层职责复盘
- JobManager 层负责“现象定界”:Operator 红、反压百分比、拓扑颜色。
- TaskManager Metrics 层负责“资源信号”:buffer、吞吐、SubTask 倾斜。
- 火焰图层负责“线程归因”:On-CPU 看算什么,Off-CPU 看等什么,Mixed 看总账。
JobManager 不碰栈,Metrics 不碰代码,火焰图不管调度,三层分工非常干净。
5. 可复用的排查模板
以后再遇到反压,可以照这个顺序点:
- UI 看哪个 Operator 红,SubTask 页看是否倾斜,火焰图 Mixed 找最宽栈。
- 如果最宽在 park + RecordWriter,说明写 buffer 卡住;
- 切 On-CPU(线程真正在跑代码),宽在 processElement 就是 UDF 重,宽在 RocksDB 就是状态问题;
- 切 Off-CPU( 线程在等,没在跑),宽在 KafkaProducer 就是 Sink 慢,宽在 park 且无业务栈,就是反压传导。
JobManager 说“哪一段”,Metrics 说“buffer 满不满”,火焰图说“线程当时在干什么”。
| 视角 | 看到的现象 |
|---|---|
| UI | order_join 反压 100% |
| Metrics | outPoolUsage=100%,inPoolUsage 没满 |
| Mixed 火焰图 | park + RecordWriter |
| On-CPU | RocksDB.get 宽 |
| Off-CPU | compaction / fsync |
| 架构语义 | 慢 record 拖慢 Buffer 周转,不是快 record 冲爆 Buffer |
慢 record 拖慢 Buffer 周转,不是快 record 冲爆 Buffer。这句话同时覆盖了:
- record 慢 → StateBackend(RocksDB)
- Buffer → Network Stack
- 周转 → Task 线程 + Mailbox 调度
- 否定“冲爆” → 纠正直觉(不是吞吐太高)
- 它是把三层现象压缩成一句“系统级理解”。
二、火焰图栈 → 架构结论速查表
实战中,你不需要每次都理解全部栈,只要认几个高频宽框,就能直接下结论。下面这张表,可以直接贴在内部排障文档里:
| 火焰图里最宽的位置 | 架构语义 | 典型根因 | 下一步动作 |
|---|---|---|---|
MailboxProcessor.run + LockSupport.park + RecordWriter | 线程等 Network Buffer | 下游写不动 / 自己占 buffer 太久 | 看是不是 buffer 满,再下钻 |
processElement 很宽(On-CPU) | 用户代码烧 CPU | JSON 解析、正则、加解密、大对象 | 优化 UDF、拆逻辑、避免装箱 |
RocksDB.get / put / RocksDB.getJNI | 状态访问慢 | key 热点、读放大、Compaction | 调 key、改 MapState、调 RocksDB |
KafkaProducer.send / NetworkClient.poll | Sink 等外部系统 | Kafka 吞吐、ack、分区不均 | 调 Sink 参数、下游集群 |
CompletableFuture.get / thenAccept | Async Sink 等回调 | 外部 IO 慢、并发不够 | 调 async 超时、max in-flight |
ObjectInputStream / readObject | 序列化重 | 大状态、POJO 复杂、Kryo | 改 POJO、注册 Kryo、用 Avro |
java.lang.Thread.yield / sched_yield | 线程让出 CPU | CPU 争抢或自旋 | 看容器 CPU quota |
GC / allocate / new byte | 对象爆炸 | 状态大、collect 频繁 | 调堆、减对象、改数据结构 |
CheckpointLock / triggerCheckpoint | CP 拖慢 | 大状态、增量 CP、对齐慢 | 调 CP 间隔、对齐超时、增量 |
这张表本质上是对前面三层逻辑的工程化压缩:看到栈,先判断是算、等、写、还是 GC,再反推是代码、状态还是外部系统。
1831

被折叠的 条评论
为什么被折叠?



