第 24 章 优化哲学——把全书的”母题”提炼成可复用的思维模型

走到第 24 章,我们已经把程序从源码拆到硬件(第 02-11 章),把 /proc/sys 各家族参数逐个钉死(第 12-17 章),又用 6 个真实事故走完了”现象→推断→可能推错→调整→再观察→改进”的完整迭代环(第 18-23 章)。 这一章不教新参数,而是把你前面 23 章里反复撞见的那些”套路”提炼成思维模型。为什么我要花一整章讲”哲学”?因为参数的答案是会过时的——新内核、新硬件、新负载,今天正确的 swappiness 取值明天可能就错。但底层的设计母题和思维方式不会过时。你记住母题,就能在任何新系统上自己推演出该调什么。 我把全书反复出现的 7 个母题 + 2 条铁律,一个一个拆开讲,每个都回扣前面的章节和事故。


一、铁律一:懂微观,才配谈优化

墨:老哥,这是全书第一句话,也是最后一章还在强调的话。我先考你:为什么”不懂微观就调参”是最危险的?

你:因为不知道锅在谁那,瞎调可能越调越乱?

墨:对,而且”越调越乱”还是轻的。最危险的是——你调对了一次,就以为自己懂了,从此无脑套用。 我给你三个层面的”不懂微观”的代价:

第一层:你连”问题属于哪一层”都分不清。 第 11 章的”卡 2 秒分段决策树”、第 18 章”所有请求同时卡”vs”单请求慢”的区分,本质都是分层定位。不懂微观的人,卡顿了就去看应用代码、去调 JVM 参数,而真正的元凶在内核的 direct reclaim 或 THP compaction(第 18 章的 8 秒卡顿)——他调应用代码,一万年也调不好,还以为是自己代码烂。

第二层:你把”相关性”当”因果性”。 第 19 章 OOM 连环杀里,DB 的慢查询日志”跟着涨”,不懂微观的人会去优化 SQL,但真相是 DB 是被连带的受害者(应用先卡 1~2 秒,DB 才跟着卡)。谁先动谁才是源头——这个判断必须建立在”请求穿越各子系统的微观时序”之上,不是看哪个指标高。

第三层:你被平均值骗,看不出分布。 这一条我们单列成母题二,因为它太常见、太致命。

你:那”懂微观”具体指懂什么?

墨:指你能在一张纸上画出”一个请求从网卡到你的 recv() 之间,经过了哪几层、每层可能发生什么、每个 sysctl 参数守在哪道门”。 第 01 章的全景地图、第 11 章的全链路时间线,就是这张图的两种画法。你脑子里有这张图,任何故障都能”按图索骥”——症状落在哪一段,就去那一段的旋钮里找。没这张图,你就是在黑暗里掷骰子。

模型一:微观地图优先于参数表。 先能在脑子里画出”请求穿越图”,再谈调哪个参数。参数表是地图的标注,不是地图本身。


二、母题一:平均值掩盖分布

墨:这是全书出现频率最高的母题,在至少 4 个现场坑过人,你一个个回忆一下:

  1. 第 20 章磁盘 I/O 毛刺:iostat 的 %util 才 30%,但 await 180ms、aqu-sz 12。%util 是”盘忙不忙的平均值”,被突发稀释了;而 aqu-sz/await 才是”有没有请求在排队等”的真相。 你被平均值骗,以为盘没瓶颈,其实请求在排队等几百毫秒。
  2. 第 11 章 load average:loadavg 是指数移动平均,它把”瞬时尖峰”平滑掉了。一个 1 秒的 200 的 load 尖峰,在 1 分钟均值里几乎看不见。所以 loadavg 高不一定是现在忙,可能是 30 秒前忙过;反之 loadavg 低,不代表此刻没卡(第 18 章的瞬时冻结,loadavg 根本来不及反应)。
  3. 平均 RT(P99 vs avg):你盯着平均 RT 8ms 觉得很健康,但 P99 是 200ms。平均值把那 1% 的倒霉请求(它们卡在排队/换页/compaction)完全稀释了。用户感知的是 P99,不是 avg。
  4. 平均带宽:链路平均利用率 20%,你以为很闲,但重传率 5% 全发生在”大包”上(第 21 章 MTU 黑洞)——平均掩盖了”特定尺寸包被丢”的分布。

你:所以”看平均”是错的?那该看什么?

墨:不是”不看平均”,是”永远用分布/分位数/极值去交叉验证平均”。 具体武器:

  • 延迟看 P99/P999,不看 avg(APM、histogram)。
  • I/O 看 aqu-sz+await,不单看 %util。
  • 系统负载看 瞬时值 + vmstat 1/sar 1 的逐秒序列,不单看 loadavg。
  • 网络看 分尺寸通断(ping -s 1472 测大包),不单看平均带宽。

模型二:任何”平均”都必须配一个”分布/极值”指标去戳穿它。 看到平均值说”没问题”时,先问:它的分布里,最坏的那 1% 长什么样?


三、母题二:缓冲区治突发,不治过载

墨:这是理解”缓存/cache/队列/缓冲”这一类机制的灵魂。我先问你一个反直觉的:page cache 是干嘛的?

你:加速文件读啊,读过的放内存下次命中。

墨:对,但这是”收益”视角的回答。从”系统设计”视角,page cache 的本质是——用一个”蓄水池”把”突发、不均匀的访问”削峰填谷,让后端(磁盘)看到的是平滑的流。 这就是”缓冲区治突发”的全部含义。

