第 01 章 两张地图:程序的微观旅程 & 系统参数家族
墨:序章说了,优化先要有地图。这章我给你两张:第一张是”一个程序从你敲回车到硬件动起来”的旅程图,标清楚每一级谁在干活、瓶颈常出在哪;第二张是”/proc/sys 下到底有哪些参数家族、各自管什么”。有了这两张,后面 26 章你随时能回来看”我现在在第几格”。
你:等下,为什么是”两张”而不是一张?
墨:因为你要解决的问题分两类。一类是”我的程序到底经历了什么”——这是链路地图(第 2-11 章慢慢走);另一类是”我想拧哪个 knob 影响哪一级”——这是参数地图(第 12-17 章)。链路告诉你”卡可能在哪”,参数告诉你”能从哪下手”。两张拼起来,才是完整的作战图。
1.1 地图一:一个程序的微观旅程(总图)
下面这条链,是全书所有故事的骨架。每一级我标注了它在哪层、干了啥、延迟/坑常出在哪。
[用户态·构建期]
① 源码 (.c/.go/.java)
└─ 预处理→编译→汇编→链接 → 产出 ELF 文件(躺在磁盘上)
(第 2 章:这里决定了代码段/数据段/符号表长啥样)
[用户态·运行期起点]
② 你敲 ./a.out
└─ shell 调 fork()+execve()
└─ 内核 execve:读 ELF、建虚拟地址空间、mmap 各段、跳到入口
(第 2 章:加载器怎么把"文件"变成"进程")
[进程活着,CPU 跑指令]
③ CPU 取指→译码→执行(用户态)
└─ 虚拟地址 → MMU 查页表 → 物理地址
├─ 命中 TLB:快
└─ 未命中:page walk(走 4 级页表),再未命中 → 缺页中断
(第 3 章:虚拟内存/MMU/缺页,延迟常出在这)
④ 要更多内存?brk()/mmap() 向内核要
└─ 内核 MM:先找空闲页(buddy)→ 没有就回收(page cache / 匿名页 swap)
(第 6 章:分配器、回收、swap、OOM)
[要干活,得进内核]
⑤ 用户态碰不到硬件 → 发系统调用(syscall)
└─ 特权级切换(用户态→内核态)、保存寄存器、查 syscall 表
(第 4 章:syscall 机制、上下文切换成本)
[内核子系统接力]
⑥ 调度:进程在 CPU 上跑多久、谁先跑
└─ CFS 红黑树 / 实时调度 / 抢占点
(第 5 章:调度,延迟常出在"等不到 CPU")
⑦ 文件 IO:read()/write()
└─ VFS → 具体 FS → page cache(命中就不落盘!)→ 不命中才走块层
(第 7 章:VFS、page cache,大多数"IO 慢"其实卡在 cache 未命中)
⑧ 块层:把 IO 请求排好队
└─ bio → 请求队列 → IO 调度器(mq-deadline/bfq/none)→ 驱动
(第 8 章:IO 栈,毛刺常出在调度器/磁盘队列)
⑨ 网络:send()/recv()
└─ socket → TCP/UDP → IP → 邻居/路由 → 网卡驱动 → 线缆
(第 10 章:网络栈,重传/拥塞常出在这)
⑩ 驱动 + 硬件
└─ 驱动把请求写成寄存器、触发 DMA 把数据搬进/出内存
└─ 磁盘/NIC 完成 → 发中断 → 内核软中断(softirq)收尾
(第 9 章:中断、驱动、DMA,这里是"离硬件最近的一跳")
[回到用户态]
⑪ 结果通过 syscall 返回值回到用户态,CPU 切回用户态继续
└─ 一圈闭环。下一次系统调用再走一遍。
墨:你扫一眼这张图,发现什么了?
你:链路太长了。一个 read() 背后横跨 VFS、page cache、块层、驱动、硬件、中断……整整七八级。所以”IO 慢”的时候,根因可能藏在这八级里的任何一级。
墨:对。链路越长,定位越值钱。外行看”IO 慢”,高手看”是 page cache 没命中(第⑦级)还是磁盘队列堵了(第⑧级)还是中断处理不过来(第⑩级)“——这三者的解法天差地别。后面每章就是把这一级里的”微观发生了什么”放大给你看。
1.2 地图一的补图:延迟都”藏”在哪几级
为了让你有”手感”,先给一张”常见性能问题的藏身处”速查:
| 现象 | 最可能藏在第几级 | 典型根因 |
|---|---|---|
| CPU 利用率高但活不多 | ③/④ | 频繁缺页、false sharing、死循环 |
| RT 偶发尖刺 | ⑥/⑧/⑩ | 调度抢占延迟、IO 写回毛刺、软中断积压 |
| 内存涨不停 | ④/⑥ | 真泄漏、page cache 挤压、slab 泄漏 |
| 磁盘 IO 慢 | ⑦/⑧ | page cache 未命中、IO 调度器不当、磁盘本身慢 |
| 网络重传/吞吐上不去 | ⑨/⑩ | TCP 参数、网卡多队列未开、软中断单核瓶颈 |
| OOM 杀进程 | ④/⑥ | overcommit 策略、swappiness、某进程真吃 memory |
墨:这张表你先有个印象就行,别背。它会在案例集(第 18-23 章)里被反复用到——那时你是”带着真问题回来查表”。
1.3 地图二:系统参数家族(/proc/sys 下都有谁)
你要的”别处还有类似 /proc/sys/vm 这种参数吗,单独分类整理”——答案在 /proc/sys/ 这个虚拟目录下。它按子系统分目录,每个目录是一”家”。我把全家族列出来,每一家给”管什么 + 几个代表参数 + 在第几章细讲”。
/proc/sys/
├── abi/ 兼容层(极少动)
├── crypto/ 内核加密算法默认(极少动)
├── debug/ 调试开关(sysctl kernel 也常挂这)
├── dev/ 设备管理(如 dev.cdrom、dev.raid)
├── fs/ 文件系统相关(file-max、inode、nr_open、aio、pipe)
├── kernel/ 内核核心(pid_max、threads-max、sem、panic、softlockup、printk)
├── net/ 网络全家桶(core / ipv4 / ipv6 / unix / bridge ...)
├── user/ 用户命名空间资源上限(user.max_*)
└── vm/ 虚拟内存子系统(你点名的这块,最常被调)
墨:所以 /proc/sys/vm 不是”孤本”,它只是”vm 这一家”。你之前只盯它,等于只看了九分之一。
你:对。而且 net 那家比 vm 还大——tcp 调优基本全在 /proc/sys/net/ipv4/ 和 /proc/sys/net/core/。
墨:没错。下面把每家摊开,给你”目录级”的认识。细讲在对应章。
1.3.1 vm/ —— 虚拟内存(第 12-14 章专讲)
管”内存怎么分配、怎么回收、怎么换出、怎么写回磁盘”。代表:
| 参数 | 一句话 |
|---|---|
vm.swappiness | 内核多积极把匿名页换到 swap |
vm.dirty_ratio / dirty_background_ratio | 脏页占内存多少比例后阻塞/后台写回 |
vm.dirty_expire_centisecs | 脏页存活多久必须写回 |
vm.overcommit_memory | 内存超卖策略(0/1/2) |
vm.overcommit_ratio | 超卖模式下允许超的比例 |
vm.min_free_kbytes | 永远留出多少空闲内存防碎片/防分配失败 |
vm.drop_caches | 手动清 page cache / dentries(排障用) |
vm.zone_reclaim_mode | NUMA 节点内存紧张时是否先回收本节点 |
vm.compaction_proactiveness | 主动内存规整(防高阶分配失败) |
vm.vfs_cache_pressure | 多积极回收 dentry/inode 缓存 |
1.3.2 net/ —— 网络(第 15 章专讲)
最大的一家。子目录:
net/core/: netdev_max_backlog、somaxconn、netdev_budget、rmem_default/max、wmem_*net/ipv4/: tcp_tw_reuse、tcp_fin_timeout、tcp_keepalive_*、tcp_rmem/tcp_wmem、tcp_sack、tcp_window_scaling、ip_local_port_range、tcp_max_syn_backlognet/ipv6/、net/unix/、net/bridge/等
代表参数(挑几个):
| 参数 | 一句话 |
|---|---|
net.ipv4.tcp_tw_reuse | TIME_WAIT 端口能否复用(高并发短连接救命) |
net.ipv4.tcp_max_syn_backlog | SYN 队列长度(防 SYN flood / 握手丢) |
net.core.somaxconn | accept 队列上限(backlog) |
net.core.netdev_max_backlog | 软中断收包队列上限 |
net.ipv4.tcp_rmem/wmem | TCP 收发缓冲自动调优区间 |
net.ipv4.ip_local_port_range | 客户端可用临时端口范围 |
1.3.3 fs/ —— 文件系统(第 16 章讲)
管”能开多少文件、多少 inode、异步 IO”。代表:
| 参数 | 一句话 |
|---|---|
fs.file-max | 全系统最大打开文件句柄数 |
fs.nr_open | 单进程最大打开文件数(受 RLIMIT_NOFILE 约束) |
fs.inode-nr / inode-state | inode 使用状态(只读观测) |
fs.aio-max-nr | 异步 IO 最大并发请求数 |
fs.pipe-max-size | 管道最大容量 |
fs.leases-enable | 文件租约(极少动) |
⚠️ 坑:fs.file-max 是系统级,但真正限制你进程的常常是 ulimit -n(per-process,对应 fs.nr_open 上限)。很多人调了 file-max 没用,因为进程的 RLIMIT_NOFILE 还是 1024。调参要看”限制到底卡在系统级还是进程级”——又是定位优先。
1.3.4 kernel/ —— 内核核心(第 16 章讲)
管”PID 上限、线程数、IPC、panic 行为、锁检测”。代表:
| 参数 | 一句话 |
|---|---|
kernel.pid_max | 最大 PID 号(容器密度高时要调大) |
kernel.threads-max | 全系统最大线程/进程数 |
kernel.sem | System V 信号量数组(某些老数据库依赖) |
kernel.panic | panic 后几秒自动重启(生产常设非零) |
kernel.softlockup_panic / hardlockup_panic | 软/硬锁死是否直接 panic(排障开关) |
kernel.watchdog | 看门狗开关 |
kernel.printk | 内核日志级别 |
kernel.sched_* | 调度相关(部分在此,部分在 /proc 或 debugfs) |
1.3.5 其余小家(第 17 章讲)
abi/:二进制兼容(如abi.vsyscall32),几乎不动。crypto/:内核加密默认算法(如crypto.fips_enabled),合规场景动。debug/:调试开关(如debug.exception-trace),排障时开。dev/:设备相关(如dev.cdrom.autoclose、dev.raid.speed_limit_max限制 md 重建速度——这个调 RAID 重建很有用)。user/:用户命名空间资源上限(user.max_user_namespaces、user.max_net_namespaces),容器/沙箱密度场景动。
墨:你看,参数家族一大圈,但真正日常会被你动的,九成在 vm / net / fs / kernel 这四家。本书花最多篇幅在这四家,小家一笔带过给个索引。
1.4 “参数”不止 /proc/sys:还有这些兄弟
你问”别处还有类似参数吗”——除了 /proc/sys(sysctl 接口),Linux 还有几类”也是旋钮、但不在 /proc/sys 下”的东西,值得一并知道,免得你找错地方:
- sysfs(
/sys/):设备/驱动级参数。例如/sys/block/sda/queue/scheduler(选 IO 调度器)、/sys/class/net/eth0/queues/(网卡多队列)、/sys/kernel/mm/transparent_hugepage/enabled(大页开关)。这是”设备视角”的旋钮,和 vm 那家是互补的。 - cgroup(
/sys/fs/cgroup/):按进程组分资源上限(cpu、memory、io、pids)。K8s 那本第 15 章讲过,这里它是”隔离级”旋钮。 - /proc 下的非 sysctl 文件:很多是只读观测,不是旋钮。如
/proc/meminfo、/proc/vmstat、/proc/zoneinfo、/proc/interrupts、/proc/slabinfo、/proc/PID/status。它们是”显微镜”,调参前先读它们。 - ulimit / RLIMIT:进程级资源上限(文件数、栈、core、地址空间),用户态
ulimit -a看,PAM//etc/security/limits.conf配。 - 内核启动参数(cmdline):
/proc/cmdline,如transparent_hugepage=never、numa_balancing=0、elevator=none,开机定生死,运行时改不了(除非走 sysfs 等价项)。
⚠️ 坑:有人想”运行时改大页”,去改 /proc/sys/vm 找不到,因为大页开关在 /sys/kernel/mm/transparent_hugepage/。旋钮的物理位置 = 它的作用域:系统级状态在 /proc/sys,设备级在 /sys,进程级在 ulimit/cgroup。找错了地方当然调不到。
1.5 问题地图总表(现象 → 先怀疑哪级 + 先读哪个文件)
这是把”地图一”和”地图二”合成的一张排障入口表。你遇到现象,先看”先怀疑哪级”,再”先读哪个文件/参数”。
| 现象 | 先怀疑子系统 | 先读的文件/参数 |
|---|---|---|
| 物理内存满、swap 飙 | vm(回收/swap) | /proc/meminfo、vmstat、swappiness |
| 写盘慢、周期性卡 | vm(脏页写回) | vm.dirty_*、/proc/vmstat(nr_dirty) |
| 打不开文件 / “Too many open files” | fs(file-max / ulimit) | fs.file-max、ulimit -n、/proc/PID/fd |
| 短连接端口不够 / TIME_WAIT 堆积 | net(ipv4) | ip_local_port_range、tcp_tw_reuse、ss -tan |
| 握手慢 / 连接丢 | net(队列) | tcp_max_syn_backlog、somaxconn、netstat -s |
| 进程起不来 / fork 失败 | kernel(pid/threads) | pid_max、threads-max、dmesg(OOM) |
| 磁盘 IO 毛刺 | 块层 / 调度器 | /sys/block/*/queue/scheduler、iostat |
| 网卡丢包 / 软中断单核满 | net(softirq) | /proc/interrupts、/proc/net/softnet_stat |
| 大内存分配失败(即使内存够) | vm(碎片/高阶) | /proc/buddyinfo、zone_reclaim_mode、compact |
| 容器密度高诡异卡 | kernel/fs/cgroup | pid_max、file-max、cgroup 限额 |
墨:这张表就是你的”第一反应清单”。但记住 0.2 节的环——怀疑哪级只是第②步,必须第④步观测验证,推错就回头。表里写”先怀疑”,不是”就是它”。
1.6 演化线小结(为什么参数是这样长出来的)
- 问题:内核要在”性能、公平、稳定、安全”之间永远权衡;且不同负载画像(IO 密集 vs 计算密集 vs 延迟敏感)要不同取值。
- 设计:把每个可调旋钮暴露成
/proc/sys下按子系统分类的文件,运行时可读写(sysctl -w),无需重编译内核。设备级走 /sys,进程级走 ulimit/cgroup,开机级走 cmdline——作用域决定物理位置。 - 演化规律:参数越来越多(新内核不断加新 knob),是因为新硬件/新负载逼出新的权衡点(如 NUMA、THP、多队列网卡、容器密度)。但内核社区纪律是”默认值要安全、要通用”——所以默认值面向”大多数服务器”,你的特殊负载才需要拧。又是一条”兼容优先、按需调优”的线,和 K8s 那本的演化规律一脉相承。
1.7 思考题
🔧 思考题 1(参数本质):为什么说”内核参数几乎都是权衡,不是开关”?举 swappiness 和 dirty_ratio 各说明一次。
【参考答案】因为内核的每个旋钮都在两个相互冲突的目标间取值。swappiness 高→积极换匿名页腾内存给 page cache→文件 IO 快但进程可能被换慢;低→进程不被换但 cache 被挤→文件 IO 慢。没有”高好”,只有”IO 密集还是内存敏感”。dirty_ratio 高→更多写先攒内存再写回→吞吐高、但一次写回的数据量大、易尖刺、掉电丢更多;低→更及时写回→延迟稳但吞吐低、写放大。两者都是吞吐↔延迟/安全的权衡。所以调参是”匹配负载画像”,不是”开高性能”。
🔧 思考题 2(定位优先):一个服务 RT P99 从 50ms 飙到 2s,你第一反应应该是”调哪个参数”还是”先读哪个文件”?为什么?
【参考答案】先”读文件/观测”,不是调参数。0.2 节的环:第①现象→第②推断(基于地图圈定”可能网络/调度/IO/内存”)→第④观察验证,确认根因后才第③试错。直接调参是”未定位就下手”,极可能调错地方。正确做法:先看 vmstat(si/so、wa)、iostat(await/util)、mpstat(CPU 分布)、ss/netstat -s(重传/TW)、perf(热点),用数据把”RT 高”翻译成”具体哪级瓶颈”,再决定拧哪个 knob。本书所有案例都先观测后动手。
🔧 思考题 3(旋钮位置):你想”关闭透明大页”和”调大单进程可开文件数”,分别该去哪改?为什么位置不同?
【参考答案】透明大页(THP)是内核内存管理特性,设备/特性级,开关在 /sys/kernel/mm/transparent_hugepage/enabled(也可开机 cmdline transparent_hugepage=never 固化)。单进程文件数是进程级资源上限,在 ulimit -n(用户态,配 /etc/security/limits.conf)或 fs.nr_open(系统级上限,进程 RLIMIT 不能超过它)。位置不同因为作用域不同:THP 影响全系统内存行为→内核特性文件;文件数限制单个进程→进程级 RLIMIT(系统级给天花板 nr_open)。找旋钮先想”它作用在哪个层级”,再去对应位置。
🔧 思考题 4(进阶·总账):你的服务是”高并发短连接网关”,现象:TCP 连接建立偶尔失败、ss -tan 看到几万 TIME_WAIT、日志偶发 “Cannot assign requested address”。请用两张地图推断:最可能卡在第几级、哪个子系统、先读哪两个参数、试错先动哪个?并说明为什么不能直接断定”就是 tcp_tw_reuse 没开”。
【参考答案】① 现象归类:连接建立失败 + 大量 TIME_WAIT + “Cannot assign requested address”(客户端端口耗尽)= 典型客户端临时端口耗尽 / TIME_WAIT 占满端口范围。② 在地图一里属于第⑨级(网络栈·客户端建连);在地图二里属 net/ipv4 家。③ 先读:net.ipv4.ip_local_port_range(可用端口范围多大)、net.ipv4.tcp_tw_reuse(是否允许复用 TIME_WAIT 端口)、ss -tan | grep TIME-WAIT | wc -l(确认 TW 量)。④ 试错优先级:tcp_tw_reuse=1(允许新连接复用 TW 状态的端口,专治此症)+ 扩 ip_local_port_range。⑤ 为什么不能直接断定”就是 tcp_tw_reuse 没开”:因为”Cannot assign requested address”也可能是本机可用端口范围本身太小(range 默认 32768-60999 仅约 28k,高并发不够)即便 tw_reuse 开了仍可能耗尽;也可能是每连接没及时关闭(应用 bug 持连)导致 TW 累积——这是应用层问题,调内核参数只治标。所以必须先 ss 看 TW 量 + 看 range,确认”端口真不够 + TW 占满”再动参数;若发现是连接泄漏,根因在代码。这正是”推断→观测验证→可能回头”的环:不能直接跳到”开 tw_reuse”。
1.8 一个”慢”字背后,可能是这九级里的某一环
墨:第 1.3 节给你的是”参数在哪家”的地图。这节给你一张反查地图——当你只会说”慢”的时候,这个”慢”到底可能落在哪一级。它是后面第 02-11 章故事线的”目录版”,你先混个脸熟,走的時候就有锚点。
- 编译/链接级(第 02 章):静态链接把 200MB 依赖打进二进制,启动慢、内存占双倍——你以为的”程序慢”,其实是”加载就重”。
- 进程/虚拟内存级(第 03、06 章):TLB miss、缺页风暴、direct reclaim——进程”卡”可能卡在”地址翻译”和”等内存回收”,根本不在你的代码。
- 系统调用/内核态级(第 04 章):一次
read的陷态 + 上下文切换 + 拷贝,高频小 IO 在这里累积成毫秒级开销。 - CPU 调度级(第 05 章):CFS 节流、优先级反转、NUMA 绑核不当——“算力够却跑不快”常是这里出问题。
- 文件系统/Page Cache 级(第 07 章):cache 命中与否差 1000 倍延迟;脏页回写拥堵让你”写”卡住。
- 块层/IO 栈级(第 08 章):调度器不匹配、队列浅、写放大——“盘慢”的锅常在这层,不在盘本身。
- 中断/软中断级(第 09 章):单核
si100%、softnet 丢包——高 PPS 下吞吐上不去的隐形天花板。 - 网络栈级(第 10 章):握手队列、TIME_WAIT、重传、窗口——建连失败和 RT 高的大本营。
- 硬件/驱动级(贯穿):DMA、MMU、cache 一致性——最底层,但前面每一级最终都落到这。
墨:记住——“慢”是一个症状,不是一层原因。这九级就是你的”怀疑清单”。后面每章拆一层,等你走完,任何”慢”你都能一口气道出”可能是第 3 级或第 7 级,我去用 X 工具验证”,而不是打开搜索引擎又去拜一次许愿池。
墨:两张地图铺好了。下一章起,我们沿地图一往下走——从你敲
./a.out的那一刻开始,看一个程序怎么从”磁盘上的文件”变成”CPU 上跑的进程”,编译、链接、ELF、加载器,一级级拆。那是理解后面所有”微观”的地基。
(第 01 章 完)
1.9 一张”怀疑清单”:卡顿时按这张表逐项排除
墨:第 1 章前面把”九级链路”和”问题地图”都铺了。这一节给你一张可以直接贴在显示器上的”故障怀疑清单”——以后任何”慢/卡/挂”,先别慌,照这张表从最可能的那一级开始,逐行往下捅。它是全书 26 章的”目录版排障入口”。
1.9.1 总表:现象 → 先怀疑哪级 → 先抡哪把工具 → 先查哪章
| 你看到的现象 | 先怀疑哪一级(第几章) | 先抡哪把工具(第25章) | 先查哪个参数/文件 |
|---|---|---|---|
| 整机卡顿、所有操作一起慢 | 内存 direct reclaim(06/12/18) | vmstat 看 allocstall | watermark_scale_factor/min_free_kbytes |
| 周期性卡几秒、像心跳 | 内核暂停类(18章五兄弟) | dmesg/bpftrace | THP compact/softlockup |
| 内存满了被杀、但 free 有盈余 | OOM 边界(19)/cgroup(17) | dmesg 看前缀 | memory.max/oom_score_adj |
| 磁盘相关慢、I/O 高 | 块层/IO 栈(08/20) | iostat/biolatency | /sys/block 队列/%util误读 |
| 网络超时、一半请求挂 | 网络栈/路由(10/15/21) | ss/tcpdump/nstat | rp_filter/somaxconn/tw_reuse |
| 连接建不起来、端口不够 | 网络 ephemeral(21) | ss -tan 看 TIME_WAIT | ip_local_port_range/tcp_tw_reuse |
| ”内存泄漏”告警但业务正常 | 缓存挤压/误报(22) | free 看 available/PSI | MemAvailable/swappiness |
| 大内存机器莫名抖动 | NUMA/大页(23) | numactl/numastat | zone_reclaim_mode/nr_hugepages |
| 文件句柄/进程起不来 | fs/kernel 配额(16) | cat /proc/sys/fs/* | file-max/nr_open/pid_max |
| 容器内 OOM 但宿主很闲 | cgroup 边界(17/19) | 看 cgroup memory.stat | memory.max(非 vm.*) |
| 改了 sysctl 不生效 | 三世界找错门(17) | tuned-adm active | tuned/ulimit/cgroup |
1.9.2 墨叔的三条排障心法(贴显示器)
你:这张表好用,但实战里我容易”先怀疑自己最熟的”。 墨:对,这是人性——懂网络的人永远先怀疑网络(第 25 章那个被 bpftrace 翻案的”网络卡”就是例子)。给你三条反人性心法:
- 先证伪,再证真:别上来”肯定是网络”,先花 30 秒证伪——
iostat/vmstat/sar三把零依赖刀各扫一遍,哪个指标真的异常,就从那一级入手。第 25 章的 SOP 就是这个意思。 - 看”相对变化”不看”绝对值”:
%util30% 不算高,但比昨天同时段涨了 20 倍就是信号;永远和历史比,和邻居比(同机多实例、同集群多节点)。 - 一次只动一个变量:这是第 0 章优化哲学的铁律。你同时改 swappiness 和 dirty_ratio,好了不知道是谁的功,坏了不知道是谁的过。改一个、观察一个、记录一个。
1.9.3 一个”照表操作”的迷你演练
📦 迷你场景:凌晨告警"订单服务 P99 从 50ms 涨到 2s",你被叫起来。
- 第 1 步(证伪):
vmstat 1看——allocstall在 2s 尖峰时暴涨 → 锁定内存 direct reclaim(表第一行),不是网络、不是磁盘。 - 第 2 步(定位):
cat /proc/sys/vm/watermark_scale_factor= 10(默认),机器 256G → 缓冲带才 256MB → 第 12 章”大内存机器必调”的坑。 - 第 3 步(止血):临时
sysctl -w vm.watermark_scale_factor=100,尖峰消失;白天再按第 13 章画像组合固化。 - 全程只动了这一个变量,根因清晰、回滚简单。
墨:你看,第 1.9 这张表 + 三条心法,把”凌晨被叫起来手忙脚乱”变成了”照表 30 秒锁定一级”。这就是”懂地图”的价值——你不需要记得每个参数,你只需要记得”去哪张地图查”。 全书剩下的 24 章,就是这张表的每一格的展开版。
1.10 实战补遗:用”九级地图”反查一次跨机房慢请求
墨:一个接口,同城快、跨机房慢,你拿到这条现象,会怎么用第 1.8 的九级地图去拆?
你:跨机房嘛,先看网络层。
墨:网络是第一嫌疑,但”慢”可能落在九级里任何一级。我带你走一遍:把”跨机房慢”当症状,逐级排除,而不是直接钉死网络——这正是地图的价值。
现象
服务 A(机房1)调服务 B(机房2)的读接口,RT 同城 5ms、跨机房 200ms,且 P99 偶发 2s。B 自身处理代码同城跨机房完全一样。
逐级反查(排除法)
- **应用层:**B 的埋点显示自身处理同城跨机房都是 3ms → 排除应用逻辑。
- 系统调用层:
strace -e trace=network,read,write -p <B_pid>看recv/send耗时 → 不多,排除 syscall 卡。 - **调度/CPU:**B 实例 CPU 没抢、没 throttle(
cpu.stat里throttled_periods=0),排除。 - **文件系统/cache:**这是读接口,B 不落盘,排除。
- **块层/IO:**同上,无磁盘 IO,排除。
- **中断/软中断:**B 网卡
si正常,排除。 - **网络层:**到这里才重点看。
ss -i看跨机房连接的rtt(200ms 级,符合物理距离)、retrans(0.1%)、cwnd(长期很小,初始 10)。nstat有偶发TCPLostRetransmit。 - **协议栈:**重传偶发说明中间专线有抖动丢包,不是参数错配。
- **硬件:**专线带宽没满,排除。
结论
根因是”长肥管道 + 偶发丢包”导致拥塞窗口爬不上去,不是参数错配;但可以调 tcp_window_scaling=1(默认开)、tcp_slow_start_after_idle=0(空闲后不重回慢启动)来缓解。tcp_slow_start_after_idle=0 拿”长连接常驻大 cwnd”换”可能占更多带宽”,对长肥管道服务值得。
注意:第 7 级才定位,前面 6 级是排除法。这正是一张地图的价值——让你知道先排除什么,而不是盲调网络。
再观察
调 tcp_slow_start_after_idle=0 后,跨机房长连接空闲后 cwnd 不回退,P99 从 2s 降到 300ms(物理距离硬下限仍在)。
🔧 思考题:为什么”排除法”在九级地图里比”直接猜网络”更稳?
【参考答案】
因为九级里每一级都可能单独造成”慢”:直接猜网络会漏掉”其实是 B 的 CPU throttle""其实是本地软中断打满""其实是块层 IO 排队”。地图的价值是给你一张有序检查清单:从应用往下逐层排除,每层有对应观测命令(strace / cpu.stat / ss / iostat),把”感觉”变成”逐项验证”。排除了 6 级,第 7 级才是网络——这种确定性比拍脑袋稳得多,也避免”调了网络半天发现是调度 throttle”的返工。慢请求排查的第一原则:先证明不是哪一级,再认定是哪一级。
实战补遗一:把”九级地图”落成一张可勾选的排障 SOP(两个填表例子)
墨:第 1.8 给了九级地图、第 1.9 给了怀疑清单。但”地图”离”用得顺手”还差一步:把它变成一张能勾选的 SOP。我给你填两个真实症状,你看怎么走。
你:不就是对着九级一个个看吗?
墨:对,但”勾选”的价值是不漏级、不跳级、不凭印象。很多人排障跳级——看到”慢”直接奔网络,跳过了前面 6 级,结果根因在调度。SOP 强制你逐级打勾。
SOP 模板(每级一句”查什么 + 用啥命令”)
□ 应用层 : 业务埋点耗时? 处理本身慢? —— 应用 metrics
□ syscall 层 : 卡在哪个系统调用? —— strace -e trace=...
□ 调度/CPU : 有 throttle? 单核满? —— cpu.stat / top -H
□ 文件/cache : 落盘? page cache 命中? —— iostat / cachestat
□ 块层/IO : await/aqu-sz 高? —— iostat -x
□ 中断/软中断 : si 单核满? 网卡丢包? —— mpstat / cat /proc/interrupts
□ 网络层 : rtt/retrans/cwnd? —— ss -i / nstat
□ 协议栈 : 重传? 窗口? —— nstat / ss -tin
□ 硬件 : 带宽/固件? —— ethtool / 硬件监控
填表例一:症状”写接口偶发 2 秒”
勾选过程:应用层(处理 3ms,排除)→ syscall(fsync 偶发 1.5s,命中)→ 后面级跳过。结论:卡在文件/cache 级(具体 fsync 等待刷盘),去第 8/18 章查脏页/写回。没必要去查网络。
填表例二:症状”新连接偶发建不上、超时”
勾选:应用层(连接都没进业务,排除)→ syscall(建连 connect 慢,命中在 network 方向)→ 调度/CPU(无)→ 文件/cache(无)→ 块层(无)→ 中断(单核 si 100%?可能命中)→ 网络层(ss -lnt Recv-Q 顶满、netstat listen queue errors 涨,命中)。结论:accept 队列满(第 15 章 somaxconn)或单核软中断瓶颈(第 9 章),不是业务代码。
墨叔一句
SOP 不是束缚,是防跳级的护栏。 你跳得过的级,勾掉;跳不过的,它会提醒你”这一级你还没看”。九级地图真正落地,就是这张能勾的表——平时贴显示器,出事照着打勾,半小时定位不靠猜。
🔧 思考题:如果两个症状在 SOP 里”同时命中多级”(比如既 fsync 慢又 si 满),怎么定主次?
【参考答案】
看”因果关系”而非”并列”。fsync 慢(写盘)和 si 满(收包软中断)可能同时出现,但问:是写盘慢导致业务吐数据慢、网卡收包堆积、si 满?还是 si 满抢了 CPU、业务 fsync 拿不到 CPU 才慢?用”时间先后 + 资源依赖”判断:iostat 的 await 先涨 → 是盘;si 先涨且 CPU 被软中断吃满 → 是中断。再不济,两路都先缓解(限 IO + 绑 IRQ),看哪个生效——SOP 给你的是”候选集”,最终用”先后 + 干预实验”定主次,不是谁数字大谁就是根因。这也呼应第 24 章:优化的每个动作都要有前后对比佐证,定位也一样,靠”干预后哪个指标回落”来确认因果。
实战补遗二:第一次读 /proc,以及”被数字骗了”的那次
墨:第一章给了全景地图,但地图是给你”认路”的,不是给你”下结论”的。讲个我刚学的时候被 /proc 数字骗惨的故事——免得你重蹈。
那年排查一个”内存泄漏”,top 看某个 Java 进程 RES 一路涨,从 4G 涨到 12G 不回头。我第一反应:泄漏了,准备 dump 堆。但老师傅拦住我,说”先 cat /proc/<pid>/smaps | grep -i pss 加总看看”。我一加总,Pss 才 4.2G。
你:RES 12G 但 Pss 才 4G?这俩不是都该是”进程用的内存”吗,差哪去了?
墨:这就是 /proc 里最容易被误解的一对。RES(RSS)是”进程映射的物理页总字节数”,但它不剔除共享页的重复计算——这个进程和别的进程共享的库(如 libc.so、JVM 自身的 mmap),在 RSS 里每个进程都算一份全量。一台机器上 20 个 Java 进程,每个 RSS 里都含一份 libjvm.so 的 200MB,实际物理内存只存了一份,但 20 个 RSS 加起来就虚高了 4G。
Pss(Proportional Set Size)才公道:共享页按”被几个进程共享”均摊。一份 200MB 的 libjvm.so 被 20 个进程共享,每个进程的 Pss 只计 10MB。所以 Pss 之和 ≈ 真实物理内存占用,RSS 之和会虚高。
推断:那个进程根本没泄漏,它 RSS 涨是因为共享库被新起的进程拉高了它的 RSS 统计口径——其实物理内存没多。老师傅一针见血:看”泄漏”看 Pss 趋势,别看 RES。我那次要是真去 dump 堆,白忙活几小时还可能误杀服务。
另一个被数字骗的经典:free 的 used 列。很多人看 used 高就慌,但 Linux 的 used 把 page cache 算进去了——而 page cache 是可以随时回收的”借来的内存”,不算真短缺。free 的 available 列才是”真能马上用的”。所以看内存永远看 available,别看 used。
你:那 top 里还有个 VIRT(虚拟内存),这个能信吗?
墨:最不能信的就是 VIRT。它包含所有映射的虚拟地址,不管有没有物理页——mmap 了一个 100G 的文件(只读了其中 1G)?VIRT 显示 100G+,但物理内存可能才用 1G。很多程序 mmap 大文件或预留大虚拟空间,VIRT 看着吓人,实际零成本。所以三兄弟的信噪比:VIRT(几乎不可信) < RES(部分虚高,因共享页重复计) < Pss(公道,但要自己算)。
🔧 思考题:一台 64G 机器,top 里 30 个 Java 进程每个 RES 都 5G,你敢说”内存用了 150G 快 OOM 了”吗?怎么用一条命令证伪?
【参考答案】
不敢。RES 把共享的 JVM/libc 等重复计了 30 遍,虚高严重。证伪用 Pss 求和,近似真实占用:
sudo grep Pss /proc/[0-9]*/smaps | awk '{s+=$2} END {print s/1024 " MB"}'这条把全系统所有进程的 Pss 加总,得到的是真实物理内存占用(不含内核、不含可回收 page cache)。如果它远低于 64G,说明”150G”是 RES 虚高,机器健康。更省事:cat /proc/meminfo 看 MemAvailable,它是内核自己算的”马上能用的内存”,直接和国际接。口诀:RES 看单进程规模、Pss 看真实占用、MemAvailable 看整机余量,三处别混。
实战补遗三:级地图反查 SOP——一张能打印的”排查 checklist”
墨:第 1 章给了九级地图(应用→VFS→页缓存→文件系统→块层→IO 调度器→驱动→驱动队列→硬件),第 11 章用它反查了跨机房慢。但”拿着地图”和”用得熟”之间差一张可执行的 checklist。我把它落成你能贴显示器上的步骤。
SOP:慢/抖/丢,逐层枚举,用工具拿证据
① 先定性:是 CPU 慢 / IO 慢 / 网络慢 / 内存紧? (top / vmstat / iostat / ss / free)
② 若是 IO 慢:
- 块层健康? iostat -x 看 %util/await/aqu-sz
- 长尾? biolatency(第25章)看分布, biosnoop 归因到进程
- 调度器选对? /sys/block/<dev>/queue/scheduler(第8章)
③ 若是网络慢:
- 重传? ss -ti 看 retrans; netstat -s 看 retransSeg
- 队列溢出? netstat -s 看 overflow/ignored(SYN队列,第15章)
- MTU 黑洞? tracepath 看 MTU 跳变(第11/21章)
- rwnd 塌方? ss -ti 看 rwnd + 接收端消费速度(第21章)
- 静默丢? rp_filter / conntrack(第15章)
④ 若是内存紧:
- 真紧? free 看 available, 别看 used(第1章)
- 泄漏? smaps 看 Pss 趋势, 别看 RES(第1/22章)
- 不可回收涨? meminfo 看 SUnreclaim + slabtop(第22章)
- 被 OOM? dmesg + cgroup memory.events(第19章)
⑤ 若是卡顿(不慢但抖):
- 软中断占满? top 看 si + /proc/softirqs(第9章)
- RCU/soft lockup? dmesg 看 stall/lockup(第18章)
- THP 合并? vmstat 看 thp_fault_alloc(第13/18/23章)
⑥ 作用域确认: 这个参数/现象是 进程级/容器级/节点级? 别找错门(全书反复)
你:这 SOP 看着长,真到事故时能想起来走吗?
墨:所以才要”贴显示器 + 演练”。新手卡壳,90% 是因为没有 checklist,凭印象乱试。我带团队时要求:每个新人 first week 拿这套 SOP 复现一遍”故意制造的故障”(如 tc 加延迟、stress 压内存),把”逐层枚举”练成肌肉记忆。等你闭眼能走完这套,你才真正”懂”了九级地图——不是背下来,是知道每一步用哪个工具、看哪个数。
这也呼应全书的”三级跳”:SOP 是第二级(会反推)的脚手架。等你熟了,脚手架可以拆,但新手期必须先把流程焊死,防止”在错误层空转”。
🔧 思考题:这套 SOP 第一步为什么是”先定性(CPU/IO/网络/内存)“,而不是直接进某一层深挖?
【参考答案】
因为定性错了,后面全错。九级地图九层,你若没先确定”问题在哪一族”,就会在错误的一族里耗几小时。例子:一个”请求慢”的事故,如果是网络重传(第③类),你去调 vm.dirty_*(第④类,内存/IO)永远查不出;如果是 page cache 误清(内存),你去抓 tcpdump(网络)也是白费。
“先定性”用的工具(top/vmstat/iostat/ss/free)都是聚合视图、零成本、秒级出结果,花 10 秒就能把怀疑面从”九层”缩到”一两层”,效率提升十倍。这正是第一章”先测量后调优”的落地:测量 = 先用聚合工具定性,再进具体层深挖。口诀:聚合视图定性(10秒) → 具体工具归因(按层) → 参数杠杆解决,顺序不能反。新手常犯的错就是跳过”定性”直接”凭猜测进某层抓包/改参”,结果在错误的层里打转——SOP 第一步就是强制你先定性,堵死这个坑。
实战补遗四:次 curl 慢到 8 秒——用九级地图在 5 分钟钉到第 4 级
墨:老哥,假设线上有人报”访问内部一个服务偶尔要 8 秒”。你拿到这个现象,第一反应会去查什么?
你:先 tcpdump 看包吧,或者看应用日志有没有慢查询。
墨:方向对,但容易一上来就扎进第 7 级(应用层)。我教你的是从外往里、一级一级排除,别跳级。那次 8 秒的真实排查是这样的。
现象:内部服务 A 调服务 B 的 HTTP 接口,p99 突然从 80ms 飙到 8s,但 B 的应用日志里处理时间只有 50ms。说明时间花在”网络/内核”这一段,不在 B 的业务代码。
推断:可能是 B 的进程太忙,请求在 B 的 accept 队列里排队。
可能推错:我第一反应是”服务端慢”,但 B 的 own 处理才 50ms,排队也不该排 8 秒。所以问题在 B 收到之前,或者 A 发出之后。
调整:用九级地图从外往里走。
- 第 1 级 网卡:A 端
ip -s link看 tx 有没有丢包、errors,无。 - 第 2 级 驱动/软中断:A 端
cat /proc/softirqs看 NET_RX 是否暴涨,平稳。 - 第 3 级 协议栈收发包:A 端
ss -tni看这个连接的rto、retrans,发现 retrans 在涨——说明包在路上丢了或被卡了。 - 第 4 级 socket 缓冲区:看
ss -tni里的skmem_r/skmem_w和cwnd,发现skmem_w长时间顶到sk_wmem_queued上限,发送缓冲区满,应用 write 阻塞。
再观察:socket 发送缓冲区满了,说明对端收得慢或者中间有瓶颈。看 B 端 ss -tni 的 skmem_r(接收缓冲区),发现 B 的 rmem 默认才 200K 左右,而 B 当时正在做批量序列化把单连接吞吐打满,接收缓冲区被填死,TCP 通告窗口归零,A 的发端被流控卡住——卡在第 4 级 socket 缓冲区,不是第 7 级应用。
改进:把 net.core.rmem_max 和 net.ipv4.tcp_rmem 上限调大(从 200K 到 4M),并给 B 的序列化加背压,8 秒消失,p99 回到 90ms。
🔧 思考题:为什么”应用处理只花 50ms”反而能帮你缩小排查范围?如果应用处理花了 7.9 秒,你的九级地图走法会有什么不同?
【参考答案】
“应用处理只花 50ms”是一个极强的否定证据:它直接排除了第 7 级(应用业务代码)和第 6 级(应用框架/序列化,如果 B 自己测的是端到端处理时间)。排查的本质是”用已知去砍掉未知”——每排除一级,搜索空间就小一圈。所以拿到现象先看”时间到底花在哪一段”,比一上来抓包高效得多。
如果应用处理花了 7.9 秒,那就说明瓶颈大概率在 B 自己的业务/框架层(第 6、7 级),九级地图的重心就立刻转向应用内 profiling(pprof、火焰图),而不是继续往下钻内核网络栈。先做”时间分段”,再决定往哪钻,这是排障 SOP 的第一条。
关键命令回顾:ss -tni(看每连接的 rto/retrans/cwnd/skmem)、ip -s link、cat /proc/softirqs、sar -n DEV 1。
实战补遗五:用火焰图把”偶发卡顿”钉到某个 Go 闭包分配——on-CPU 火焰图的正确读法
墨:老哥,你服务偶发卡顿,第一反应是看日志还是挂 pprof?
你:挂 pprof 看堆,但卡的是 CPU 不是内存。
墨:堆 pprof 查内存,卡顿查 CPU 该用 on-CPU 火焰图。但火焰图读错方向,你会盯着一个”看起来忙”的函数白忙。这节讲怎么用火焰图把卡顿钉到”某个闭包里的临时分配”。
现象:一个 Go 服务偶发 GC 尖峰(STW 几十 ms),p99 随之抖。你看 go tool pprof 的 CPU profile,发现 runtime.mallocgc 占比高,但到底是谁在疯狂分配看不出来——CPU profile 只告诉你”在分配”,不告诉”为什么分配这么多”。
推断:mallocgc 高 = 分配太多,加缓存减少分配。
可能推错:你说”减少分配”对方向,但火焰图如果只看”宽”(占比高)的框,会盯上 json.Marshal 这种”看起来是元凶”的大函数,于是在 json 上做文章,其实真正的分配源是被 json 调用的一个闭包里每次新建的临时 struct。火焰图读反了主语。
调整:正确读火焰图(on-CPU,横轴是采样占比、纵轴是调用栈):
- 找”尖”(高而窄)的塔,不是”宽”的底座。宽的是通用库(json、runtime),人人都调;尖的是你业务里”某条特定路径”异常突出。把鼠标放到尖塔顶,看它是被谁调用、调了什么——通常尖塔顶是你那个”每次请求 new 一个大 slice 的闭包”。
- 火焰图要抓对时机:偶发卡顿用
perf record -F 99 -g -p <pid> -- sleep 30在卡顿发生时抓;或 Go 的pprof.Profile()(CPU)定时采。别在空闲时抓(抓不到)。 - 配合
go tool pprof -http=:8080 cpu的 flame graph 视图 + peek/squeeze 交互,从底座层层钻到尖塔。
再观察:那次根因是”一个处理请求的闭包里 make([]byte, 0, 1024) 每次都新建,且这个闭包被高频调用”——把 make 提到外层复用(sync.Pool 或预分配字段),mallocgc 占比从 18% 掉到 4%,GC 尖峰消失,p99 抖平。火焰图钉到的不是 json 而是”闭包里的 make”。
改进:
- 卡顿查 CPU 用 on-CPU 火焰图,别用 heap pprof(那是查泄露的)。
- 读图看”尖塔”(异常突出的调用路径),别被宽底座的通用库带偏。
- 抓图时机要对(卡顿时抓),采样频率
-F 99足够且不重的。 - 定位到”谁在分配”后用 sync.Pool / 预分配 / 复用 buffer,而不是笼统”优化 json”。
🔧 思考题:on-CPU 火焰图只能看”在 CPU 上跑的”,那如果卡顿是因为”等锁/等 IO/等网络”(不在 CPU 上),火焰图会显示什么?这种卡顿该用什么图?
【参考答案】
on-CPU 火焰图只采样”正在 CPU 上运行”的栈。如果线程卡在等锁(mutex)、等 IO、等网络、等调度,它不在 CPU 上跑,on-CPU 火焰图里这些等待时间根本不出现——你看到的是”CPU 占用很低、火焰图很矮”,但请求就是慢。这种叫 off-CPU 时间(阻塞/off-CPU),on-CPU 火焰图对它是盲的。
正确工具是 off-CPU 火焰图 / 阻塞分析:
- 用
bpftrace/perf抓 off-CPU:记录线程”离开 CPU 到回到 CPU”之间在等什么(哪个锁、哪个 fd、哪个 futex)。offcputime(bcc 工具)能直接生成 off-CPU 火焰图,横轴是被阻塞的时长占比,尖塔就是”卡在最久的等待上”的路径。 - 或者 Go 自带:
pprof的block(阻塞 profile,runtime.SetBlockProfileRate开)看等锁/通道多久;mutexprofile 看互斥锁竞争;goroutineprofile 看 goroutine 卡在哪(等 channel/IO)。 - 网络等待看
net相关:连接建立、读写超时、epoll 等待,用offcputime抓或在pprof的 goroutine 栈里看。
实战分工:
- p99 抖但 CPU 占用高 → on-CPU 火焰图(找烧 CPU 的尖塔,如疯狂 malloc/序列化)。
- p99 抖但 CPU 占用低、线程像”睡着了”→ off-CPU 火焰图 / block / mutex profile(找等锁、等 IO、等网络)。
- 内存只涨不跌 → heap pprof(inuse_space)。
- 延迟全链路 → 第 11 章的”延迟账本” + 第 25 章工具箱(perf/bpftrace)。
一句话:火焰图不是万金油,先判断”卡在 CPU 上还是 CPU 外”,再选 on-CPU 还是 off-CPU。很多人拿着 on-CPU 图去查”等锁”的卡顿,当然查不出——图里压根没那段等待。