深色模式
服务假死排查
摘要:假死比崩溃更难发现——进程在、端口通、CPU 和内存都正常,但业务请求全部超时。本文从"探活是否通过"入手,用线程栈、连接状态和下游耗时三个方向定位线程池打满、死锁与夯住。
适用环境
bash
curl -V | head -1
which jstack jcmd py-spy gdb strace
ss -V1
2
3
2
3
排障步骤
第 1 步:区分"进程死了"与"假死"
bash
systemctl status app --no-pager
ss -lntp | grep <端口>
curl -sS -o /dev/null -w 'code=%{http_code} total=%{time_total}\n' http://127.0.0.1:8080/health
curl -sS -o /dev/null -m 5 -w 'code=%{http_code} total=%{time_total}\n' http://127.0.0.1:8080/api/order1
2
3
4
2
3
4
健康接口秒回、业务接口超时,即典型假死特征。
第 2 步:看连接堆积在哪一侧
bash
ss -antp | grep <端口> | awk '{print $1,$2}' | sort | uniq -c
ss -ant state established '( sport = :8080 )' | wc -l1
2
2
大量 ESTABLISHED 且不返回,说明请求进了应用但没出来。
第 3 步:抓线程栈,找阻塞点
bash
jstack -l <PID> > /tmp/j1.txt
sleep 5; jstack -l <PID> > /tmp/j2.txt
sleep 5; jstack -l <PID> > /tmp/j3.txt
grep -A 15 'BLOCKED' /tmp/j1.txt | head -601
2
3
4
2
3
4
关注 BLOCKED(锁竞争)、大量线程 WAITING 在同一把锁、或全部卡在同一个下游调用栈上。
一次栈不够,必须连抓三次
单份栈只能看到瞬时状态,无法区分"正在处理"和"卡住不动"。三次栈中位置不变的线程才是真凶。
第 4 步:判断线程池是否打满
bash
grep -c 'http-nio' /tmp/j1.txt
grep -A 5 'pool-.*Threads' /tmp/j1.txt | head -401
2
2
所有工作线程都卡在同一类调用(如同一个 HTTP/DB 调用),说明慢依赖把线程池占满,进而导致无关接口也不可用。
第 5 步:确认是否夯在下游
bash
ss -antp state established | grep <下游IP>
tcpdump -i any -nn -c 20 host <下游IP>
curl -sS -o /dev/null -m 3 -w '%{http_code} %{time_total}\n' http://<下游IP>:<端口>/health1
2
3
2
3
第 6 步:非 JVM 语言的替代手段
bash
py-spy dump --pid <PID> # Python
gdb -p <PID> -batch -ex 'thread apply all bt' # 原生/C++1
2
2
gdb 附加会暂停进程
gdb -p 会挂起目标进程直到 detach,生产环境只能短时操作,且严禁在 gdb 内执行 kill、写内存等命令。
验证
bash
curl -sS -m 5 -o /dev/null -w 'code=%{http_code} total=%{time_total}\n' http://127.0.0.1:8080/api/order
jstack -l <PID> | grep -c BLOCKED
ss -ant state established '( sport = :8080 )' | wc -l1
2
3
2
3
常见坑
只看 CPU 发现不了假死
假死时 CPU 往往很低(线程都在等),资源类监控完全正常,必须有业务级探活与耗时指标。
健康检查探的是"活没活"不是"能不能干活"
只返回固定字符串的 /health 无法发现假死。健康检查应包含一次真实的轻量依赖调用。
容器内 PID 1 无法抓栈
容器里进程常是 PID 1,缺少 attach 权限。需开启 SYS_PTRACE 或从宿主机侧执行。
直接重启掩盖证据
重启能立刻恢复,但栈、连接、堆都消失了,同类故障会反复发生。应先抓栈再重启。