线上出现“服务卡死”时,最先拿到的往往只有一句描述:CPU 高、连不上、或者报了个 Too many open files。这三类症状对应三个不同的证据源——ps 给进程状态快照,ss 给连接与监听,lsof 与 /proc 给句柄与打开的文件。分不清谁负责哪层,就会在错误的工具里反复找同一个答案。
这篇按“进程 → 端口 → 句柄 → 内核上限”的顺序,把三个工具的取证边界划清楚。
1. ps aux 与 ps -ef:两种语法、两套列
$ ps aux | head -3
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 167432 13208 ? Ss Nov08 0:04 /sbin/init
deploy 5123 12.4 3.8 4120336 611200 ? Sl 04:11 3:22 java -jar app.jar
$ ps -ef | head -3
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Nov08 ? 00:00:04 /sbin/init
deploy 5123 5102 12 04:11 ? 00:03:22 java -jar app.jar
| 维度 | ps aux(BSD 风格) |
ps -ef(POSIX 风格) |
|---|---|---|
| 资源列 | 有 %CPU %MEM VSZ RSS |
没有,要自己 -o 加 |
| 父进程 | 无 | 有 PPID |
| 启动时间 | START(相对或绝对) |
STIME |
| 累计 CPU | TIME |
TIME |
| 适合 | 快速看谁吃资源 | 看进程树、看父子关系 |
| 兼容性 | Linux/macOS 都认 | 更接近标准,脚本里更稳 |
结论很简单:看资源用 aux,看关系用 -ef,写脚本一律 -eo 显式指定列。
2. ps -o:把列裁成你需要的
$ ps -eo pid,ppid,stat,etime,pcpu,rss,args --sort=-pcpu | head -8
$ ps -o pid,rss,vsz,nlwp,cmd -p 5123
$ ps -eLf | grep 5123 | head # 看线程(LWP)
| 列 | 含义 | 排障价值 |
|---|---|---|
stat |
状态位:R/S/D/T/Z + 修饰符 |
D 不可中断睡眠 = 卡在 IO |
etime |
已运行时长 | 与 START 交叉验证是否被重启过 |
pcpu |
平均 CPU 占用(不是瞬时) | ps 的 %CPU 是生命周期均值,看瞬时要用 top |
rss |
常驻内存(KB) | 含共享库,多进程会重复计 |
nlwp |
线程数 | 与 top -H 对照看线程膨胀 |
wchan |
内核等待点 | D 状态时看它到底等什么 |
STAT 的修饰符同样有用:Ss 是会话首进程、Sl 表示多线程、R+ 在前台进程组。看到 Z 就是僵尸——它已经死了,只是父进程没 wait(),kill -9 对它无效,要处理父进程。
3. 进程树:pstree 与 --forest
$ pstree -ap 5102
java,5102 -jar app.jar
├─{java},5103
└─{java},5104
$ ps -eo pid,ppid,cmd --forest | grep -B2 -A4 5123
pstree -p 带 PID、-a 带参数、-u 标出用户切换。排查“谁拉起了谁”时,进程树比 PPID 一列一列对快得多;systemd 环境下还能顺带看出 systemd → 服务 → worker 的层级。
4. top 的 %CPU 与 load average 不是一回事
$ uptime
05:12:03 up 12 days, 3:41, 2 users, load average: 0.82, 1.94, 2.31
$ nproc
4
| 指标 | 含义 | 读法 |
|---|---|---|
load average |
1/5/15 分钟内可运行 + 不可中断睡眠的进程平均数 | 与 nproc 比:4 核长期 > 4 就是排队 |
%CPU(top 单进程) |
该进程占用单核的百分比,可超 100 | 400% 在 4 核机器上等于跑满 |
us / sy |
用户态 / 内核态 | sy 高 = 系统调用或中断多 |
wa |
IO 等待 | 高说明磁盘是瓶颈,此时 CPU 反而闲 |
st |
被宿主机“偷走”的时间 | 云主机 st 高 = 邻居抢资源 |
si / hi |
软/硬中断 | 网络包量大时 si 会明显上升 |
关键点:load 包含 D 状态进程。一台 NFS 挂住或磁盘卡住的机器,CPU 可能只有 5%,但 load 冲到 50——这时盯着 CPU 找热点永远找不到。正确顺序是先看 wa 与 D 状态进程,再看 CPU。
5. /proc/<pid>/ 里有什么可用的
$ ls -l /proc/5123/ | head
dr-xr-xr-x 2 deploy deploy 0 Nov 9 05:10 fd
-r--r--r-- 1 deploy deploy 0 Nov 9 05:10 limits
lrwxrwxrwx 1 deploy deploy 0 Nov 9 05:10 cwd -> /srv/app
lrwxrwxrwx 1 deploy deploy 0 Nov 9 05:10 exe -> /usr/lib/jvm/java-17/bin/java
| 路径 | 内容 | 用法 |
|---|---|---|
fd/ |
每个打开的 fd 一个符号链接 | 数量即句柄数;指向 socket:[...] 说明是连接 |
cwd |
工作目录 | 确认服务在哪跑的,WorkingDirectory 是否生效 |
exe |
实际可执行文件 | 与 which java 不一致时说明用了别的 JDK |
root |
进程视角的 / |
容器/chroot 里看根目录差异 |
status |
State、VmRSS、Threads、CapEff |
比 ps 更细,含能力集 |
limits |
软/硬上限,含 Max open files |
判句柄上限的唯一权威来源 |
cmdline |
启动参数,NUL 分隔 | tr '\0' ' ' < cmdline 才可读 |
environ |
环境变量,NUL 分隔 | 查 JAVA_HOME、SPRING_PROFILES 是否注入成功 |
ns/ |
命名空间链接 | 判断是否在独立 netns/mnt ns |
cmdline 与 environ 是 NUL 分隔的,直接 cat 会挤成一行乱码;strings 或 tr 处理一下才能看。
6. 句柄泄漏:从 Too many open files 倒推
$ ls -l /proc/5123/fd | wc -l
$ sudo lsof -p 5123 | wc -l
$ sudo lsof -p 5123 | awk '{print $5}' | sort | uniq -c | sort -rn | head
| 现象 | 判断 | 动作 |
|---|---|---|
| fd 数持续单调上升 | 泄漏 | 按类型聚合,看是 socket 还是普通文件 |
lsof 里大量 (deleted) |
文件被删但 fd 未关 | 这是“磁盘不降”的主因 |
大量 socket:[...] 且指向同一远端 |
连接池没回收或没复用 | 查连接池上限与 keep-alive 配置 |
pipe 数量暴涨 |
子进程或线程间管道未关 | 查 popen/subprocess 是否 wait() |
fd 数不涨但报 EMFILE |
别的进程/host 级上限 | cat /proc/sys/fs/file-nr 看系统总量 |
lsof -p 会顺带列出 mmap 的库,所以它的行数通常大于 /proc/PID/fd 的条目数。报数时要说清用的是哪个口径,否则两个人数出来对不上。
7. ulimit -n 与 /proc/PID/limits 为什么对不上
$ ulimit -n
1024
$ cat /proc/5123/limits | grep -i 'open files'
Max open files 65535 65535 files
$ systemctl show -p LimitNOFILE app.service
LimitNOFILE=65535
| 不一致的原因 | 说明 |
|---|---|
ulimit 是当前 shell 的软上限 |
服务由 systemd 直接拉起,不经过你的 shell |
| systemd 有独立默认 | 按 systemd 版本核对,常见 soft 1024 / hard 524288 |
LimitNOFILE= 覆盖 |
unit 里显式设定,优先于 pam_limits |
pam_limits / limits.conf |
只对走 PAM 的登录会话生效 |
进程自己 setrlimit |
应用启动时改过,/proc 才反映最终值 |
判断“上限到底是多少”,只看 /proc/<pid>/limits;想改运行中进程可以用 prlimit:
$ sudo prlimit --pid 5123 --nofile=65535:65535
$ sudo prlimit --pid 5123 --nofile # 查看当前值
注意软上限不能超过硬上限,硬上限只有 root 能抬。LimitNOFILE 改了要 systemctl daemon-reload + 重启服务才生效——daemon-reload 只重读 unit,不会把新上限注入已在跑的进程。
8. kill 的信号怎么选
$ kill -l | head -5
$ kill -TERM 5123
$ kill -HUP 1204 # nginx: 重开日志
$ kill -USR1 5123 # 应用自定义:常见于触发 dump 或轮转
$ kill -9 5123 # 最后手段
| 信号 | 编号 | 默认行为 | 适用 |
|---|---|---|---|
TERM |
15 | 优雅退出,可被捕获 | 首选,让程序自己收尾 |
INT |
2 | 同 Ctrl-C | 前台调试 |
HUP |
1 | 默认终止,但常被重载实现覆盖 | nginx -HUP 重载配置/重开日志 |
QUIT |
3 | 优雅退出 + core | 需要栈现场时 |
USR1 / USR2 |
10/12 | 默认终止,实际多为应用自定义 | 触发日志轮转、平滑升级 |
KILL |
9 | 不可捕获,内核直接回收 | 卡死且 TERM 无效时 |
STOP / CONT |
19/18 | 暂停/继续 | 临时冻结进程保留现场 |
KILL 的代价是进程没有机会 flush 缓冲、删临时文件、释放锁;数据库类进程被 -9 之后可能需要恢复流程。所以顺序永远是 TERM → 等 → 再 KILL,等待时间按业务收尾耗时定,不是拍脑袋 3 秒。
9. 排错:六种“数据对不上”
| 现象 | 原因 | 处理 |
|---|---|---|
ps 的 %CPU 与 top 差很多 |
ps 是生命周期均值 |
用 top -b -n1 -p PID 或 pidstat 看瞬时 |
load 高但 CPU 空 |
D 状态进程堆积 |
ps -eo stat,pid,cmd | awk '$1 ~ /D/',再看 wchan |
进程在但 ss 没它的端口 |
在别的 netns | nsenter -t PID -n ss -tlnp |
lsof 看不到别人进程 |
非 root | sudo lsof |
| 僵尸进程杀不掉 | 已死,等父进程回收 | 处理父进程,或重启其父 |
df 不降但文件已删 |
fd 仍持有 | lsof +L1 找 deleted 文件 |
10. 边界:什么时候该换工具
| 需求 | 更合适的工具 | 理由 |
|---|---|---|
| 持续采样与告警 | pidstat / node_exporter |
ps/top 只有当下 |
| 系统调用级定位 | strace / bpftrace |
进程状态不给调用细节 |
| 内存分配热点 | pmap / smem / heaptrack |
RSS 是结果,不是归属 |
| 网络报文细节 | tcpdump |
ss 只给连接状态 |
| 容器内外对照 | crictl / docker top |
宿主视角看不到容器内进程树 |
一句话收口:ps 回答“这个进程在什么状态”,ss 回答“它在和谁通信”,lsof 回答“它抓着哪些资源”,/proc/<pid>/limits 回答“它能抓多少”。 四问齐了,Too many open files 就不再是玄学。
11. 自测
ps aux与ps -ef各自的强项是什么?ps里的%CPU为什么和top对不上?- load average 高而 CPU 空闲,最可能是哪种进程状态造成的?
lsof -p PID | wc -l与/proc/PID/fd的条目数为什么不等?- 服务由 systemd 拉起,
ulimit -n为什么不能代表它的真实上限? - 什么情况下才应该用
kill -9,代价是什么?