卷 VI · 落地CH 23深度 23/24
那些你以为是「玄学」的现象
这本书开头列了六个看起来互不相干的问题。现在你有了足够的零件,可以把它们——以及另外一些——一次性解决掉。这一章是一张对照表:现象、原因、怎么确认、怎么办。
怎么用这一章
每一条的结构都一样:你看到什么 → 实际发生了什么 → 怎么确认 → 怎么办。第三步最重要——绝大多数排障失败,是因为跳过了「确认」直接开始猜。
一 · CPU 与调度
1. load average 是 15,但 CPU 几乎空闲
| 实际发生的 | Linux 的 load 是「可运行的 + 不可中断睡眠(D 状态)的」进程数,不是 CPU 使用率。那 15 个大概率卡在磁盘或网络存储上(第 5 章) |
|---|---|
| 怎么确认 | ps -eo stat,comm | grep ^D —— 数一数有几个 D。再看 vmstat 1 的 b 列(阻塞进程数)和 wa 列(IO 等待) |
| 怎么办 | 往 IO 方向查:磁盘满了?NFS 挂点断了?云盘限流打满了?加 CPU 一点用都没有 |
2. 某个进程 kill -9 都杀不掉
| 实际发生的 | 它在 D 状态,正卡在内核里等 IO。信号的投递需要「返回用户态」这个时机,而它没有(第 21 章) |
|---|---|
| 怎么确认 | cat /proc/PID/stack 看它卡在内核的哪个函数;ps -o stat 确认是 D |
| 怎么办 | 别再试着杀它。修复底层存储,IO 一恢复它自己就死了。如果是网络存储,umount -f 或恢复网络 |
3. top 里所有进程 CPU 都不高,但机器满了
| 实际发生的 | CPU 被花在了不属于任何进程的地方:中断上半部(hi)和软中断下半部(si)。高并发网络负载下 si 能占到 20%–30%(第 4 章) |
|---|---|
| 怎么确认 | top 按 1 展开每个核,看 hi/si 两列。再看 /proc/interrupts 和 /proc/softirqs 哪一类在涨 |
| 怎么办 | 开网卡多队列 + RPS/RSS 把中断分散到多个核;检查是不是某个核被打满了(中断默认可能全打在 0 号核上) |
4. 容器里 CPU 用量远没到限额,P99 却有一堆尖刺
| 实际发生的 | ★ CFS 限流抖动。平均用量低,但某个瞬间多个线程并行用完了 100 ms 周期里的配额,整个进程组被冻住直到下个周期(第 8 章) |
|---|---|
| 怎么确认 | cat /sys/fs/cgroup/cpu.stat,看 nr_throttled 和 throttled_usec 涨不涨 |
| 怎么办 | 放宽 cpu.max,或者减少并行度(线程池调小、让运行时数对核数——见第 8 条) |
二 · 内存
5. free 显示只剩几百 MB,但一切正常
| 实际发生的 | 那些内存被 page cache 占着,随时可以回收(第 18 章) |
|---|---|
| 怎么确认 | 看 free -m 最后那列 available,不是 free 列 |
| 怎么办 | 什么都不用做。这是正常的。也别去 echo 3 > /proc/sys/vm/drop_caches——那只会让你的服务重新变慢 |
6. malloc 明明成功了,进程却被杀了
| 实际发生的 | overcommit:malloc 只是记账,物理页在你第一次写时才分配。分不出来时,OOM killer 直接 SIGKILL——一次成功的分配被事后撤销(第 15 章) |
|---|---|
| 怎么确认 | dmesg | grep -i oom(全局)或 cat /sys/fs/cgroup/memory.events 的 oom_kill(容器) |
| 怎么办 | 加内存 / 降低用量 / 调 oom_score_adj。写代码层面拦不住它——SIGKILL 无法捕获 |
7. 容器限了 512 MB,堆只设了 256 MB,还是 OOM
| 实际发生的 | 容器限的是 RSS,而 -Xmx 只管堆。Metaspace、线程栈、JIT 缓存、GC 结构、直接内存全在外面(第 15 章) |
|---|---|
| 怎么确认 | JVM 开 -XX:NativeMemoryTracking=summary,然后 jcmd <pid> VM.native_memory summary |
| 怎么办 | 把 -Xmx 设成容器限额的 50%–70%,别设 80%+。或者用 -XX:MaxRAMPercentage 让 JVM 自己按 cgroup 算 |
8. 内存只涨不落,但确实没有泄漏
| 实际发生的 | 两种可能叠加:① free 不把内存还给操作系统,只还给分配器的空闲链表;② 外部碎片让空闲内存用不上,只好继续向内核要(第 14 章) |
|---|---|
| 怎么确认 | 对比 RSS 和你的应用自己统计的对象总量。如果应用层内存稳定而 RSS 一直涨,多半是碎片 |
| 怎么办 | 换分配器试试:LD_PRELOAD=/usr/lib/libjemalloc.so ./app。jemalloc / tcmalloc 对多线程碎片的处理明显更好,不用改一行代码 |
9. 内存少了一点点,性能直接塌方
| 实际发生的 | ★ 颠簸。工作集装不下,刚换出的页马上又要用。主缺页比内存访问慢三万多倍,所以只要百分之十几的访存变成主缺页,平均值就被完全主导(第 12 章) |
|---|---|
| 怎么确认 | vmstat 1 的 si/so 列持续非零;或者 ps -o maj_flt 持续增长。注意:关了 swap 也会颠簸——那时 si/so 是 0 但 bi 很高(在反复丢弃并重读代码页) |
| 怎么办 | 加内存或减小工作集。没有第三条路——关 swap 只是换了个死法 |
三 · 文件与 IO
10. rm 掉了大日志,df 显示磁盘没变空
| 实际发生的 | rm 只是 unlink——删了名字。nlink 归零了但还有进程开着它,数据块不释放(第 17 章) |
|---|---|
| 怎么确认 | lsof +L1 —— 列出所有 nlink < 1 但仍被打开的文件,一抓一个准 |
| 怎么办 | 重启那个进程;或 : > /proc/PID/fd/N 截断。下次别 rm,用 truncate -s 0,或者让 logrotate 发 SIGHUP 让进程重开文件 |
11. 磁盘还有 70% 空间,却报 No space left
| 实际发生的 | inode 用完了,不是空间用完了。海量小文件的典型症状(第 17 章) |
|---|---|
| 怎么确认 | df -i(注意是 -i 不是 -h) |
| 怎么办 | 清理小文件(缓存目录、session 文件、邮件队列)。长期方案是换 XFS/Btrfs——它们动态分配 inode |
12. 服务还活着,但接不了新连接
| 实际发生的 | fd 耗尽,accept 返回 EMFILE(第 16 章) |
|---|---|
| 怎么确认 | ls /proc/PID/fd | wc -l 对比 cat /proc/PID/limits | grep files。而且 ls -l /proc/PID/fd 会直接告诉你泄漏的是什么类型 |
| 怎么办 | 短期调 ulimit -n;真正的问题是泄漏——找那些没关的 socket、Cursor、Response body |
13. 同一段代码,有时 1 微秒有时 3 毫秒
| 实际发生的 | page cache 命中与否。命中约 1 μs,不命中要读盘(第 18 章) |
|---|---|
| 怎么确认 | 看 ps -o maj_flt;或 strace -T 看每次 read 的实际耗时分布 |
| 怎么办 | 预热(vmtouch);给 page cache 留够内存;或者接受它,并在设计时按最坏情况算超时 |
四 · 容器
14. 容器里性能莫名其妙比宿主差
| 实际发生的 | ★ 程序数错了 CPU 核数。CPU 核数不属于任何 namespace,容器里看到的是宿主的全部核,于是 JVM/Go/Netty 按 16 核开线程去抢 0.5 核的配额(第 22 章) |
|---|---|
| 怎么确认 | 容器里跑 nproc 和 cat /sys/fs/cgroup/cpu.max 对比。Java 里打印 Runtime.getRuntime().availableProcessors() |
| 怎么办 | 升级到支持容器感知的运行时(JDK 10+ 默认开);或显式设 -XX:ActiveProcessorCount / GOMAXPROCS |
15. docker stop 每次都要等满 10 秒
| 实际发生的 | 你的进程没收到 SIGTERM。两个可能:① ENTRYPOINT 用了 shell 形式,PID 1 是 sh 且它不转发信号;② 你的进程是 PID 1 但没注册处理器,而 PID 1 的未注册信号会被静默丢弃(第 21 章) |
|---|---|
| 怎么确认 | 容器里 ps -ef 看 PID 1 是谁 |
| 怎么办 | ENTRYPOINT 用 exec 形式 ["java","-jar","app.jar"];加 docker run --init;应用里显式处理 SIGTERM |
16. 容器跑几天就报 fork: Resource temporarily unavailable
| 实际发生的 | 僵尸进程堆积耗尽 PID。你的应用当了 PID 1 却没履行 init 的收尸义务(第 5 章) |
|---|---|
| 怎么确认 | ps -eo stat | grep -c Z 数僵尸;cat /sys/fs/cgroup/pids.current 对比 pids.max |
| 怎么办 | docker run --init,或镜像里放 tini |
五 · 启动与延迟
17. 服务重启后前几分钟特别慢
| 实际发生的 | 三层缓存全冷:page cache(第 18 章)、应用层缓存、JIT 还没热 |
|---|---|
| 怎么确认 | 看 maj_flt 是不是在启动阶段暴增,之后趋于平缓 |
| 怎么办 | 预热(vmtouch 把关键文件读进 cache);滚动重启时控制节奏,别让冷实例一上来就扛全部流量 |
18. 平均延迟很漂亮,P99 有一堆固定间隔的尖刺
| 实际发生的 | 周期性事件的典型特征。按间隔猜:~30 秒 → 脏页回写(第 18 章);~100 ms 的整数倍 → CFS 限流(第 8 章);不规则但成簇 → GC |
|---|---|
| 怎么确认 | 把尖刺的时间戳画出来看间隔是否规律。cpu.stat 的 throttle 计数、GC 日志、vmstat 的 bo 列三者对时间轴 |
| 怎么办 | 按定位到的原因处理。关键是先把间隔量出来——间隔本身就是最强的线索 |
一个通用的排查顺序
如果你完全不知道从哪下手,按这个顺序走,十分钟内基本能定位到大类:
◆ 十分钟定位法
uptime—— load 高不高?top(按 1)—— CPU 花在哪:us用户态?sy内核态?wa等 IO?si软中断?这一步就能分出四个大方向。vmstat 1—— 看r(可运行)、b(阻塞)、si/so(换页)、cs(切换次数)free -m—— 看available,不是freeiostat -x 1—— 磁盘%util和awaitss -s—— 连接数、有没有大量 TIME_WAIT- 如果在容器里:
cat /sys/fs/cgroup/{cpu.stat,memory.events}—— 有没有被限流/被 OOM 过
这套流程的价值在于它是分岔的:第 2 步的四个方向会把你导向完全不同的后续。而这本书前面二十二章,就是这四个方向各自的地图。
↑ 回到应用层
Android 上对应的工具链,同样的思路换一套命令:
| 想知道 | 用什么 |
|---|---|
| 整体 CPU / 调度 / 帧率 | Perfetto(systrace 的继任者)——它能同时看到调度、缺页、Binder 调用、渲染管线 |
| 内存分解(PSS) | dumpsys meminfo <pkg> |
| fd 泄漏 | ls -l /proc/$(pidof pkg)/fd |
| 进程被谁杀的 | logcat | grep -i "lowmemorykiller\|am_kill" |
| 系统调用 | strace -p(需要 root 或可调试应用) |
Perfetto 特别值得学:它把这本书讲的几乎所有东西——线程状态(R/S/D)、上下文切换、缺页、系统调用、Binder 事务——放在同一条时间轴上。你在这本书里建立的每一个概念,在那张图上都有一条对应的轨道。
这一章的一句话
这十八个现象没有一个是玄学。它们全都是前面某一章那个机制的直接后果——而排障的关键从来不是「猜得准」,是「确认得快」:每一条都有一个能在十秒内跑完的确认命令。
最后一章,我们把散落在全书的价目表汇总成一张,并说清它到底该怎么用。