这套逻辑贯穿全书:

  • page cache(第 07 章):把突发读蓄在内存,磁盘看到的读变平滑。
  • 写回脏页(第 06/07 章):dirty_ratio 让你”瞬间写一大坨”先落在内存,后台异步慢慢刷盘,削平写的突发。
  • 网卡 ring buffer + 软中断 backlog(第 09 章):突发包先攒在缓冲区,再批量上送,避免每包一次中断。
  • cgroup io.max/cpu.max:给突发限个上限(缓冲→节流)。
  • socket 接收缓冲(第 10 章):TCP 用窗口吸收对端突发的发送。

你:那”不治过载”是什么意思?缓冲不是万能的吗?

墨:关键就在这:缓冲能吸收”突发”,但吸收不了”持续过载”。 如果下游的处理能力(磁盘吞吐、CPU、带宽)本来就小于上游的输入速率,那么缓冲只会越积越多,直到填满,然后开始丢/卡/超时。第 20 章的 IO 毛刺最后那股力量就是这个——buffer pool 只有 8G、要管 50G 热数据,物理读是”持续过载”(每秒几千次硬读盘),不是突发。任何缓冲(队列、cache)都填不满这个洞,唯一解法是扩大缓冲本身(扩 buffer pool)或降低输入(归档冷数据)。

模型三:遇到”缓冲没用”的情况,先问:这是突发还是持续过载?突发→调大缓冲/队列/缓存;持续过载→扩大处理能力或削减输入,调缓冲无效。

一个反面例子:第 18 章有人想用”定时 drop_caches 清 page cache”来”解决内存问题”,这既误解了 page cache 是好事,也没意识到如果真是”持续内存压力”(泄漏),清 cache 只是把问题延后几秒——因为根因(泄漏的进程)还在,它马上又吃满。清缓存治不了过载,只治了”缓存太多”这一个表象。


四、母题三:通用机制 + 专家逃生口

墨:Linux 内核最迷人的设计哲学,就是先给所有人一个”大致合理”的通用默认,再给专家留一个”我知道我在干什么”的逃生口。理解了这点,你就不会再骂”为什么这个参数默认这么蠢”。

全书到处是这个模式:

通用默认(给普通人)专家逃生口(给知道自己在干嘛的人)出处
overcommit_memory=0(启发式,允许略超)=1(狂开空头支票) / =2+ratio(严格)第 06/12/14/19 章
swappiness=60(积极换出)容器/DB 设 1~10(保低延迟)第 12/13/18 章
transparent_hugepage/defrag=always(自动整理)madvise/never(专家自管)第 18/23 章
cgroup v2 默认权重公平cpu.max/io.max/memory.max(专家限额)第 05/17/19 章
vm.drop_caches 默认 0(不动)写 1/2/3 手动泄洪(应急)第 12/14/22 章
调度器 CFS 公平sched_rt_runtime_us 给实时任务留 95%(安全带)第 05 章

墨:这套模式的内核智慧是——“默认”必须让 90% 的人不踩坑,所以它的取向是”保守、通用、不翻车”。而”专家逃生口”是给那 10% 知道自己在做什么的人,去压榨特定负载的最后一点性能。两者不矛盾:默认保底,逃生口拔尖。

你:那我该怎么用这个认知?

墨:第一,别怪默认”不够好”——它的好是”对所有人不过差”。第二,动逃生口之前,先确认自己是那 10%(真懂负载画像)。 第 13 章的”五套负载画像”就是在帮你判断”你是不是该动这个逃生口、动哪个”。第 19 章的反例:有人给核心服务设 oom_score_adj=-500 当免死金牌,却掩盖了真泄漏——他以为自己是专家,其实是用逃生口逃避定位真凶。

模型四:每个”默认”都配一个”逃生口”。默认保底,逃生口拔尖。动逃生口前,先确认你真的懂自己的负载——否则逃生口会变成”埋雷口”。


五、母题四:规模逼出结构

墨:这是理解”为什么内核要这么复杂”的总钥匙。我讲个贯穿全书的规律:小规模下简单的方案,到了大规模会因为某个瓶颈而被迫演化出复杂结构。 你看到的所有”看起来过度设计”的机制,都是被规模逼出来的。

例子一个一个来:

  • 单文件 → slab/slub(第 06 章):内核要分配几亿个”同样大小的小对象”(inode、task_struct),若每次都找 buddy 要页,碎片和锁竞争爆炸。slab 用”对象缓存+每 CPU 池”解决——是被”海量小对象”逼出来的结构。
  • 单队列 → blk-mq(第 08 章):老块层是单个请求队列 + 一把大锁,多核高 IOPS 下锁成瓶颈。blk-mq 改成每硬件队列 + 每 CPU 软件队列的多级结构,是为”多核 + NVMe 百万 IOPS”逼出来的。
  • 每包一中断 → NAPI(第 09 章):千兆以下每包中断还行;到了 10G/25G,每秒百万包,中断能把 CPU 打死。NAPI 改成”中断触发一次,然后轮询批量收”——是被”高 PPS”逼出来的 upper/lower half 演化。
  • 单 CPU 调度 → CFS 红黑树 + 每 CPU 运行队列(第 05 章):早期 O(n) 遍历所有任务找下一个跑谁,任务多了就崩。CFS 用红黑树按 vruntime 排、每 CPU 一个队列减少锁——被”多核多任务规模”逼出来。
  • 单流 TCP → 拥塞控制 + 窗口缩放(第 10 章):早期 TCP 窗口只有 64KB,在”高带宽高延迟”(长肥管道)下喂不饱链路。窗口缩放 + BBR 是被”网络规模(带宽×延迟)“逼出来的。

你:这跟调参有什么关系?

