订阅更新

Flink TaskManager卡死了但没报错——lsof / strace / /proc三板斧怎么用?

Linux手册 第 7 题 约 15 分钟读完

本文目录
  1. 先说结论
  2. 业务场景:Flink TM 'RUNNING' 但不消费数据
  3. 手段一:/proc/<pid/ 三件套
  4. 手段二:strace 看系统调用
  5. 手段三:lsof 看打开的资源
  6. 三板斧对比与组合
  7. 面试高频追问
  8. 实战踩坑:Flink TM 在 NFS checkpoint 上集体卡 D
  9. 总结

先说结论

'卡死但没报错' 的本质是进程处于 D 状态或者在某个阻塞系统调用里。D 状态(uninterruptible sleep)大多是等磁盘 IO 或者 NFS;阻塞系统调用常见是 read/recvfrom 等网络、poll/epoll_wait 等事件、flock 等锁。

三板斧的分工:① `/proc//stack`(或 wchan)看内核栈,告诉你进程在内核哪里等;② `strace -p` 看当前在哪个系统调用;③ `lsof -p` 看它打开了哪些 fd(对应哪些文件、sockets),三者结合才能精准定位。

别上来就 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//task/*/stack` 找 D 状态线程的内核栈,一次性看到所有卡死线程在等什么。这招我在四家公司都用过,没失手过。

手段二:strace 看系统调用

strace 是动态追踪工具,`-p ` 可以附加到运行中的进程,看它每一次系统调用的调用和返回。卡住不动的进程,strace 会停在最后那个系统调用的入口。

# 附加到一个进程看系统调用
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 ` 一附加上去,如果命令行停在 'read(xx, ' 不动,说明正在等这个 fd 读数据。下一步就是 lsof 看这个 fd 对应什么资源。

坑: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//fd/87` 或者 `lsof -p | grep '\b87'`,一步到位。

三板斧对比与组合

工具看什么优势局限
/proc/pid/stack内核栈最精准、无开销需要 root
strace -p用户态系统调用动态、细粒度有性能开销
lsof -p打开的 fd 和资源看到资源映射快照,不动态
jstack / jcmdJVM 线程栈看到 Java 代码卡死的 JVM 可能挂住

定位流程:① `/proc/pid/task/*/stack` 找 D 线程和内核栈;② 如果在用户态 `strace -p tid` 看卡在哪个 syscall;③ `lsof -p pid | grep ` 找这个 fd 对应啥;④ 最后 `jstack` 回到 Java 代码层。顺序是从底到上。

面试高频追问

追问一: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//syscall` 文件一次性告诉你,比 strace 更轻量。

追问三:JVM 卡死,jstack 也卡住出不来,怎么办?

三个办法。① 先用 `jcmd Thread.print`,jcmd 是 JDK 里兼容性更好的工具,有时候 jstack 卡住 jcmd 还能出。② `kill -3 `(发 SIGQUIT 给 JVM),JVM 收到后会把所有线程栈打到它的 stdout(一般是 catalina.out 或者启动日志),这个不依赖 SA 通信,是 JVM 内部机制,最可靠。③ 实在不行上 async-profiler,它走 JVMTI 接口,和 SA 是两个机制,SA 挂了 JVMTI 一般还能用。我在某出海硬件品牌排查过 Spark driver 卡死的问题,jstack 挂了 5 分钟没出来,最后 kill -3 搞定,堆栈直接打到日志里。

实战踩坑: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年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器