线上任务发现个别 TaskManager 节点的 CPU 利用率长期保持在 100%,你会如何利用各种诊断工具抓取火焰图(FlameGraph)或线程 Dump 来分析热点代码?
在处理 Flink 线上任务中个别 TaskManager(TM)节点 CPU 长期 100% 的问题时,需要一套从定位到抓取再到分析的标准排查方法论。
以下是完整的实战排查步骤,涵盖 Flink 原生功能、基础命令以及高阶诊断工具(如 Async-profiler、Arthas)。
第一阶段:精准定位异常进程与线程
在抓取信息之前,必须先明确是哪个进程、哪个线程在消耗 CPU。
- 定位异常 TaskManager 的 PID
- 登录到 CPU 100% 的机器或 Pod。
- 使用
jps -l或ps -ef | grep TaskManager找到 TM 的进程 ID(假设 PID 为12345)。
- 定位高 CPU 消耗的 OS 线程
- 执行
top -H -p 12345。 - 观察
%CPU列,找到占用 CPU 最高的几个线程 PID(假设为12350)。 - 将该十进制的线程 ID 转换为十六进制:
printf "%x\n" 12350,得到303e。这个十六进制值对应 Java 线程 Dump 中的nid(Native ID)。
- 执行
第二阶段:轻量级诊断 —— 抓取与分析 Thread Dump
如果机器还没有完全卡死,可以先用轻量级方式抓取线程快照。
1. 使用 Flink Web UI(最简单,但可能因 100% 响应慢)
- 进入 Flink UI -> TaskManagers -> 选中问题节点 -> Thread Dump。
- 优点: 无需登录服务器。
- 缺点: 只能看瞬时状态,无法统计累计耗时。
2. 使用 JDK 自带工具 (jstack)
- 在服务器上执行:
jstack -l 12345 > /tmp/tm_jstack.txt。 - 分析方法:
打开tm_jstack.txt,搜索刚才转换的十六进制nid(如nid=0x303e)。 - 经验法则: 连续抓取 3-5 次(间隔 5 秒),如果该线程每次都停留在同一段代码上,那大概率这里就是死循环或热点代码。
3. 使用 Arthas 的 thread 命令(极力推荐)
如果环境允许使用 Arthas,这是最高效的轻量排查方式:
- 启动 Arthas 挂载到 TM:
java -jar arthas-boot.jar 12345 - 执行命令:
thread -n 3(直接打印出当前 CPU 占用率最高的前 3 个线程的堆栈)。 - 优点: 一键直达病灶,自动关联高 CPU 线程和 Java 堆栈,无需手动算十六进制。
第三阶段:深度诊断 —— 抓取与分析火焰图(FlameGraph)
Thread Dump 只能看瞬时状态,如果热点代码在多个方法间快速跳转,必须使用火焰图来统计 CPU 周期分布。
1. Flink Web UI 原生火焰图 (Flink 1.13+)
- 前提: 必须在
flink-conf.yaml中开启配置rest.flamegraph.enabled: true(默认关闭,因为有少许开销)。 - 操作: Flink UI -> TaskManagers -> 选中节点 -> FlameGraph 页签。
- 分析: 看“平顶山”(最宽的横条),横条越宽,说明采样期间该方法处于调用栈顶部的次数越多,即吃 CPU 越多。
2. 使用 async-profiler(工业级标准,开销极低)
如果 Flink 未开启原生火焰图,或者需要分析 Native 代码、GC 线程,推荐使用 async-profiler。
- 下载与运行:bash
# 抓取 30 秒的 CPU 运行数据,生成 html 格式的火焰图 ./profiler.sh -d 30 -f /tmp/cpu_flamegraph.html 12345 - 优点: 基于 JVM TI 机制,不会受到 Safepoint 偏见(Safepoint bias)的影响,结果极其精准,且能抓到 JVM 底层(如 GC、JIT 编译)的 CPU 消耗。
3. 使用 Arthas 的 profiler 命令
Arthas 底层也集成了 async-profiler:
- 启动 Arthas 后执行:
profiler start --event cpu - 跑 30 秒后执行:
profiler stop --format html - 下载生成的 HTML 文件在浏览器中打开即可。
第四阶段:热点代码分析指南(常见 Flink 100% CPU 场景)
拿到火焰图或线程栈后,如何解读?以下是 Flink 中常见的 CPU 飙升原因,请对号入座:
1. 用户代码层面的热点 (User Code)
- 频繁的序列化/反序列化: 火焰图显示大量
Jackson、Gson、Fastjson方法。优化: 避免在算子中频繁解析大 JSON,尽量在 Source 端一次性解析并转换为 Flink 内部 RowData 或 POJO。 - 复杂的正则表达式: 火焰图显示
java.util.regex.Pattern,可能是发生了正则灾难性回溯。 - 死循环或低效算法: 堆栈始终指向你的某段 UDF(如
MapFunction/ProcessFunction内部的while循环或复杂的双重for循环)。 - 集合类滥用: 比如针对非常大的
List执行contains()操作(O(N) 复杂度),导致 CPU 跑满。应改用HashSet。
2. Flink 框架与状态层面的热点 (State & Framework)
- Kryo 序列化退化: 火焰图显示
KryoSerializer占用过宽。原因: Flink 无法推断你的数据类型,退化为低效的 Kryo 序列化。优化: 注册 PojoTypeInfo,或者自定义 TypeInformation。 - RocksDB 序列化/反序列化开销: 如果是大量频繁的状态读写,序列化会占据主导。优化: 开启微批处理(MiniBatch),或者优化业务逻辑减少不必要的状态访问。
3. JVM 层面的热点 (GC 引起)
- GC 线程吃满 CPU:
top -H发现占用 CPU 的是多个名为GC Thread的线程。线程堆栈或火焰图显示大量ParallelScavenge或G1相关的 Native 方法。 - 原因: 内存泄漏或堆内存不足,导致 JVM 陷入持续的 Full GC (GC 死亡螺旋)。虽然表现是 CPU 100%,但根本原因是内存问题。
- 后续动作: 此时不应该再看 CPU 火焰图,而是应该抓取 Heap Dump(
jmap -dump:live,format=b,file=heap.bin 12345)使用 MAT 进行内存泄漏分析。
总结排查套路:
- 先看系统监控(确定是 User CPU 高还是 Sys CPU 高)。
- 如果怀疑是 GC 导致,看一眼 GC 日志或 Flink UI 的 JVM 内存曲线。如果是 GC 造成的 CPU 100%,转战内存排查。
- 如果是纯计算导致的 CPU 高,使用 Arthas
thread -n 3看瞬时栈。 - 如果瞬时栈看不准,使用 Async-profiler 抓 30 秒生成火焰图。
- 顺藤摸瓜,找到最宽的栈帧,定位到具体的 UDF 代码行,重构业务逻辑。