墨:关系大了。当你在一个”大规模”场景抄”小规模”的配置,就会翻车。 第 09 章的 32 核丢包事故:有人把”老机器单队列”的配置(ethtool -L combined 1)抄到 32 核新机器,用小规模的结构去扛大规模的 PPS,直接被打死。第 08 章”换 NVMe 没变快”:用”盘慢”的小规模经验去解释”缓存没盖住工作集”的大规模问题,药不对症。

模型五:评估任何配置前,先问”它服务的是哪个规模”?把小规模配置套到大规模场景,几乎必翻车。 看到复杂结构,先假设”它是被某个规模瓶颈逼出来的”,再理解它守的是哪个瓶颈。


六、母题五:成本假设反转,优化变负担

墨:这是全书最”憋屈”也最值得记住的母题。很多参数,在它被设计时的成本假设下是优化,但当硬件成本假设变了,它反而成了负担。调优帖的坑,十有八九是把”昨天的优化”无脑用到”今天的硬件”上。

铁证如山的几个:

  • swappiness=0(或很高):当年设高 swappiness 是为了”腾 page cache 给文件读”——那时内存小、文件读是主角。今天大内存 + 容器,设 0 反而让 page cache 被挤、direct reclaim 抖(第 12/18 章)。“积极换出”的成本假设(内存贵)反转了。
  • zone_reclaim_mode=1:当年为保 NUMA 本地性(跨 node 访问慢)。但本地回收的 direct reclaim 卡顿代价,在现代大内存 + 互连带宽充裕下,比”偶尔跨 node 慢一点”更伤(第 23 章)。成本假设(跨 node 极慢)反转了。
  • transparent_hugepage/defrag=always:当年为”自动吃大页 TLB 收益”。但在延迟敏感负载上,同步 compaction 冻进程的代价,远大于那点 TLB 收益(第 18/23 章)。成本假设(compaction 便宜)反转了。
  • tcp_tw_recycle:当年为”快速回收 TIME_WAIT 省端口”。但 NAT 普及后,它用时间戳 PAWS 把 NAT 后合法包当过期丢——网络拓扑假设(NAT 少)反转了,于是它在 4.12 被删(第 21 章)。

你:那我怎么避免踩这种”昨天的优化”?

墨:第一,任何调优帖都看它的”年份和硬件背景”,别直接抄数值。第二,调参时想清楚”这个参数赌的是哪个成本假设”,当你的环境那个假设还成立吗? 第 13 章的”负载画像”本质就是在帮你确认”成本假设是否匹配”。

模型六:参数赌的是”某个成本假设”。当硬件/拓扑/负载变了,昨天的优化变今天的负担。抄数值前,先问”它赌的假设在我这还成立吗?”


七、母题六:诊断铁律——先定位,再调参

墨:这条和”铁律一”是双胞胎,但更具体,是你在每个事故里都在用的动作规范。我把第 18-23 章所有事故抽象成一句话:

先有观测证据,再下结论;每推断一步,都用”能证伪它的观测”去验证;推错了就回头,别硬杠。

你回忆第 18 章:我们先排除了 GC、网络、DB(每个都用”时间差/受害者同步”证伪),才把嫌疑收敛到”内核暂停类”;然后用 vmstat 对齐尖峰,看到 si/so 和 compact_stall 两个铁证,才动手。没有一步是”我觉得应该是 X 所以调 X”。

第 21 章的反例更是经典:一群人调了一周 tcp_sack/窗口(拥塞参数),因为跳过了”测不同尺寸包通断”这一步,没发现是 MTU 黑洞。他们”先调参,后定位”,顺序反了,所以白费一周。

墨:给你一套动作纪律,贴在显示器上:

  1. 记录现象,不解释(第 0 层):先把”什么时候、什么指标、多大量”记下来,别上来就编故事。
  2. 提出假设,同时想”什么观测能证伪它”:每个”我猜是 X”,必须配一个”如果 X 真,那 Y 应该成立;我去查 Y”。
  3. 优先排除”最可能且最便宜”的假设:先查软(缓存/配置),后查硬(换盘/换硬件)。
  4. 一次只动一个变量:改完观察,别同时改五个参数然后不知道哪个生效。
  5. 推错是正常的:证伪了就回头,诊断环的价值就在”能回头”。

模型七:诊断 = 假设 + 可证伪的观测 + 单变量实验。先定位再调参,顺序永远不能反。


八、benchmark 的七大陷阱

墨:最后讲一个”优化之后怎么验证”的坑。很多人调完参,跑个 ab/wrk 说”QPS 翻倍了”,结果上线没用——因为他的 benchmark 测的根本不是生产的样子。这是”懂微观才配谈优化”在验证环节的延伸。

陷阱 1:冷缓存 vs 热缓存。 你重启服务后立刻压测,page cache 是空的,所有读落盘,测的是”冷”性能;生产跑了一天,cache 是热的,测的是”热”性能。两者能差 10 倍(第 07 章)。要么都冷、要么都热,别混着比。 真要比”调参前后”,必须同一缓存状态。

陷阱 2:单连接 vs 生产并发。 wrk -c 1 测的是单连接延迟,生产是几千并发。-c 必须贴近生产的并发模型(短连接/长连接、连接池与否)。

陷阱 3:客户端瓶颈。 压测机本身 CPU/网络打满了,你测的是”压测机能发多少”不是”服务端能收多少”。压测机要比服务端强,或者用分布式压测。

陷阱 4:忽略尾延迟。 你看 avg QPS 涨了,但 P99 高了——用户感知的是 P99。第 11/20 章强调过。

陷阱 5:暖机(warm-up)不足。 JIT、JIT 编译、内核热路径、CPU 调频还没稳定就统计,数字忽高忽低。先跑 30 秒暖机再统计。

