Flink TaskManager卡死了但没报错——lsof / strace / /proc三板斧怎么用?
本文目录
先说结论
'卡死但没报错' 的本质是进程处于 D 状态或者在某个阻塞系统调用里。D 状态(uninterruptible sleep)大多是等磁盘 IO 或者 NFS;阻塞系统调用常见是 read/recvfrom 等网络、poll/epoll_wait 等事件、flock 等锁。
三板斧的分工:① `/proc/
别上来就 jstack 卡死的 Java 进程。卡死的 JVM 很多时候 jstack 也会挂住,因为它要和目标进程做 SA(Serviceability Agent)通信,目标进程本身卡住不响应。先用系统级工具(strace/lsof)定位,再回到 JVM 工具。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
这一题比前面几题更上一个档次。CPU 高、内存高都是'有现象'的故障,真正难的是 '没现象'——进程看着活着,日志不往外吐,但业务就是停了。Flink TaskManager 卡死、Spark driver hang 住、HDFS namenode 假死,这种'僵尸现场'考的就是 lsof/strace//proc 这三板斧。能熟练用这几个工具定位到 '它卡在哪个系统调用、哪个 fd、哪台机器' 的候选人,面试官会直接在心里打上 '资深' 标签。
业务场景:Flink TM 'RUNNING' 但不消费数据
我在某元宇宙社交应用做实时数仓,有一次运营反馈用户行为看板的数据停在 10 分钟前。登录 Flink UI 看作业状态:RUNNING,没有异常,没有 Checkpoint 失败告警,Web UI 看 subtask 的 records in/out 停在某个数字不动了。这就是典型的 '僵尸 RUNNING' ——框架层面看进程还活着,但实际上不干活了。
# 第一步:确认 TM 进程存在
ps -ef | grep TaskManagerRunner
# flink 23451 ... java ...TaskManagerRunner
# 第二步:看进程状态
cat /proc/23451/status | head
# Name: java
# State: S (sleeping) <-- S 状态,表面正常
# 但 TM 有几十个线程,找到工作线程再看
ls /proc/23451/task/ | wc -l
# 128
# 逐个线程看状态(找 D 状态)
for tid in $(ls /proc/23451/task/); do
state=$(awk '/^State/ {print $2}' /proc/23451/task/$tid/status 2>/dev/null)
echo "$tid $state"
done | sort -k2 | uniq -c -f1 | sort -rn
# 120 S <-- 大多数睡眠
# 5 D <-- 5 个在 D 状态(不可中断睡眠)
# 3 R <-- 3 个 running
关键信号:5 个线程在 D 状态。D 状态进程无法被 kill -9,它在等内核层面的事件(多数是 IO)。接下来看它们在等什么:
# 看 D 状态线程的内核栈
for tid in $(ls /proc/23451/task/); do
state=$(awk '/^State/ {print $2}' /proc/23451/task/$tid/status 2>/dev/null)
if [ "$state" = "D" ]; then
echo "=== tid $tid ==="
cat /proc/23451/task/$tid/stack 2>/dev/null
echo
fi
done
# 输出示例:
# === tid 23587 ===
# [<ffff...>] rpc_wait_bit_killable+0x23/0xa0
# [<ffff...>] nfs4_call_sync+0x18/0x20
# [<ffff...>] nfs_file_write+0xdb/0x100
# [<ffff...>] vfs_write+0xa0/0x1a0
# 立刻就清楚了:卡在 NFS 写,RPC 等响应
看到 rpc_wait / nfs 就明白了:TM 写 checkpoint 到 NFS 挂载的目录,NFS server 那边网络抖动,RPC 一直在等,线程卡成 D 状态。Flink 框架层看 TM 还 RUNNING,因为 JVM 没死、没抛异常,但所有工作线程都卡在写 checkpoint 上不能处理数据。临时处理:强制 kill TM(-9 对 D 状态没用,等 RPC 超时自己醒),或者强制 umount NFS。根本解决:换 checkpoint 存储到本地或者 S3,别用 NFS。
手段一:/proc// 三件套
/proc 是 Linux 最强大的调试接口,没有之一。进程的一切状态都可以从 /proc/
# 1. status - 进程基本信息
cat /proc/23451/status
# Name: java
# State: S (sleeping)
# Tgid: 23451
# Threads: 128
# VmRSS: 8256012 kB
# voluntary_ctxt_switches: 12345
# nonvoluntary_ctxt_switches: 89
# 2. stack - 内核栈(最强大的卡死定位工具)
cat /proc/23451/stack
# 需要 CONFIG_STACKTRACE,一般发行版都开了
# 如果是 ? 0xffffffffffffffff,说明进程在用户态
# 如果是一串 kernel 函数,说明进程在等某个内核事件
# 3. wchan - 简化版,只显示一个函数名
cat /proc/23451/wchan
# 输出:futex_wait_queue_me
# 一眼知道在等 futex(锁)
# 4. fd - 打开的文件描述符
ls -l /proc/23451/fd | head
# lrwx... 0 -> /dev/null
# lrwx... 1 -> /data/logs/tm.log
# lrwx... 87 -> socket:[12345678]
# lrwx... 88 -> /nfs/checkpoint/chk-123 (deleted)
# 5. limits - 资源限制(NOFILE 常成瓶颈)
cat /proc/23451/limits | grep -E 'open files|processes|memory'
# 6. task/ - 所有线程
ls /proc/23451/task/ # 每个子目录是一个线程
# 7. comm / cmdline - 进程名和完整命令行
cat /proc/23451/cmdline | tr '\0' ' '
最强大的组合:遍历 `/proc/
手段二:strace 看系统调用
strace 是动态追踪工具,`-p
# 附加到一个进程看系统调用
strace -p 23451
# 如果卡住,会停在类似:
# recvfrom(87,
# epoll_wait(23,
# flock(10, LOCK_EX|LOCK_NB
# 这个就是它正在等的系统调用
# 追踪某个线程(而不是整个进程)
strace -p 23587 # 就是 tid
# 统计最近 10 秒所有系统调用的耗时(定位慢 syscall)
strace -c -p 23451 &
sleep 10
kill %1
# % time seconds usecs/call calls syscall
# 45.2 0.45 4500 100 read
# 32.1 0.32 3200 100 write
# 只跟踪某类系统调用
strace -e trace=network -p 23451 # 只看网络
strace -e trace=file -p 23451 # 只看文件
strace -e trace=read,write -p 23451 # 只看读写
# 持久记录到文件(常用)
strace -ttt -T -f -o /tmp/strace.log -p 23451
# -t 时间戳 -T 每个 syscall 耗时 -f 跟踪子进程 -o 输出到文件
关键用法:线上卡住的进程,`strace -p
坑:strace 会让目标进程变慢(ptrace 开销),生产上别乱用。关键进程只短时间附加一下看状态就立刻 detach。JDK 进程用 strace 有时候反而看不出有用信息(因为 JVM 有自己的 IO 模型),要结合 jstack 一起看。
手段三:lsof 看打开的资源
lsof 全称 'list open files',但 '文件' 在 Linux 里是广义的,包括普通文件、目录、socket、pipe、设备。lsof 能告诉你一个进程打开了多少 fd、对应什么资源。
# 看某进程的所有打开 fd
lsof -p 23451 | head -30
# 按类型筛选
lsof -p 23451 | awk '{print $5}' | sort | uniq -c
# 45 REG 普通文件
# 32 sock socket
# 12 DIR 目录
# 8 CHR 字符设备
# 看它打开的网络连接
lsof -p 23451 -i
# COMMAND PID USER FD TYPE NAME
# java 23451 flink 87u TCP 10.0.1.5:45678->10.0.1.10:9092 (ESTABLISHED)
# java 23451 flink 88u TCP 10.0.1.5:45679->nfs-server:2049 (ESTABLISHED)
# 看某个端口被谁占用
lsof -i :9092
lsof -i TCP:9092
# 看某个文件被谁打开
lsof /var/log/app.log
# 看 deleted 但未释放的文件
lsof -p 23451 | grep deleted
# 看连接状态(找 CLOSE_WAIT、TIME_WAIT 堆积)
lsof -p 23451 -i | awk '{print $NF}' | sort | uniq -c
# 结合 strace 的信息:strace 看到卡在 read(87, ...)
# 然后 lsof -p 23451 | grep '87u'
# 找到 fd 87 对应什么,就知道它在等哪个资源
面试爱考:strace 里看到 `read(87, ...)` 卡住,怎么知道 fd 87 是什么?答案:`ls -l /proc/
三板斧对比与组合
| 工具 | 看什么 | 优势 | 局限 |
|---|---|---|---|
| /proc/pid/stack | 内核栈 | 最精准、无开销 | 需要 root |
| strace -p | 用户态系统调用 | 动态、细粒度 | 有性能开销 |
| lsof -p | 打开的 fd 和资源 | 看到资源映射 | 快照,不动态 |
| jstack / jcmd | JVM 线程栈 | 看到 Java 代码 | 卡死的 JVM 可能挂住 |
定位流程:① `/proc/pid/task/*/stack` 找 D 线程和内核栈;② 如果在用户态 `strace -p tid` 看卡在哪个 syscall;③ `lsof -p pid | grep
面试高频追问
追问一:D 状态进程 kill -9 都杀不掉,怎么办?
D 状态(uninterruptible sleep)是 Linux 设计上的一种'不可中断'状态,通常是为了保证磁盘 IO 原子性(硬件层面 IO 正在进行,中断会破坏数据)。kill -9 发信号,但内核不会响应 D 状态。处理办法:① 最好的是等它自己醒,绝大多数 D 是 IO 阻塞,几秒到几分钟内 IO 完成就好了;② 如果是 NFS/CIFS 卡死,看能不能强制 umount(`umount -f`)或者重启 NFS 服务让 RPC 返回错误;③ 如果是硬件 IO 故障(磁盘坏道),只能重启机器。我在某元宇宙社交应用遇到过 NFS server 挂了导致一批进程卡 D,最终是重启 NFS 服务让 RPC 超时返回 EIO 才解决。
追问二:strace 附加到进程会不会影响线上业务?
会,而且影响不小。strace 用 ptrace 机制,每次系统调用都会拉目标进程的上下文到 tracer,开销至少 10%,高频系统调用场景可能 50%+。所以生产上:① 只短时间附加(10 秒内)看状态;② 大流量业务进程慎用 strace,换 perf trace 或者 bpftrace(eBPF 基本无开销);③ 用 -e 限定要跟的 syscall 类型,减少开销;④ 如果只是看'它在哪个 syscall 卡住',`/proc/
追问三:JVM 卡死,jstack 也卡住出不来,怎么办?
三个办法。① 先用 `jcmd
实战踩坑:Flink TM 在 NFS checkpoint 上集体卡 D
还是某元宇宙社交应用的那一次。实时数仓一共 6 个 Flink 作业都是早期搭建时把 checkpoint 目录设成了一个 NFS 共享存储(图方便)。某个中午 NFS server 所在机器磁盘 IO 慢了,所有 Flink 作业几乎同时出现 '看着 RUNNING 但不消费数据' 的现象。
# 第一台机器的 TM:
ps -ef | grep TaskManager # 进程在
# 状态检查
awk '/State/ {print $0}' /proc/23451/task/*/status | grep -c '^State:\sD'
# 8 <-- 8 个线程 D 状态
# 看一个 D 线程的栈
cat /proc/23451/task/23587/stack
# rpc_wait_bit_killable
# __rpc_wait_for_completion_task
# nfs4_wait_on_sequence
# _nfs4_proc_getattr
# vfs_getattr
# 根因清楚了:stat 一个 NFS 文件时 RPC 卡住
# 看这些线程在对哪个 fd 操作
ls -l /proc/23451/task/23587/fd | grep -v '\->'
# 结合 lsof
lsof -p 23451 | grep nfs
# java 23451 flink 45u REG 0,47 1234 /nfs/checkpoint/...
# 临时处理:强制 umount NFS
umount -f /nfs # 让所有 RPC 立刻返回 EIO
# 然后 Flink 作业会 restart,fail over 到另一个 TM
# 长期处理:checkpoint 存储换掉
教训:生产的 Flink/Spark checkpoint 绝对不要用 NFS(除非是专门做存储的 NetApp/EMC 那种企业级 NFS)。强一致、高可用、低延迟三者都不满足。我们后来换成 OSS/S3(远程)+ 本地 RocksDB(近端缓存),再没出过这种事。面试时这段经历特别适合讲——一个事故能引出 'NFS 为什么不适合 checkpoint'、'D 状态为什么 kill -9 杀不掉'、'/proc/pid/stack 怎么定位'、'Flink checkpoint 存储选型' 四五个知识点,面试官基本会被你带着走。
总结
'进程卡死但没报错' 是 Linux 排障里最难的场景。记住三板斧的顺序:/proc/pid/task/*/stack 找 D 状态线程和内核栈,strace 看用户态系统调用,lsof 把 fd 映射回资源。能把这一套讲流畅的,面试官已经在给你写 offer 了。
下一篇聊一个每个大数据人都会碰到的网络题:Kafka 客户端报 Connection refused,telnet 通 nc 通就是连不上,到底怎么回事?
你线上卡死过的最奇葩的一次是什么?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长