第 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_modeNUMA 节点内存紧张时是否先回收本节点
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_backlog
  • net/ipv6/、net/unix/、net/bridge/ 等

代表参数(挑几个):

参数一句话
net.ipv4.tcp_tw_reuseTIME_WAIT 端口能否复用(高并发短连接救命)
net.ipv4.tcp_max_syn_backlogSYN 队列长度(防 SYN flood / 握手丢)
net.core.somaxconnaccept 队列上限(backlog)
net.core.netdev_max_backlog软中断收包队列上限
net.ipv4.tcp_rmem/wmemTCP 收发缓冲自动调优区间
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-stateinode 使用状态(只读观测)
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.semSystem V 信号量数组(某些老数据库依赖)
kernel.panicpanic 后几秒自动重启(生产常设非零)
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 下”的东西,值得一并知道,免得你找错地方:

  1. sysfs(/sys/):设备/驱动级参数。例如 /sys/block/sda/queue/scheduler(选 IO 调度器)、/sys/class/net/eth0/queues/(网卡多队列)、/sys/kernel/mm/transparent_hugepage/enabled(大页开关)。这是”设备视角”的旋钮,和 vm 那家是互补的。
  2. cgroup(/sys/fs/cgroup/):按进程组分资源上限(cpu、memory、io、pids)。K8s 那本第 15 章讲过,这里它是”隔离级”旋钮。
  3. /proc 下的非 sysctl 文件:很多是只读观测,不是旋钮。如 /proc/meminfo、/proc/vmstat、/proc/zoneinfo、/proc/interrupts、/proc/slabinfo、/proc/PID/status。它们是”显微镜”,调参前先读它们。
  4. ulimit / RLIMIT:进程级资源上限(文件数、栈、core、地址空间),用户态 ulimit -a 看,PAM//etc/security/limits.conf 配。
  5. 内核启动参数(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/cgrouppid_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 章故事线的”目录版”,你先混个脸熟,走的時候就有锚点。

  1. 编译/链接级(第 02 章):静态链接把 200MB 依赖打进二进制,启动慢、内存占双倍——你以为的”程序慢”,其实是”加载就重”。
  2. 进程/虚拟内存级(第 03、06 章):TLB miss、缺页风暴、direct reclaim——进程”卡”可能卡在”地址翻译”和”等内存回收”,根本不在你的代码。
  3. 系统调用/内核态级(第 04 章):一次 read 的陷态 + 上下文切换 + 拷贝,高频小 IO 在这里累积成毫秒级开销。
  4. CPU 调度级(第 05 章):CFS 节流、优先级反转、NUMA 绑核不当——“算力够却跑不快”常是这里出问题。
  5. 文件系统/Page Cache 级(第 07 章):cache 命中与否差 1000 倍延迟;脏页回写拥堵让你”写”卡住。
  6. 块层/IO 栈级(第 08 章):调度器不匹配、队列浅、写放大——“盘慢”的锅常在这层,不在盘本身。
  7. 中断/软中断级(第 09 章):单核 si 100%、softnet 丢包——高 PPS 下吞吐上不去的隐形天花板。
  8. 网络栈级(第 10 章):握手队列、TIME_WAIT、重传、窗口——建连失败和 RT 高的大本营。
  9. 硬件/驱动级(贯穿):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 看 allocstallwatermark_scale_factor/min_free_kbytes
周期性卡几秒、像心跳内核暂停类(18章五兄弟)dmesg/bpftraceTHP 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/nstatrp_filter/somaxconn/tw_reuse
连接建不起来、端口不够网络 ephemeral(21)ss -tan 看 TIME_WAITip_local_port_range/tcp_tw_reuse
”内存泄漏”告警但业务正常缓存挤压/误报(22)free 看 available/PSIMemAvailable/swappiness
大内存机器莫名抖动NUMA/大页(23)numactl/numastatzone_reclaim_mode/nr_hugepages
文件句柄/进程起不来fs/kernel 配额(16)cat /proc/sys/fs/*file-max/nr_open/pid_max
容器内 OOM 但宿主很闲cgroup 边界(17/19)看 cgroup memory.statmemory.max(非 vm.*)
改了 sysctl 不生效三世界找错门(17)tuned-adm activetuned/ulimit/cgroup

1.9.2 墨叔的三条排障心法(贴显示器)

你:这张表好用,但实战里我容易”先怀疑自己最熟的”。 墨:对,这是人性——懂网络的人永远先怀疑网络(第 25 章那个被 bpftrace 翻案的”网络卡”就是例子)。给你三条反人性心法:

  1. 先证伪,再证真:别上来”肯定是网络”,先花 30 秒证伪——iostat/vmstat/sar 三把零依赖刀各扫一遍,哪个指标真的异常,就从那一级入手。第 25 章的 SOP 就是这个意思。
  2. 看”相对变化”不看”绝对值”:%util 30% 不算高,但比昨天同时段涨了 20 倍就是信号;永远和历史比,和邻居比(同机多实例、同集群多节点)。
  3. 一次只动一个变量:这是第 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 自身处理代码同城跨机房完全一样。

逐级反查(排除法)

  1. **应用层:**B 的埋点显示自身处理同城跨机房都是 3ms → 排除应用逻辑。
  2. 系统调用层:strace -e trace=network,read,write -p <B_pid> 看 recv/send 耗时 → 不多,排除 syscall 卡。
  3. **调度/CPU:**B 实例 CPU 没抢、没 throttle(cpu.stat 里 throttled_periods=0),排除。
  4. **文件系统/cache:**这是读接口,B 不落盘,排除。
  5. **块层/IO:**同上,无磁盘 IO,排除。
  6. **中断/软中断:**B 网卡 si 正常,排除。
  7. **网络层:**到这里才重点看。ss -i 看跨机房连接的 rtt(200ms 级,符合物理距离)、retrans(0.1%)、cwnd(长期很小,初始 10)。nstat 有偶发 TCPLostRetransmit。
  8. **协议栈:**重传偶发说明中间专线有抖动丢包,不是参数错配。
  9. **硬件:**专线带宽没满,排除。

结论

根因是”长肥管道 + 偶发丢包”导致拥塞窗口爬不上去,不是参数错配;但可以调 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 开)看等锁/通道多久;mutex profile 看互斥锁竞争;goroutine profile 看 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 图去查”等锁”的卡顿,当然查不出——图里压根没那段等待。