陷阱 6:被测系统被 benchmark 本身改变。 你压测时疯狂写日志/打 metrics,这些 I/O 本身成了瓶颈,测的是”日志系统”不是业务。

陷阱 7:只看吞吐不看资源。 QPS 翻倍但 CPU 也翻倍、延迟方差变大,这”优化”不划算。要同时看 QPS、P99、CPU、I/O、内存。

你:所以正确的 benchmark 姿势是?

墨:贴近生产画像 + 控制变量 + 看分布不看平均 + 资源一起看 + 暖机 + 冷热一致。 而且——最好的 benchmark 是生产本身的灰度对比(同一份流量,一部分走新配置一部分走旧),比任何实验室压测都准。

模型八:benchmark 测的是”你的测试方法”,不是”生产的样子”,除非你刻意让它像。 验证优化的唯一金标准是生产灰度,实验室压测只是”排除明显退化的快速检查”。


九、参数不是开关,是权衡(总账)

墨:把全书所有参数收个总账。我反复说”内核参数是权衡不是开关”,这一节给你一张”权衡总表”,把每个参数”拿什么换什么”钉死——这是你调参时真正的决策依据。

参数调大换来代价(放弃的)典型场景
vm.swappiness积极换出,腾 page cache匿名页进 swap 后访问变慢容器/DB 设 1~10,通用 60
vm.min_free_kbytes防 direct reclaim、保原子分配可用内存变少大内存/网卡 DMA 敏感
vm.dirty_ratio限制脏页峰值,宕机丢的少前台 sync 卡顿风险写密集设小,批量写设大
vm.watermark_scale_factor大内存更平滑回收略多占内存256G+ 调 100~1000
vm.zone_reclaim_modeNUMA 本地性本地回收卡顿(常划不来)保持 0
vm.drop_caches应急清缓存自残 I/O(若滥用)仅测试/手动
net.ipv4.tcp_tw_reuse客户端复用 TIME_WAIT仅 PAWS 安全高并发短连接
net.ipv4.tcp_mtu_probing绕过 MTU 黑洞略增探测开销禁 ICMP 环境
fs.file-max/nr_open支撑更多 fd极少量内存高并发服务
kernel.pid_max/threads-max支撑更多任务极少量内存容器密度高
transparent_hugepage/defrag大页覆盖高同步 compaction 冻进程设 madvise/never
queue/scheduler(none)SSD/NVMe 低延迟不做合并(SSD 无妨)云盘改 none
queue/nr_requests缓突发排队略多内存云盘调大
oom_score_adj(负值)核心服务免死真泄漏时它苟活拖垮全机配合 PDB/限流

墨:看这张表,你会发现没有一个参数是”调大永远好”或”调小永远好”。每个都是”用 A 换 B”,而 A 和 B 哪个重要,取决于你的负载画像(第 13 章)。这就是为什么我从不给你”一套通用最优值”——给了就是害你。

模型九:调参 = 在你的负载画像下,为每个参数选”换什么、放弃什么”。没有全局最优,只有”匹配你的负载”。


十、🔧 思考题(都配参考答案)

思考题 1(基础): 全书反复出现”平均值掩盖分布”。请举出至少 3 个你印象最深的”被平均值骗”的现场,并说明每个该用什么指标戳穿它。

【参考答案】

3 个现场:

  1. 磁盘 I/O(第 20 章): %util 才 30% 但 await 180ms、aqu-sz 12。被 %util(盘忙的平均值,被突发稀释)骗,以为盘没瓶颈;实际请求在排队等几百毫秒。戳穿:aqu-sz+await,不是 %util。
  2. 系统负载(第 11/18 章): loadavg 是 1/5/15 分钟指数移动平均,瞬时 1 秒的冻结(THP compaction / direct reclaim)在 1 分钟均值里几乎看不见;反之 loadavg 高可能只是 30 秒前忙。戳穿:vmstat 1/sar 1 逐秒序列 + dmesg lockup。
  3. 延迟(第 11/20 章): avg RT 8ms 但 P99 200ms,平均值把那 1% 卡在排队/换页的请求稀释了,用户感知的是 P99。戳穿:看 P99/P999 分位数,不看 avg。 补充第 4 个:网络 MTU 黑洞(第 21 章)——平均带宽 20% 看着闲,但重传全发生在”大包”上,小包(ICMP)通。戳穿:分尺寸测(ping -s 1472)。 模型:任何平均都必须配分布/极值/分位数去交叉验证。

思考题 2(深入): “缓冲区治突发,不治过载”(母题二)。请用一个你亲历或书中学的现场,说明如何区分”该调大缓冲”和”该扩大处理能力”,并指出调错会怎样。

【参考答案】

区分:看”下游处理能力 vs 上游输入速率”的关系。

  • 突发:输入速率平时远低于处理能力,只是偶尔尖峰超过。→ 调大缓冲/队列/缓存能吸收,正确。例:网卡 ring buffer(第 09 章)吸收瞬时报潮;page cache(第 07 章)吸收突发读。
  • 持续过载:输入速率持续 > 处理能力。→ 缓冲只会越积越多直到填满然后丢/卡/超时,调大缓冲无效,必须扩大处理能力或削减输入。

书中学的现场:第 20 章 buffer pool 8G 管 50G 热数据——每秒几千次物理读落盘是”持续过载”(不是突发),任何队列/cache 都填不满这洞。正确解法是扩 buffer pool 到 32G(扩大缓冲本身=提高命中=降低硬读)或归档冷数据(削减输入)。调错:若误判为”盘慢”去换 NVMe,每次物理读从 0.5ms 降到 0.1ms,但物理读总量不变,尖峰形态还在——白换。

