凌晨 3 点,容器里连 htop 都没有
凌晨 3:17,PagerDuty 响了。生产环境某个服务的 CPU 飙升到 95%,响应延迟从 80ms 跳到 3000ms。你 SSH 进容器——发现这是最小化镜像,没有 htop、没有 iotop、没有 netstat(现在连 ifconfig 都没了)。怎么办?
答案在 /proc。这个虚拟文件系统是 Linux 内核暴露的运行状态窗口,不需要额外安装任何工具——因为它是内核自己维护的。本文教你用 /proc 做一次完整的生产环境诊断,从 CPU 到内存到 IO 到网络,只靠 cat、ls 和你自己的眼睛。
/proc 是什么:一行说清楚
/proc 不是真实磁盘上的文件。你 cat /proc/cpuinfo 的时候,内核当场生成内容返回给你。所以读取开销极低,而且永远是最新数据——没有缓存过期这回事。

内核版本不同,/proc 文件内容可能略有差异。本文基于 Linux 5.15+(Ubuntu 22.04/24.04 默认内核),但核心文件自 2.6 时代就没怎么变过。
CPU 诊断:比 top 更直接
/proc/cpuinfo — 快速看一眼 CPU 架构
别急着用 lscpu。/proc/cpuinfo 给你每个逻辑核的完整信息:
$ grep -E "processor|model name|cpu MHz|cache size" /proc/cpuinfo | head -12
processor : 0
model name : Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
cpu MHz : 3499.998
cache size : 55296 KB
processor : 1
model name : Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
cpu MHz : 3499.998
cache size : 55296 KB
关键行:processor 是逻辑核编号(含超线程)、siblings 是同一物理核上的逻辑核数(判断超线程)、bogomips 在容器里通常不可靠,忽略就行。
/proc/loadavg — 负载一眼定生死
$ cat /proc/loadavg
2.85 2.10 1.67 3/1524 42891
前三个是 1/5/15 分钟负载均值。第四个字段 3/1524 表示「当前运行中的进程数 / 总进程数」。第五个是最近创建的 PID。
实战判断:如果 1 分钟负载 > CPU 核数 × 1.5,而且第四个字段里「运行中」的数字接近 CPU 核数——CPU 瓶颈无疑。如果是磁盘 IO 导致的负载(D 状态进程),CPU 可能很闲但负载很高。区分方法见下面 /proc/pid/status。
/proc/stat — CPU 时间片的原始账本
$ head -1 /proc/stat
cpu 11824571 2934 8392104 148293847 111849 0 34458 0 0 0
从左到右:user、nice、system、idle、iowait、irq、softirq、steal(虚拟化偷走的时间)、guest、guest_nice。
实战脚本:采样两次差值计算瞬时 CPU 使用率,只有 4 行:
$ read cpu user nice system idle iowait irq softirq steal < <(head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}')
$ sleep 1
$ read cpu2 user2 nice2 system2 idle2 iowait2 irq2 softirq2 steal2 < <(head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}')
$ total=$((user2 - user + nice2 - nice + system2 - system + idle2 - idle + iowait2 - iowait + irq2 - irq + softirq2 - softirq + steal2 - steal))
$ echo "CPU: $((100 * (total - (idle2-idle) - (iowait2-iowait)) / total))% iowait: $((100 * (iowait2-iowait) / total))%"
如果 iowait% > 15%,问题不是 CPU 慢,是磁盘慢——CPU 在等 IO。这时候该查的不是代码,是 /proc/diskstats。
内存诊断:/proc/meminfo 全景图
$ cat /proc/meminfo | head -20
MemTotal: 32781128 kB
MemFree: 512436 kB
MemAvailable: 18234912 kB
Buffers: 234128 kB
Cached: 17302864 kB
SwapCached: 0 kB
Active: 10492136 kB
Inactive: 14823648 kB
别再盯着 MemFree 看了! Linux 会把空闲内存全拿去做 page cache(Buffers + Cached),所以 MemFree 低很正常。真正有用的指标是 MemAvailable——内核估算的「可立即分配给新进程的内存量」。如果 MemAvailable < 总内存的 10%,才需要紧张。
内存泄漏特征:Active(anon) 持续增长不回落,Cached 没变化——说明是进程直接分配的内存(非文件缓存)在泄漏。检查 /proc/pid/smaps 定位具体进程。
进程诊断:/proc/pid/* 全家桶
/proc/pid/status — 进程身份证
$ cat /proc/$(pgrep -f "your-service")/status | grep -E "Name|State|VmRSS|Threads"
Name: your-service
State: S (sleeping)
VmRSS: 524288 kB
Threads: 24
State 字段定生死:
| 状态 | 含义 | 生产信号 |
|---|---|---|
| S | 可中断睡眠 | 正常等待(网络 IO、锁) |
| R | 运行中 | CPU 密集型任务 |
| D | 不可中断睡眠 | ⚠️ 等待磁盘 IO,无法被 kill |
| Z | 僵尸 | ⚠️ 子进程已死但父进程未 wait |
| T | 停止 | SIGSTOP 暂停中 |
核心经验:如果大量进程在 D 状态,磁盘 IO 是瓶颈。如果 D 状态进程长时间不退——可能是 NFS 挂载点无响应,或磁盘控制器故障。
/proc/pid/fd/ — 文件描述符泄漏诊断
$ ls -l /proc/$(pgrep -f "your-service")/fd | wc -l
1247
1247 个文件描述符?这不对劲。看看是什么:
$ ls -l /proc/$(pgrep -f "your-service")/fd | tail -20
lrwx------ 1 app app 64 Aug 7 02:17 1228 -> 'socket:[9482713]'
lrwx------ 1 app app 64 Aug 7 02:17 1229 -> 'socket:[9482714]'
lrwx------ 1 app app 64 Aug 7 02:17 1230 -> 'socket:[9482715]'
大量未关闭的 socket——HTTP 客户端没复用连接池。定位代码里 requests.get() 是否忘记用 Session、数据库连接是否没放在 with 语句里。
快速统计 FD 类型分布:
$ ls -l /proc/PID/fd | awk '{print $NF}' | sed 's/.*\[//;s/\]//' | sort | uniq -c | sort -rn | head -5
892 socket
184 pipe
102 eventfd
45 anon_inode
12 regular_file
892 个 socket 没释放——证据确凿。
/proc/pid/oom_score & oom_score_adj — OOM Killer 的生死簿
$ cat /proc/$(pgrep -f "your-service")/oom_score
57
$ cat /proc/$(pgrep -f "your-service")/oom_score_adj
0
oom_score 越高(最大 1000),越优先被杀。oom_score_adj 是手动调整值:-1000 完全豁免(给数据库用),1000 优先牺牲。
实战操作:保护关键进程不被 OOM Kill:
$ echo -500 > /proc/$(pgrep -f "postgres")/oom_score_adj
IO 诊断:/proc/diskstats
$ cat /proc/diskstats | grep -E "nvme|sda|vda"
8 0 sda 2453824 129384 198273442 2384729 3847291 2957392 482940182 12938472 0 2847391 15323201 0 0 0 0
字段 4/8 是读写操作次数,字段 7/11 是读写耗费的毫秒数。
实战脚本——1 秒 IO 利用率:
$ awk '/sda /{r=$7;w=$11;print r,w}' /proc/diskstats; sleep 1; \
awk '/sda /{r2=$7;w2=$11;printf "IO util: %d%%\n",((r2-r)+(w2-w))/10}' /proc/diskstats
持续 > 80% 说明磁盘是瓶颈。
网络诊断:/proc/net/*
/proc/net/tcp — TCP 连接状态一览
$ cat /proc/net/tcp | awk '{print $4}' | sort | uniq -c | sort -rn
892 0A # LISTEN 状态后的 ESTABLISHED——正常
45 06 # TIME_WAIT——偏高但尚可
12 07 # CLOSE——正在关闭
3 08 # CLOSE_WAIT ⚠️ 关键!
状态码速查:01=ESTABLISHED、06=TIME_WAIT、07=CLOSE、08=CLOSE_WAIT。
CLOSE_WAIT 是红色警报:对方已经关闭连接,本端还没调 close()。这说明代码里没关闭 socket,最终会耗尽文件描述符。和上面 FD 泄漏的诊断完全对应上了。
/proc/net/dev — 网卡流量实时统计
$ cat /proc/net/dev | grep eth0
eth0: 48273918472 2384729 0 0 0 0 0 0 2394827394 29384728 0 0 0 0 0 0
第二个数字是接收字节数,第十个是发送字节数。采样差值就能算出实时带宽。
实战:三分钟诊断决策树
下次凌晨被叫醒,按这个顺序走,不用装任何工具:
cat /proc/loadavg→ 确认负载异常head -1 /proc/stat(两次采样)→ 区分 CPU 忙还是 IO 等待cat /proc/meminfo | grep Available→ 排除内存不足for p in /proc/[0-9]*/status; do grep -H State "$p"; done | grep -c " D "→ D 状态进程数cat /proc/net/tcp | ...→ TIME_WAIT/CLOSE_WAIT 计数- 定位嫌疑进程 →
ls /proc/PID/fd | wc -l→ 确认 FD 泄漏
常见问题
Q: /proc/meminfo 里 MemFree 只有 500MB 但 MemAvailable 有 18GB,内存到底够不够?
够。MemFree 低是 Linux 的正常行为——空闲内存会被用作 page cache。MemAvailable 才是内核估算的「可立即分配内存」,相信它而不是 MemFree。如果 MemAvailable 也在掉,才需要紧张。
Q: 进程状态 D(不可中断睡眠)过多久算有问题?
几秒是正常的(磁盘寻道)。持续超过 30 秒通常是存储问题——NFS 挂载超时、SAN 链路故障、磁盘控制器卡死。检查 dmesg 看内核日志里有没有 I/O error。
Q: 容器里 /proc/diskstats 显示的数据是宿主机全局的还是容器隔离的?
全局的。/proc/diskstats 在容器里显示的是宿主机所有磁盘的 IO 数据,没有被 namespaced 隔离。如果你看到其他容器的 IO 也在这一块盘上,它们是混在一起的。这也是为什么容器里用 iostat 看到的数据不一定是你自己的。
总结
/proc 是 Linux 最被低估的调试工具——因为它太朴素了,没有花哨的 TUI,没有彩色柱状图。但当你 SSH 进一个连 htop 都没有的最小化容器时,它是你唯一的武器。
核心记住这几条:
- 负载异常 → /proc/loadavg → 区分 CPU vs IO 瓶颈 → /proc/stat 采样
- 内存紧张 → /proc/meminfo 看 MemAvailable,别看 MemFree
- FD 泄漏 → ls /proc/PID/fd | wc -l,配合 /proc/net/tcp 查 CLOSE_WAIT
- OOM 风险 → /proc/PID/oom_score_adj 保护关键进程
- D 状态进程 → 磁盘 IO 瓶颈或存储故障
下次凌晨被叫醒,先别急着谷歌「ubuntu 怎么装 htop」。/proc 就在那里,内核从你开机那一刻就在帮你记账了。
相关阅读:
- 生产环境死锁排查完全指南 — 线程死锁与 MySQL 锁等待
- Python 内存泄漏排查实战:tracemalloc + objgraph — 从症状到根因
- 生产环境 MySQL 慢查询排查实战 — 全链路复盘
- Linux 生产环境 CPU 100% 问题排查实战 — 从发现到定位