调错的代价:把”持续过载”当”突发”去调大缓冲,只会让请求在更大的缓冲里”排队更久才失败”,延迟更高、定位更晚。反之把”突发”当”过载”去换硬件,浪费钱还治不了本。

思考题 3(权衡): “成本假设反转,优化变负担”(母题五)。除书中已列的 swappiness/zone_reclaim/defrag/tw_recycle,请再举一例你学过的、或能推断的”昨天的优化今天变负担”的参数场景。

【参考答案】

补充举例(基于全书机制可推断):

  1. net.ipv4.tcp_timestamps=0(为防边信道攻击而关):当年关它是安全考量;但关了_timestamps 后,tcp_tw_reuse 的 PAWS 保护失效(第 21 章依赖时间戳判断复用安全)、且 RTT 测量变粗。在现代”短连接高并发”场景,关 timestamps 反而让 TIME_WAIT 复用不安全、连接效率降。成本假设(边信道风险高)在内部网络下常不成立,于是很多场景又开回 1。
  2. vm.vfs_cache_pressure 设很高(>100):当年为”省内存”积极回收 dentry/inode 缓存;但今天大内存机器上,设太高导致频繁 ls/路径解析变慢(缓存被回收),而内存本来就不紧,这个”省内存”的优化无意义甚至有害。
  3. io 调度器 cfq(已淘汰):当年为桌面公平调度设计,在服务器 SSD 上反而引入不必要排队;内核已默认弃用,改用 mq-deadline/none。 核心判断法:该参数赌”内存贵/跨 node 极慢/compaction 便宜/NAT 少”等假设;当假设因硬件/拓扑进步而不成立,昨天的优化变今天负担。抄数值前先问”它赌的假设在我这成立吗”。

思考题 4(进阶总账): 假设你是 SRE 负责人,要给团队定一条”调参规范”。请基于本章 7 母题 + 2 铁律,写 5 条可落地的团队规则(每条对应一个母题/铁律,并说明它防的是什么坑)。

【参考答案】

5 条团队调参规范(对应母题/铁律):

  1. “任何调参前,先在脑图里画出请求穿越路径”(铁律一/模型一)——防”不懂微观瞎调、调错层”。
  2. “上报/告警必须用分位数(P99/P999)、I/O 用 aqu-sz+await、负载用逐秒序列,禁止只看平均”(母题一/模型二)——防”平均值掩盖分布”导致的假告警和误判。
  3. “改任何参数,必须先写明’它赌的成本假设在我这是否成立’,否则不准上”(母题五/模型六)——防”无脑抄老旧调优帖数值”(swappiness=0/zone_reclaim=1/defrag=always 之流)。
  4. “生产变更遵循:记录现象→假设+可证伪观测→单变量实验→观察→回头;禁止先调参后定位”(母题六/模型七)——防”第 21 章调一周 tcp_sack 白费”的顺序反转。
  5. “验证优化必须生产灰度对比,实验室压测仅作排除明显退化;压测须冷热一致、并发贴近生产、看 P99 与资源”(模型八)——防”benchmark 测的是测试方法不是生产”。 附则可加:“逃生口(oom_score_adj/overcommit=1/cgroup 限额)只在确认懂负载画像后用,且不得用来掩盖真凶(泄漏/水位顶高)“(母题三/模型四)。


实战补遗一:两次优化,一次封神一次翻车

墨:第 24 章把”优化哲学”讲了一堆道理。这一节给你两个真实对照案例——同是”动手调参”,一个封神一个翻车,差距全在”懂不懂机制”。把它当第 24 章的”活教材”。

补遗.1 封神案例:一眼看穿 direct reclaim,把 P99 砍到十分之一

📦 某交易系统,P99 从 8ms 周期性飙到 200ms+,DBA 折腾了两周索引、连接池、SQL 都没用。

过程(懂机制的人怎么推):

  • 第一刀 vmstat 1:在尖峰时 allocstall(direct reclaim 次数)同步暴涨——不是 SQL,是分配时同步回收卡线程(第 06/12 章)。
  • 第二刀看机器:128G 内存,watermark_scale_factor=10(默认 0.1%)→ 缓冲带才 128MB;业务有周期性大分配,free 一跌破 low 就 direct reclaim(第 14 章第七个实战同源)。
  • 第三刀:临时 sysctl -w vm.watermark_scale_factor=500(128G 上变 640MB 缓冲带),只动这一个变量,观察:P99 尖峰消失,allocstall 归零。
  • 收口:白天按第 13 章画像 C(低延迟)固化 watermark_scale_factor=1000 + min_free_kbytes 留缓冲 + buffer pool 留余量。

为什么封神: 他没有”抄一堆优化帖”,而是用机制推演锁定 direct reclaim → 用单一变量验证 → 用画像固化。两周的 DBA 乱拳,他一个 vmstat 加一个参数解决。差距不在”懂多少参数”,在”懂参数背后的机制链”。

补遗.2 翻车案例:为”快速失败”设 panic_on_oom=1,把单机故障升级成整节点事故

📦 某团队学了"fail fast 哲学",给一台多实例节点设了 vm.panic_on_oom=1,理由是"OOM 就赶紧重启,别磨叽"。

过程(不懂机制的人怎么翻):

  • 某天一个实例内存泄漏,本该被 OOM Killer 杀掉、其余实例无感(第 19 章)。
  • 但 panic_on_oom=1 让内核不杀进程、直接 panic 整机(第 12 章分类③、第 14 章第六个实战)。
  • 结果:一个实例泄漏 → 整台机器重启 → 上面 20 个实例全宕 → 业务大面积中断。
  • “快速失败”变成了”失败得惊天动地”。他以为自己在学 SRE 最佳实践,实际踩了第 14 章反复警告的”把局部问题升级成全局”的坑。

为什么翻车: 他把”fail fast”这个应用层/无状态服务的哲学,错误地套到了”整机 OOM”这个内核级最后防线上。fail fast 的前提是”挂了能立刻被拉起且不影响别人”;而 panic_on_oom=1 恰恰消灭了”只杀一个、别人无感”的可能性(第 19 章),把单点故障炸成全节点。他缺的不是”好学”,是”没理解这个参数改的是哪段机制、边界在哪”。

补遗.3 把两个案例焊成一张对照表

维度封神案例翻车案例
动手前先用 vmstat 看证据,锁定 direct reclaim先套”哲学”,设 panic_on_oom=1
对机制的理解懂 watermark/direct reclaim 的因果链不懂 panic_on_oom 是”杀进程→变重启”
改动方式单一变量,可验证可回滚一次性全局开关,影响所有进程
结果P99 降到 1/10,根因清晰单机故障→全节点宕机
本质”推”出来的优化”抄”出来的优化

墨:这两个案例,一个人省了团队两周,一个人制造了一次 P1 事故——起点都是”想优化”,差距全在”动手前脑子里有没有机制链”。这也是为什么第 24 章说”优化是科学不是玄学”:科学就是”先有可证伪的假设(机制推演),再动手验证”。你合上这章去搞优化时,记住补遗.3 这张表——问自己一句”我动这个,是推出来的还是抄出来的?” 这一句,值钱。


实战补遗二:“先量化再优化”——把火焰图当摆设、凭感觉调了一周的反面教材

墨:你接到”接口慢”的活,二话不说开始调 swappiness、调 somaxconn、调线程池——一周后没快。我说:你跳过了哪一步?

你:参数都试了啊,还能调啥?

墨:你没量化。你连”慢在哪一级、是 CPU 还是 IO 还是锁”都不知道,就动手调内核参数,等于闭眼打靶。第 24 章讲”优化哲学”,第一条就是:先有 baseline,再谈优化。

现象:一周调参,RT 纹丝不动

某接口 P99 800ms,运维按”经验”调了一圈:swappiness 0→1、somaxconn 128→65535、net.ipv4.tcp_* 几个、Go GOMAXPROCS 调大。复测 P99 还是 800ms,毫无变化。

推断(可能推错):先以为”调得还不够狠”

第一推断:「参数没调到极致。」——继续加大 somaxconn、加 tcp_mem,还是没变。推错,因为方向就错了:问题根本不在这些参数守的层。

真相:没量化 = 在错的那级瞎忙

回过头用第 25 章工具箱量化:

  • top -H 看 CPU:单核 100%,其他闲 → 卡在 CPU 单核;
  • perf top 热点:runtime.findrunnable + 业务 sync.Mutex 锁竞争占 70% → 是锁竞争,不是内存/网络;
  • 火焰图最宽栈:一个全局 map 的 RWMutex 写锁被高频抢。

根因是应用层锁竞争,和 swappiness/somaxconn/tcp_* 完全无关。调那些当然没用——它们在第 5/15 章守的层,根本不是瓶颈所在。

调整:量化先行,再动手

  • 第一步:拿 baseline(P99、各层指标:CPU/vmstat/iostat/nstat)确认瓶颈在哪级;
  • 第二步:锁竞争 → 改代码(缩小锁粒度/用 sync.Map/分片),不是调内核;
  • 第三步:改完复测,和 baseline 比,确认 P99 真降;
  • 铁律:优化的每个动作都要有”前后对比”佐证,否则你不知道是它生效还是别的变量在变。

再观察

砍掉全局锁(改分片 map)后,P99 从 800ms 降到 60ms,CPU 单核 100% 消失。之前调的一周内核参数全部回滚(本来就不该动)。

改进(权衡:拿什么换什么)

动作拿什么换什么
量化先行多花半小时抓基线不瞎调、不返工
改代码非调参改动大真解决瓶颈

🔧 思考题:为什么”局部最优”常导致”全局最差”?举一例说明你调参反而拖垮集群。

【参考答案】

局部最优=单实例某个指标漂亮,但牺牲了集群其他维度。例:给某实例 cpu.max 设很大(让它跑满不吃节流),单实例吞吐升,但同机邻居被它抢光 CPU,邻居 P99 爆→集群整体 SLA 破。又如:为降自己延迟把 swappiness=0,自己不 swap,但整机内存压力转嫁到别人,OOM 时别人先死。所以优化要看”对整机的外部性”,用 cpu.weight(相对)而非 cpu.max(绝对独占)、用 cgroup 隔离而非实例内裸调——第 17/24 章反复强调:参数是全局资源在调,不是单实例私产。你以为在优化自己,其实在偷邻座的 CPU/内存,集群视角是净亏。


实战补遗三:可观测性优先于调优——一套”优化前先看灯”的文化

墨:第 24 章讲优化哲学。我把最重要的一条单独拎出来:可观测性不是优化的”事后补充”,而是优化的”前提条件”。没有度量,调参就是盲调。讲个团队级的故事。

那年我们接手一个”老系统”,前任留了一堆”神秘 sysctl”:vm.swappiness=0、net.core.somaxconn=65535、还有个定时 drop_caches。没人知道为啥设,只知道”不设就出问题”(传说)。新员工想”优化”,把这些参数又改了一轮,结果线上抖动。

你:这不就是”祖传参数”吗?没人敢动,因为不知道当初为啥设。

墨:对,这是最典型的”不可观测导致的技术债”。这些参数当初可能是为某次具体事故设的(比如 somaxconn 是因为某次 SYN 队列溢出),但没人记录”当时什么问题、怎么验证的、现在还成立吗”。参数变成了迷信,而非”有依据的杠杆”。

我们做的第一件事不是”调参”,是把灯装上:

  1. 把 MemAvailable、SUnreclaim、tcp 重传统计、soft lockup/RCU stall、cgroup memory.events 这几条做成 Grafana 看板 + 告警(第 1、22、18、19 章都提过);
  2. 把那堆”祖传 sysctl”逐个回退到默认,同时盯看板——结果:回退 swappiness=0→默认 60、drop_caches cron 删掉后,看板毫无异常(说明当初的”不设就出问题”早被别的修复解决了,参数纯属历史遗留);
  3. 唯一回退后看板报警的是 somaxconn 改回默认 128——果然连接数高时 accept 队列溢出(netstat -s 的 overflow 涨),证明这条参数至今有效,保留。

推断:没有看板,我们永远不知道哪些参数是”真有效”、哪些是”迷信”。装了灯,回退+观测,一晚上理清了全部祖传参数,删掉 2 条无效的历史债,确认 1 条仍有效。这比”继续盲调”有价值一万倍。

你:所以”优化”的第一步是”先证明现在有问题”?

墨:没错,而且更激进——先证明”你打算改的参数,改了能解决一个你能量化的现象”。我们内部排障 SOP 就一句话:“你说要调 X,先给我看 Y 指标证明 X 现在不对。” 拿不出指标,就别动。这句话挡掉了 90% 的”凭感觉调参”。

具体落地成三条文化规则:

  1. 每个参数改动 = 一条 Issue + 一个指标:改 somaxconn 前,Issue 写”accept 队列溢出导致超时”,指标是 netstat -s 的 overflow 计数;改完验证计数归零。没有 Issue 和指标的改动,Code Review 直接打回。
  2. 默认值优先,除非有证据:新服务上线,sysctl 全默认,跑一周看板,只有”某指标持续报警”才针对性调——而不是”照帖子预设一堆”。
  3. 参数即文档:每个非默认 sysctl 写进 sysctl.d/ 文件,文件头注释”为何设、证据在哪、谁加的”。杜绝祖传谜参数。

这三条把”优化”从”玄学”变成”工程”:可观测(看板)→ 定位现象(指标)→ 反推根因(机制)→ 针对性调参(杠杆)→ 验证(指标回归),闭环。全书 26 章的每一个”实战补遗”,走的都是这个闭环——你现在该明白,为什么我每节都逼你”先测量、再下结论”了。

🔧 思考题:如果团队里有人坚持”我就想按帖子设 swappiness=0,反正也没坏处”,你怎么用这一节说服他?

【参考答案】

用”举证责任 + 可观测性”反将:

  1. 问证据:“设 0 解决过你遇到的哪个具体现象?有指标证明吗?” 多数情况是”别人都这么设”——那就回到本节的”祖传迷信”问题,没有证据的参数不该进生产。
  2. 指出隐性风险:第 0 章的翻车——swappiness=0 在新内核语义下会”宁可回收 page cache 也不 swap”,内存紧时反而 OOM。这不是”没坏处”,是有已知雷区,只是他没踩过。
  3. 给可观测的替代:“你担心 swap 慢?那我们装看板盯 vmstat 的 si/so 和 MemAvailable,真出现 swap 抖动再用 swappiness=1(留兜底)精准处理,而不是无脑设 0。” 把”盲设”变成”有证据再设”。
  4. 制度化:把”非默认参数必须有 Issue+指标+注释”写进团队规范,他的改动若拿不出证据,Code Review 挡下——不是不让他设,是让他证明设得对。

核心:不是反对设 0,是反对”无证据设”。可观测性给了”证明”的能力,也给了”不设也安心”的底气。这节的本质是用工程流程替代个人经验迷信,对 5 年+ 想”精通”的你,这比记参数值重要——参数会过时,方法论不过时。



实战补遗四:先测量后优化——perf stat 五个关键计数器别只看 CPI

墨:老哥,你说”先测量”,那测量到底看什么?跑个 perf stat 出来一堆数,你盯哪个?

你:看 CPU 利用率?或者 IPC?

墨:CPU 利用率是最水的指标之一。真正定位”瓶颈在 CPU/内存/IO 哪”的是几个硬件计数器。这节给你 perf stat 的”五个数读法”,和第 24 章”优化哲学”的”测量优先”落地。

现象:一个服务”慢”,运维看 top CPU 才 30%,说”CPU 够啊,不是计算瓶颈”,于是在应用层疯狂加缓存。结果更慢。其实瓶颈是内存带宽/缓存未命中,CPU 30% 是因为它在等内存,根本没在算。

推断:CPU 利用率低 = 不卡 CPU。

可能推错:top 的 %CPU 是”在不在 CPU 上跑”,不包含”在 CPU 上但等内存/等缓存”的时间。CPU 30% 可能意味着它在 30% 时间烧计算、剩下 70% 在 stall(等 L1/L2/L3 未命中、等内存)。所以”CPU 低”反而常常是”访存瓶颈”的信号——芯片在等数据,没法算。

调整:perf stat -e 看几个关键计数器(用 perf stat -d 自动给一组):

  1. IPC / CPI(每指令周期数 / 每指令周期):instructions / cycles。IPC 低(如 <1,x86 正常 1~4)说明”每周期干的活少”= 大量 stall。但 IPC 低要进一步区分是”等内存(cache miss)“还是”等分支预测”还是”等 IO”。
  2. cache-misses / cache-references:L1/L2/L3 未命中率。miss 高 = 数据局部性差,热数据没在缓存里。
  3. LLC-misses(末级缓存未命中):这个才是”真去内存取”的信号,对应内存带宽瓶颈。
  4. branch-misses / branches:分支预测失败率。高 = 控制流乱(如大量 if-else 热路径、虚函数)。
  5. stalled-cycles-frontend / backend:前端(取指/解码)还是后端(执行单元等数据)停滞。backend stalled 高 = 等内存/执行资源。

再观察:那次服务 IPC 只有 0.8、LLC-misses 暴涨,说明”计算量不大但在疯狂等内存”——根因是数据结构缓存行伪共享(false sharing):多线程各改一个 struct 里不同字段,但这些字段在同一缓存行(64 字节),一核改 → 整行在别的核失效(见第 3 章 TLB 实战补遗的缓存行概念),核间疯狂来回无效化。改成”每字段独立缓存行对齐(cacheline-aligned)“后,LLC-misses 掉 70%,吞吐翻倍,CPU 利用率反而升(因为不再等内存、真在算)。

改进:

  • “慢”先看 perf stat 的 IPC + cache/LLC-misses,别只看 %CPU。
  • %CPU 低 + IPC 低 + LLC-misses 高 = 访存/缓存瓶颈(伪共享、数据局部性差),不是计算瓶颈。
  • 优化方向:改数据结构布局(缓存行对齐、结构体字段按冷热分离)、提升局部性,而非加 CPU 缓存(应用层)。
  • 配合第 25 章工具箱:on-CPU 火焰图看”烧 CPU 在哪”、perf stat 看”瓶颈类型”、off-CPU 看”等什么”。

🔧 思考题:为什么”CPU 利用率低”反而可能说明”访存是瓶颈”?IPC 低就一定是内存问题吗,还可能是什么?

【参考答案】

CPU 利用率(%CPU)度量的是”任务是否在 CPU 上处于运行态(R 状态)“。但现代 CPU 在”运行态”里大部分时间可能在停顿(stall)——等指令/数据从缓存或内存回来、等分支预测、等执行单元。这段”在 CPU 上但没推进有效计算”的时间仍计入 %CPU(因为任务没睡眠、没让出 CPU,只是 CPU 内部在等)。所以当瓶颈是”内存太慢、数据供不上”时,CPU 被卡在后端 stall,看起来 %CPU 可能不高(没干多少有效活),但任务也没睡觉——表现就是”CPU 利用率低、但程序就是慢”。所以”CPU 低 = 不卡 CPU”是错的:CPU 低恰恰常意味着”卡在内存/缓存”(芯片在等数据,没法算)。

IPC 低 ≠ 一定是内存问题。IPC(每周期指令数)低表示”每周期有效指令少”,stall 多,但 stall 来源有多种,靠 perf 细分的计数器区分:

  • 后端 stall(stalled-cycles-backend)高 + LLC-misses 高 = 等内存/缓存(数据供不上)→ 访存瓶颈。
  • 前端 stall(stalled-cycles-frontend)高 + branch-misses 高 = 取指/解码受阻、分支预测失败多 → 控制流乱(虚函数、复杂 if 热路径、指令缓存未命中)。
  • 两者都不高但 IPC 仍低 = 可能是”指令本身重”(除法、加密、原子操作串行化)或”ILP 低”(指令间依赖链长,没法并行)。
  • 还有等 IO/锁根本不反映在 perf 的 CPU 计数器里(任务 sleep 了,%CPU 也低),要用 off-CPU 火焰图看(见第 1 章火焰图实战补遗)。

所以 perf stat 的正确读法:先看 IPC 粗判”是否有效推进”;IPC 低 → 拆 cache-misses/LLC-misses 看是不是内存;拆 branch-misses 看是不是控制流;都正常还慢 → 去 off-CPU 看等锁/IO。%CPU 几乎是最没信息量的那个数字(它连”在跑还是在等”都分不清),优化别拿它当依据。这跟第 24 章”优化哲学”一致:测量要测”瓶颈类型”(CPU/访存/IO/锁),不是测”忙不忙”;先定性再定量,才不会被 %CPU 这种表面指标带偏。

延伸:缓存行伪共享是后端 stall 的典型代码层成因——多核各改同一缓存行的不同字段,MESI 协议让该行在各核间反复失效(Invalidation 风暴),每核改完别人缓存行失效、重读内存,LLC-misses 暴涨、IPC 暴跌。解法 struct{ _ [64]byte pad } 或 Go 的 aligned 字段隔离,让每个热字段独占缓存行。这是”测量(perf 看到 LLC-miss 高)指引方向、再改数据结构”的标准优化闭环。

下一章预告

墨:第 24 章我们把全书的 7 个母题 + 2 条铁律提炼成了可复用的思维模型——微观地图优先、平均值掩盖分布、缓冲治突发不治过载、通用机制+专家逃生口、规模逼出结构、成本假设反转、先定位再调参,外加 benchmark 八大陷阱。这些不会过时,比任何参数数值都值得你记住。

下一章(第 25 章)我们上工具箱——把前面 23 章里零碎出现的 perf/ftrace/bpftrace/vmstat/iostat/sar/ss/nstat/numastat 等工具,系统地讲清楚:每个工具解决哪一类问题、看什么输出、什么时候该上它、典型命令怎么敲。这一章是你”下次真遇到故障,手里该抡哪把扳手”的速查手册。


本章完。工具箱见第 25 章。