第 00 章 序:只有懂微观,才配谈”优化”

墨:老哥,先别急着翻 /proc/sys/vm。我问你个事——你有没有过这种经历:线上某个服务莫名卡,你 Google 一圈,抄了条 sysctl -w vm.swappiness=10,或者 net.ipv4.tcp_tw_reuse=1,重启,好像好点,又好像没好,最后你也不知道到底是谁管用。

你:……这不就是我日常吗。抄参数像拜菩萨。

墨:对,这就是”参数民科”——把 sysctl 当许愿池。今天这本书要治的,就是这种病。我要先跟你掰扯清楚一个核心命题:只有懂微观,才配谈”优化”。但这句话得翻译成人话,不然又成一句正确的废话。


0.1 “懂微观”到底懂的是什么

“微观”不是让你去背 /proc/sys/vm 下 40 个文件名的默认值。那是另一种民科。微观是一条因果链:

你敲的命令
  → 程序怎么被编译、加载、变成进程
    → 进程怎么向内核要内存、要 CPU、要 IO
      → 内核的每个子系统(MM、调度、VFS、块层、网络、中断)怎么接力
        → 驱动怎么把请求变成电信号、DMA 怎么搬数据
          → 硬件(MMU、cache、DRAM、磁盘、网卡)怎么真正干活
            → 反过来,延迟/吞吐/抖动是从哪一级产生的

墨:你品品——为什么”懂微观”和优化有关系?

你:因为如果不知道延迟卡在哪一环,你调参就是盲调。比如服务 RT 高,你怀疑网络,调了 tcp 参数,其实瓶颈在 page cache 被挤、缺页风暴。你调一万条网络参数也没用,因为根因在内存子系统。

墨:一针见血。优化的第一步永远是”定位”,而定位的前提是你知道链路长什么样。你脑子里没有那条因果链,现象到你这儿就是一堆孤立的数字(RT 高了、CPU 满了、IOwait 高了),你只能瞎蒙。懂微观,等于脑子里装了一张”地图”——现象一出现,你能立刻圈出”大概在地图的哪一块”,而不是全场乱找。

⚠️ 坑(本书最重要的一句话):绝大多数”调优”失败,不是因为不知道某个参数,而是因为没定位就对参数下手。先观测、画假设、再验证,顺序不能反。本书所有案例都严格遵守这个顺序,哪怕中途推错。


0.2 诊断循环:现象→推断→试错→观察→改进

这是全书的灵魂方法,比任何单个参数都值钱。我把它画成一个环:

   ① 现象(RT 高 / 抖动 / OOM / 重传)
        │
        ▼
   ② 推断(基于微观地图,提出"可能是 X 子系统"的假设)
        │   ←── 这一步最容易错,因为现象会骗人
        ▼
   ③ 试错(改一个变量:调一个参数 / 加一条规则 / 改代码)
        │
        ▼
   ④ 观察(用 vmstat/iostat/perf/sar 看指标变了没)
        │
        ├── 没变 / 更糟 ──→ 回到 ②,推翻假设,重新推断(这很正常!)
        │
        ▼
   ⑤ 改进(确认根因,固化成配置 / 代码 / 文档)

墨:你看这个环,第②步”推断”是不是可能一开始就错?

你:肯定啊。现象说”CPU 满了”,我第一反应可能是”业务量涨了”,其实是某个死循环或者频繁缺页把 CPU 吃满。推断错了,第③步白调。

墨:对,而且推错不是失败,是诊断的一部分。真正危险的是”推错之后不观察、不回头”——很多人调了一个参数,发现”好像好点”,就以为对了,其实只是噪声波动,根因还在。所以第④步”观察”是铁律:任何改动,必须配对观测,且观测要持续足够久,排除噪声。本书每个案例都会故意让你看一次”推错→观察发现不对→回头”的过程,因为那才是真功夫。

📦 案例(预告):后面第 18 章有个”神秘卡顿”案例。现象是 API P99 偶尔飙到 2 秒。第一直觉是”网络抖”,调了 tcp 参数没用;第二直觉是”GC 慢”,看了 GC 日志也不是;最后用 perf 抓到是周期性写回导致磁盘 IO 毛刺,IOwait 抢了 CPU 调度,根因在 vm.dirty_* 阈值。三进三出才定位。你会跟着走完全程。


0.3 “懂微观”不会让你变成算命的,但会让你少走弯路

我把话说克制一点,免得你以为读完这本书就能秒杀一切性能问题:

  • 懂微观给的是”假设空间的质量”。外行面对 RT 高,假设空间是”全宇宙”;你懂链路,假设空间缩小到”网络/调度/内存/IO 这几块里的某几个具体点”。假设空间小,试错次数就少。
  • 懂微观给的是”观测指标的语义”。你知道 vmstat 的 si/so 是 swap 进出、iostat 的 await 是含队列等待的响应时间,你才不会把 util 100% 当成”磁盘满了”(它常常只是”有请求在排队”)。
  • 懂微观救不了”不观测”。再懂,不抓数据就下结论,照样翻车。工具(perf/ftrace/bpftrace/sar)是显微镜,本书第 25 章专门讲。

墨:所以这本书到底教什么、不教什么?

你:教”地图 + 方法 + 真实案例的迭代全程”,不教”背参数速查表”。参数我在第 12-17 章会列,但列完一定配场景和案例,不会让你当许愿池。

墨:总结到位。本书三块硬货:

  1. 故事线(第 2-11 章):一个程序从你敲回车到硬件动起来,每一级发生了什么——这是你的”地图”。
  2. 参数家族(第 12-17 章):/proc/sys/vm 全解 + net/fs/kernel 等分类整理,每个参数配”什么时候动它”。
  3. 案例集(第 18-23 章):真实问题,走完整诊断环,含推错与回头。

0.4 一个必须提前破除的错觉:内核参数不是”性能开关”

很多人把 sysctl 想成游戏里的”画质开关”——开高画质更爽。错。内核参数几乎都是”在相互冲突的目标之间取权衡(trade-off)”:

  • swappiness 高 → 更积极换出匿名页,腾物理内存给 page cache,文件 IO 快,但进程可能被换到磁盘变慢。
  • swappiness 低 → 死守匿名页在内存,进程不被换,但 page cache 被挤,文件 IO 变慢。
  • 没有”高好还是低好”,只有”你的负载是 IO 密集型还是内存密集型”。

墨:你悟到了吧——为什么抄别人的参数常常翻车?

你:因为别人的负载画像和我不同。他 IO 密集,swappiness 调低对他好;我内存敏感,抄过来反而糟。参数是”匹配负载”的,不是”越大越好”的。

墨:对,这就是为什么”懂微观 + 懂自己负载”才是调参的入场券。本书每个参数我都会先讲它权衡了哪两个目标,再讲什么负载画像该往哪边拧。你带着自己的负载画像来对照,而不是无脑抄。


0.5 本书的阅读约定

沿用你熟悉的风格:

  • 墨: 老兵追问,你: 预设回答;
  • 🔧 思考题 + 【参考答案】:像 K8s 那本一样,思考题是学习材料,答案给足;
  • 📦 案例:真实或高度拟真的故障场景;
  • ⚠️ 坑:血泪教训;
  • 每章结构:机制(发生了什么)→ 场景(什么时候动它)→ 演化(内核为什么这么设计 / 后来怎么改的)→ 案例 → 思考题。

字数上,你要的”不低于 30 万字”是硬指标。我会往里填真东西——微观链路的细节、真实案例的全程、参数背后的权衡——不是注水。这一本和 K8s 那本是一脉相承的:从问题入手,看微观,走诊断环。

墨:序就到这。下一章我给你两张地图:一张是”一个程序从源码到硬件”的微观旅程总图,一张是”系统参数都在哪、分哪几家”的地图。有了这两张图,后面 26 章你都不会迷路。翻开第 1 章。

0.6 参数能解决什么,不能解决什么(先划清边界)

墨:序的最后,我给你划一条最重要的边界——哪些问题该找 /proc/sys 参数,哪些问题你调穿了也白搭。这条线划不清,是”调参民科”和”工程师”的分水岭。

参数能解决的(内核权衡类):

  • 内存紧张时”换还是回收、保匿名页还是保缓存”(swappiness/dirty_*);
  • 网络建连拥塞时”队列多长、syn 怎么挡”(somaxconn/tcp_max_syn_backlog);
  • 调度时”实时任务留多少、普通任务怎么公平”(sched_rt_runtime_us/cpu.max);
  • 文件句柄/进程号”全局和分组的上限”(file-max/pid_max/pids.max)。 这些都是”内核这个通用机制在你的负载下的行为取舍”,参数刚好是调节那个取舍的旋钮。

参数解决不了的(非内核权衡类):

  • 依赖/版本契约:第 2 章会把你 glibc 版本不对导致程序起不来的例子讲透——那是运行环境的契约,不是内核参数。
  • 硬件故障:磁盘坏道、内存条损坏、网卡掉线,调 vm/net 没用,得换硬件或隔离。
  • 架构设计缺陷:单点、无缓存、N+1 没做,调内核参数只能缓解表象。
  • 代码 bug:死循环、锁没释放、SQL 没索引,那是改代码,不是调参。

你:那我怎么快速判断”该调参还是该查别的”?

墨:一句话:如果问题是”内核在做某件通用的事,在你的负载下取舍不合适”,就调参;如果问题是”这件事本身就错了/坏了/不属于内核职责”,就别调参。 第 2 章那个 glibc 案例,你调任何 vm/fs 参数都救不了,因为根因在”运行环境契约”,不在”内核权衡”。先定位问题属于哪一类,再决定手伸向哪里——这是本书贯穿的诊断纪律的第一条。

0.7 贯穿全书的六个真实现场(先睹为快)

墨:序的尾巴,我先把后面第 18-23 章要带你亲手破的六个现场,像菜单一样摆出来。你不用记细节,只要知道”将来你会跟着走完这些坑”,心里有个底。等你真走到那一章,回头看这张表,会有”原来序里就埋了伏笔”的快感。

章节现场(现象)你大概率会”推错”的环节真凶(显微镜下的答案)
18 神秘卡顿全机同时卡几秒、过后自愈先去查 GC / 网络 / 慢 SQL内核”暂停类”:swap thrashing + THP compaction + soft lockup
19 OOM 连环杀进程像多米诺一个接一个被干掉以为”内存满了杀最胖的”dmesg 前缀分整机/cgroup + 流量转移雪崩链
20 磁盘 IO 毛刺RT 偶发尖峰、%util 却低以为”盘慢,该换 NVMe”aqu-sz/await 排队 + 缓存没盖住工作集
21 网络重传重传率 5%,调一周 tcp_* 没用在拥塞控制参数里打转MTU 黑洞(ICMP 被禁废了 PMTU) / 连接池耗尽
22 内存告警free 低 = 泄漏?一见 free 低就查泄漏page cache 挤压 / THP 碎片(都是健康假象)
23 NUMA 大页大内存机器毛刺照”调优帖”把 node 绑死提速zone_reclaim 本地回收 / 绑核不当并行度砍半

你:这六个,我好像都”凭直觉”踩过。

墨:对,这就是写书的动机——你的直觉里,至少有三处是”看起来对、其实反”的。比如 18 章你大概率会先去查 GC;22 章你大概率会一见 free 低就查泄漏;23 章你大概率会照着”调优帖”把 node 绑死。本书的价值,就是把这三个(以及更多)直觉,一个个用显微镜掰开,让你以后再遇到,第一反应就是对的——不是因为记了结论,是因为你亲眼看过”推错→观察→回头”的全过程。

(第 00 章 完)


0.8 一个把优化做成”负优化”的真实故事(序章收尾)

墨:老哥,序章快收尾了,我给你讲个亲历的真事,它比任何道理都更能说明”为什么这本书要你先懂机制、再动手调”。听完你就明白,为什么前面 0.4~0.7 反复强调”参数不是性能开关”。

📦 背景:一个做支付的团队,MySQL 主库偶发慢。一个刚看完"Linux 性能优化 Top 10"博客的同事,周末兴致勃勃地"优化"了一把。

0.8.1 他做的四件事(每件单独看都”对”)

  1. vm.swappiness=0——“关掉 swap,内存不就快了?”
  2. vm.dirty_ratio=5 + dirty_background_ratio=2——“脏页少攒点,宕机丢得少。”
  3. vm.overcommit_memory=2 + overcommit_ratio=80——“严格点,别让进程乱开内存。”
  4. 写了个 cron 每 10 分钟 echo 3 > drop_caches——“清清缓存,服务器干净。“

0.8.2 周一上班:不是变快,是炸了

你:这四件不都是”常见优化”吗?博客里都这么写。 墨:单独看,每一句在”对应那个博客的特定画像”下都对——但支付库不是那个画像,而且四件事凑一起,互相打架。 周一的现象:

  • 慢查询更多了(swappiness=0 让 page cache 被挤、direct reclaim 卡,第 12/13 章);
  • 半夜一次内存尖峰,直接 OOM 杀了一堆连接线程(overcommit=2 把 malloc 提前失败,应用没优雅处理,雪崩,第 14 章事故一);
  • 每 10 分钟一次全站卡顿 20 秒(drop_caches 定时清,第 12/14 章泄洪阀当旋钮);
  • 写入延迟方差暴涨(dirty_ratio=5 太低,前台 sync 频繁卡,第 12/13 章)。

一句话:他以为在做优化,实际在制造”负优化”——而且四件事的乘积效应,谁单独都预料不到。

0.8.3 根因一句话

墨:根因不是”他调错了参数”,是**“他调参数时,脑子里没有这台机器跑的是什么、参数之间怎么联动”**。swappiness=0 对”能优雅失败的批处理”也许OK,对”扛不住 malloc 失败的 MySQL”是毒药;drop_caches 进 cron 对任何服务都是自残;overcommit=2 必须配套”程序能处理 EAGAIN”——他全都没配套。

0.8.4 这一章想让你记住的

墨:把第 0.8 节焊死成一句话,作为你翻开后面 25 章的入场券:

任何一次”照着帖子调”,都是一次没有保单的赌博;你赢的只是运气,输的是你不懂机制时永远算不清的耦合。

而这本书的全部努力,就是把”运气”换成”推演”——让你下次动手前,先能在脑子里跑一遍”我动这个,会逼着哪个相邻参数补偿、代价我扛不扛得住”(第 12 章联动、第 13 章冲突章节早把这话说透了)。序章到此,下章开始,我们从”程序到底经过什么”讲起。

0.8.5 一道送你的序章思考题

思考题(入门): 假设你是那个同事,现在让你”在不读后面章节的情况下”只改一处、且保证不制造新问题——你会改哪一处、为什么?【参考答案倾向于”哪处都不动,先搞清楚机器画像再说”:因为四件事里没有一件是”必须在不懂时就动”的急救;真正该做的是先 free/vmstat/sar 看清当前瓶颈(第 25 章),再决定。这本身就是序章要灌给你的”克制”。】


实战补遗一:sysctl -w 调好,重启却失效——加载顺序的坑

墨:你 sysctl -w net.core.somaxconn=65535 一敲就好,重启后又变回 128。你第一反应会去查什么?

你:肯定是没写进 /etc/sysctl.conf,重启丢了呗。

墨:多半是这样,但还有一种更阴的——你写了 sysctl.conf、也 sysctl -p 过,可重启还是 128。为什么?因为加载顺序。这一节专治”我以为持久化了其实没持久化”。

现象:内核升级后连接开始被静默丢

某服务 somaxconn 调好上线,平稳跑了一周。一次内核小版本升级 + 重启后,开服突发又出现”玄学超时”(第 15 章讲的那类 accept 队列溢出)。排查发现 sysctl net.core.somaxconn 重启后是 128,不是 65535。可 /etc/sysctl.conf 明明写着 net.core.somaxconn=65535,手动 sysctl -p 也能生效。

推断(可能推错):先以为配置文件丢了

第一推断:「配置文件被覆盖 / 没保存。」——vim 打开一看,明明在。推错。

真相:sysctl.d 里有个”后来者”盖了你的

系统启动时 systemd-sysctl 按字母序加载 /etc/sysctl.d/*.conf 和 /etc/sysctl.conf。问题在:某些发行版/云镜像会在 /etc/sysctl.d/ 下放一个 50-default.conf 或 20-netdefaults.conf,里面把 somaxconn 设成默认 128;而你的配置写在主 sysctl.conf(加载顺序靠前)或某个字母序靠前的文件里。字母序靠后 = 后加载 = 生效。你的 65535 被后面那个 128 盖了,重启后实际是 128。

更隐蔽:sysctl -p 默认只加载 /etc/sysctl.conf 这一个文件,不碰 sysctl.d/——所以你手动验证时一切正常,但开机走的是”全套 sysctl.d + 主 conf”的合并顺序,结果不同。

调整:让”你的”最后加载

  • 把你的配置放进字母序靠后的文件,如 /etc/sysctl.d/99-myservice.conf,确保它是最后加载、盖掉前面所有默认值;
  • 验证用 sysctl --system(模拟开机全部加载后的最终值),而不是 sysctl -p;
  • 上线前在目标内核上 sysctl --system | grep somaxconn 确认;
  • 容器场景:这些要在宿主或 init 容器里设,容器内的 sysctl -w 很多参数是 read-only 或 namespace 隔离,重启即丢(呼应第 17 章)。

再观察:改名后稳定

配置移到 99-myservice.conf,sysctl --system | grep somaxconn 显示 65535,连做三次内核升级重启都稳。

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

动作拿什么换什么
放 sysctl.d/99-*.conf多管一个目录重启一定生效、不被盖
sysctl --system 验证一条额外命令避免”手动好、开机坏”误判
容器里靠宿主/init 设配置外移容器重启不丢

🔧 思考题:sysctl -p 和 sysctl --system 到底差在哪?为什么只跑 -p 会骗自己?

【参考答案】

-p [file] 默认只加载 /etc/sysctl.conf(或你指定的文件),完全不读 /etc/sysctl.d/*.conf;而开机时 systemd-sysctl 走的是 --system 那套:按字母序合并 sysctl.d/ 下所有文件 + 主 conf,后加载的覆盖先加载的。所以你 sysctl -p 通过 ≠ 开机生效——只要 sysctl.d/ 里有个字母序更靠后的文件把同一个参数改回默认,开机后就是默认。验证持久化必须 sysctl --system,它给你”开机后真实的最终值”,而不是单个文件的局部值。


实战补遗二:把 echo 3 > drop_caches 写进 cron 的”清内存”脚本,反而制造周期卡顿

墨:你机器 free 看 available 掉到 20%,慌了,写了个 crontab 每 5 分钟 echo 3 > /proc/sys/vm/drop_caches”释放内存”。结果业务开始周期性卡顿,你又去查 IO。我说:凶手就是你这个 cron。

你:drop_caches 不是”清掉没用的缓存”吗?清了不是省内存?

墨:page cache 不是”没用的”,是”下次读盘能省一次 IO 的朋友”。你每 5 分钟把它全清了,业务下一次读任何文件都得重新从磁盘读——等于你亲手制造了一场每 5 分钟一次的 IO 风暴,而且把本来该缓存的热数据全打回冷盘。

现象:每 5 分钟一次的”神秘卡顿”

某服务监控显示,每隔 5 分钟出现一波 P99 延迟尖峰(200ms~1s),持续约 10 秒后恢复。iostat 同步看到那 10 秒磁盘 await 飙高、r/s 暴涨。top 没见异常进程。完全周期性,像时钟。

推断(可能推错):先查”5 分钟一次的定时任务”

第一推断:「有 cron 任务在跑。」——crontab -l 一看,只有一个自己写的 drop_caches 清内存脚本,时间正好 5 分钟。但运维第一反应是”清内存能有什么坏”?没往这想,反而去查是不是别的 cron。

真相:你清的是业务的”加速缓存”

drop_caches=3 清 page cache + dentries + inodes。这些正是业务读文件/读索引的热缓存。清掉后:

  • 业务读索引/读数据文件 → page cache miss → 真去磁盘读 → IO 暴涨、延迟尖峰;
  • 10 秒后缓存重新热起来,延迟恢复 —— 于是”每 5 分钟一波”的钟形曲线;
  • 更糟:清 cache 本身要内核扫 LRU、锁内存,清的瞬间也有开销。

page cache 是内核用”空闲内存”做的性能投资,内存紧时内核会自动回收,你手动清是”拔掉网线省电”。

调整:别清,或只清真正可回收的、且非高峰

  • 直接删掉这个 cron——drop_caches 只该在诊断(确认”掉的是 cache 不是泄漏”,第 22 章)时手动跑一次,绝不该周期性自动跑;
  • 若非要(如某特定大文件处理完想释放),只 echo 1(清 page cache)且避开业务高峰,或限定 cgroup;
  • 真问题是 available 掉:去看是哪种内存涨(上一章 SReclaimable/SUnreclaim 拆分),治本而非清 cache;
  • 监控 page cache 命中率(cachestat/fincore),命中率高说明 cache 在干活,别清。

再观察

删掉 cron 后,周期性尖峰消失,available 稳定在 20%(cache 健康常驻),业务读延迟更稳(因为热数据一直在 cache)。

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

动作拿什么换什么
删 drop_caches cron少一个”安心”脚本不再自造 IO 风暴
看命中率而非 available多指标知 cache 在干活

🔧 思考题:那 drop_caches 到底什么时候该用?它有害还是有用?

【参考答案】

有用,但只在”诊断”这一场景:当你怀疑”内存涨是 page cache 还是真泄漏”时,手动 echo 3 > drop_caches 看 available 回不回——回了说明是 cache(健康),不回说明是 SUnreclaim 真泄漏(第 22 章)。仅此而已。它绝不该周期性自动跑,因为那等于每周期废掉业务的读缓存、自造 IO 风暴。一句话:drop_caches 是”听诊器”,不是”保健品”;健康人天天吃保健品,只会被副作用坑。这也正是第 0 章”负优化”的典型:以为在优化,实际在制造新问题——而你调的每个内核参数,背后都是”程序在某一层遇到了什么”,清 cache 只是你没想清”哪一层”。


实战补遗三:次”优化翻车”全过程——把 swappiness 调 0 反而 OOM

墨:第一章就讲哲学,我得先给你泼盆冷水:我见过最多的”优化事故”,恰恰出自”照着网上帖子无脑调参”。讲一个我亲手翻过的车。

那年带一个新服务上线,帖子说”数据库机器 swappiness 一定要设 0,禁用 swap 才不会慢”。我照做了:sysctl -w vm.swappiness=0,心里美滋滋。结果半夜告警:OOM killer 把从库杀了。

你:诶?禁用 swap 不是应该更快吗,怎么还 OOM?

墨:这就是典型的”只知结论不知前提”。swappiness=0 的含义在**老内核(3.5 之前)和新内核(3.5+)**完全不同:

  • 老内核:0 = 尽量不 swap,但内存真不够时仍会 swap;
  • 新内核:0 = 除非匿名页和文件页都无处可退,否则绝不 swap,但更关键的是它改变了”回收优先级”——内核会疯狂回收**文件页(page cache)**来腾内存给匿名页。

我的从库当时情况:物理内存 80G,innodb_buffer_pool 设了 60G(匿名 mmap),剩下 20G 给内核 + page cache + 其他进程。设 swappiness=0 后,一旦内存紧张,内核宁可把热乎的 page cache 全收回去,也不肯 swap 那 60G 缓冲池。于是 MySQL 读数据发现 page cache 空了,狂读磁盘,IO 飙升;同时那 20G 余量被其他进程一点点吃掉,匿名页无处退(不敢 swap),直接触发 OOM。

推断我推错的过程:① 先看 dmesg 的 OOM 日志,oom_score 最高的确实是从库进程——但为什么是它?它是内存大户;② 再看 free -h,available 不算太低,可 Swap 一行是 0(从没用过)——这就反常:内存紧张却完全没 swap,说明 swappiness=0 把 swap 这条路焊死了;③ 关键证据:cat /proc/vmstat | grep pgscanned 和 pgsteal_file 暴涨、pgsteal_anon 几乎为 0,证明内核在猛回收文件页、死活不动匿名页。

调整:把 swappiness 从 0 提到 1(注意不是回到默认 60!1 是”几乎不 swap,但留一丝退路”),并给 innodb_buffer_pool 留足 10G 系统余量。再观察:内存紧张时内核开始小幅 swap 那 10G 余量里的匿名页,page cache 保住了,IO 回落,OOM 不再出现。

你:那到底 swappiness 该设多少?帖子错在哪?

墨:帖子没错在”数据库机器要压低 swappiness”,错在”压到 0”。swappiness=1 才是新内核下的正确姿势——它表达”我极度不想 swap,但请你在匿名页真没处去的时候,允许 swap 一点点保命”,而不是”宁可 OOM 也不碰 swap”。

这条事故的哲学教训,正好呼应本章开头说的三句话:

  • 没有银弹:别人的生产参数贴的是他的负载,不是你的。
  • 先测量后调优:我没测 pgsteal_* 就敢改,活该翻车。
  • 理解机制才能反推:不懂 swappiness 新老内核语义差异,就把”禁用 swap”当教条。

🔧 思考题:如果当时我没改 swappiness,而是选择”直接关掉 swap 分区(swapoff -a)“,结果会有什么不同?哪种更危险?

【参考答案】

swapoff -a 比 swappiness=0 更危险。区别:

  • swappiness=0(新内核)至少保留了”匿名页完全没退路时仍允许 swap”的兜底语义,内核回收策略只是极度偏向文件页;
  • swapoff -a 是物理上没有 swap 设备可用,无论 swappiness 设多少,匿名页都无处可退。一旦匿名页需求超过物理内存余量,直接 OOM,连”swap 一点点保命”的机会都没有。

所以正确顺序永远是:保留 swap 分区(或 swap 文件)存在,但用 swappiness=1 把它压成”最后兜底”。很多 DBA 的”生产机不配 swap”是另一个常见翻车点——他们以为内存大就不需要 swap,却忘了 swap 在 OOM 前提供的是”缓冲突变量”,不是”慢速内存扩展”。除非你 100% 确定匿名页峰值永远不会超物理内存(几乎没人能保证),否则保留 swap、压低 swappiness 是更稳的组合。


实战补遗四:观测优先于调参——从一款 APM 的”自我放大”翻车讲起

墨:第一章说”先测量后调优”。我再给你一个反面教材,说明不测量就调参,不仅无效,还会让问题自我放大。

那年团队上了个自研 APM(应用性能监控),它自己是个 agent,挂在每个进程里采 CPU/内存/系统调用。上线后整机 CPU 从 30% 飙到 70%,业务延迟涨了 2 倍。第一反应是”业务代码有 bug”,各种 profile 业务,找不到;又有人猜”是不是内核参数要调”,开始翻 sysctl 想压 sched_* 调度开销——完全调错方向。

你:等等,APM 自己不也吃 CPU 吗?会不会是监控 agent 太重?

墨:对,但没人往这想,因为”监控怎么会有问题”。直到有个同事 perf top 看了一眼:CPU 烧得最多的不是业务,是一个叫 apm-agent 的进程,它 openat/read /proc/<pid>/stat 和 /proc/<pid>/stack 的频率高得离谱——它为了”无侵入”,每秒把全机几百个进程的系统文件读一遍,而读 /proc/<pid>/stack 这种会触发内核栈遍历、持锁,本身就贵。

推断:问题不是业务、不是内核参数,是监控 agent 自己的采样开销。但根因更讽刺——它是”监控延迟”的,发现延迟高就提高采样频率想看清楚,采样越频繁、/proc 读越狠、CPU 越满、业务延迟越高、它又提高采样频率……正反馈自我放大,直到机器半死。

调整:① 立刻把 APM 采样频率砍到 1/10,机器 CPU 当场回落到 35%,业务延迟回归——这就证伪了”业务 bug”和”要调内核参数”两种猜测;② 根本:改 APM,用 eBPF(第 25 章)挂跟踪点拿数据,别每秒扫几百个 /proc 文件;且给 agent 自己的 CPU 设 cpu.max 硬墙(cgroup,第 17 章),防止它再自我放大。

你:这个故事和”调参”有啥关系?

墨:关系在思维方式。当时如果先 测量(哪怕一条 top/perf top),3 分钟就能锁定 apm-agent;但团队跳过了测量,直接去”调内核参数”想缓解症状——这恰恰是最蠢的路径:你调的参数(如 sched_min_granularity)根本不控制”用户态进程读 /proc 的频率”,调了也不会好,反而引入新变量、掩盖真因。

这就是第一章那句”先测量后调优”的血泪版:测量是锚,调参是桨;没锚就划桨,只会原地打转还以为在前进。所有”我调了 XX 没用”的抱怨,九成是没先测量、调错了旋钮。

🔧 思考题:如果你的监控 agent 也靠扫 /proc 工作,怎么在不改代码的前提下,用 cgroup 先把它”关进笼子”止血?

【参考答案】

用 cgroup v2 给 agent 进程设 cpu.max(硬节流),限制它最多能用的 CPU 份额,切断”自我放大”的正反馈:

mkdir -p /sys/fs/cgroup/apm
echo "max 100000" > /sys/fs/cgroup/apm/cpu.max   # 限 1 核(100000 微秒/100000 微秒)
# 把 agent 主进程移进去
echo $APM_PID > /sys/fs/cgroup/apm/cgroup.procs

这样无论 agent 怎么提高采样频率,它的 CPU 占用被焊死在 1 核以内,整机不会被拖垮,业务先恢复。这招的价值:“不改代码、秒级止血、且不影响业务”——比急着改内核参数对症一万倍。等稳定了再回头改 agent 用 eBPF 替代扫 /proc(根治)。注意 cpu.max 是容器/节点级 cgroup 参数,不是 sysctl,别去 sysctl 里找它——又回到”找错门”的老坑。



实战补遗五:实测驱动 vs 玄学调参——一张 A/B 对照表怎么读

墨:老哥,你说 swappiness 默认 60 不好,生产要设 1。这话没错,但我问你一个扎心的问题:你上一次把 swappiness 从 60 改成 1 之后,有没有真的对比过改之前和改之后的指标?还是说群里有人说”生产都设 1”,你就顺手 sysctl -w 了?

你:……被你说中了。我是照着别的团队的最佳实践抄的,没专门对比。

墨:这就是”玄学调参”——只改不改验。调参这件事,真正值钱的不是那行命令,而是你证明了它有用的那张对照表。我给你看一次我翻车的真实经历。

现象:有台 64G 内存的 API 机器,平时空闲内存常年在 20G 以上,但偶尔会卡顿几百毫秒。我一看 swappiness=60,心想”多余内存被拿去缓存,触发 swap 倾向高,肯定在偷偷 swap”。于是 sysctl -w vm.swappiness=1,改完重启服务,心里踏实了。

推断:我以为改完卡顿会消失。

可能推错:第一,机器空闲内存 20G,根本没到 swap 的阈值,swap 在改之前几乎没发生,所以 swappiness 跟这次卡顿没关系;第二,卡顿是周期性的,更像是某个后台任务(日志压缩)的定时抖动,不是 swap。

调整:我做了一次真正的 A/B——同一台机器,连续两天分别跑 swappiness=60 和 swappiness=1,用 sar -B 1 记录 pgpgout/s(换出速率)和 pgscank/s,用 sar -W 记录 pswpin/s/pswpout/s。结果发现两天 pswpout/s 都是 0,swappiness 根本没起作用。

再观察:卡顿还在。我换成 perf sched 抓调度延迟,发现是每天凌晨 3 点的日志 rotate + gzip 占满了单核,把 Go 的 GC mark 阶段挤到边上去了。根因是 cron 任务没绑核,不是 swappiness。

改进:把日志压缩任务 taskset 绑到单独两个核,卡顿消失。swappiness 我最后留了 1(无害),但这次优化的功劳不在它身上。

🔧 思考题:如果你手头有个”大家都在用”的参数模板(比如 Redis 的 vm.overcommit_memory=1),你打算怎么向自己证明”它在这台机器上真的必要”,而不是照抄?

【参考答案】

证明”必要”的标准动作是对照实验,不是信仰:

  1. 先量基线:改之前连续采集 24~48 小时的目标指标(延迟 P99、swap 速率、OOM 次数)。
  2. 再改一个变量:只动这一个参数,别顺手改一串。
  3. 同条件再量:同样时长、同样流量曲线,记录同一组指标。
  4. 看差异是否显著:如果基线和改后指标没区别,说明这个参数对你这台机器是” placebo”,该还回去或保持默认,别让它成为没人懂的祖传配置。
  5. 记进文档:把”为什么设、设了之后哪个指标变了”写清楚,否则半年后接手的人又得猜。

overcommit_memory=1 对 Redis 多数情况是真必要的(避免 fork 时因 overcommit 检查被拒导致 BGSAVE 失败),但你要验证的是”fork 时是否出现过 Cannot allocate memory”,而不是”大家都设”。玄学调参和工程调参的分界线,就是这张表。


实战补遗六:优化前先建基线——没有基线,所有”调好了”都是幻觉

墨:老哥,你打算优化一个服务,第一件事做什么?

你:看哪慢,改参数,再测。

墨:顺序反了。“改参数再测”缺了基线这步,你根本不知道改完是变好还是变差,甚至分不清”我改的”和”流量自己降了”。这节讲基线的纪律,是优化哲学的第一条。

现象:一个 API p99 120ms,有人说”调 net.core.somaxconn 能更快”。运维调了,隔天看 p99 变 100ms,宣布”优化成功”。一周后流量涨了 p99 又到 130ms,他懵了——到底是参数有用还是流量波动?没法证伪。

推断:调了之后数字好看就是有用。

可能推错:没基线的”好看”不可信。p99 本来就随流量、GC、网络抖动波动 ±20ms,你调完正好落在波动低点,归因错误——把波动当成效。更危险的是”调完变糟但没发现”:没记录前后,你连”糟了”都不知道。

调整:优化前必做”基线三件套”:

  1. 采集窗口够长:至少覆盖一个完整业务周期(一天/一周的峰谷),取 p50/p90/p99/p999,别拿单次截图当基线。用 prometheus/grafana 或 sar -A 留历史。
  2. 固定变量:基线期间不动任何参数、不发布,确保数字只反映”自然状态”。
  3. 记录环境:内核版本、CPU 型号、cgroup limit、并发量,这些会变,基线要带上下文,否则半年后不可比。

再观察:有了基线再改一个变量(只动 somaxconn),同条件重采 24h,对比 p99 分布——如果重叠(没显著偏移),说明这个参数对你这台机器是 placebo,该还原默认,别留祖传配置。如果显著左移,记录”改了什么 → 哪个指标变多少 → 代价是什么”(如连接队列变大占更多内存)。

改进:

  • 任何优化先留基线(histogram percentile,不是平均值)。
  • 一次只改一个变量,对照实验。
  • 优化结果写进”变更记录”,含回归风险(如 somaxconn 过大在 SYN flood 时更危险)。
  • 基线是会过期的:内核/流量大变后重测,别迷信一年前的”最佳实践”。

🔧 思考题:为什么用”平均值”衡量优化是坑,而要用 p99/p999?如果基线 p99 是 120ms,改完 p99 也是 120ms,但 p999 从 800ms 降到 200ms,算优化成功吗?

【参考答案】

平均值掩盖长尾:120ms 平均可能由 100 个 100ms 请求 + 1 个 2000ms 请求组成,平均被”多数正常”拉平,你优化掉那个 2000ms 的毛刺,平均几乎不动,但用户体验(那个卡 2 秒的请求)天差地别。长尾(p99/p999)才是”最差用户体验”和”稳定性”的真实指标,优化要盯它,不是平均。

p99 持平但 p999 从 800ms→200ms:算成功,而且是很典型的”消毛刺”型优化。p99 代表”绝大多数请求”,p999 是”最糟的 0.1%“。很多优化(如消除锁竞争、修 GC 尖峰、消跨 node 访存)不改善常态延迟,但能把偶发长尾砍掉——这恰恰是最有价值的稳定性优化,因为长尾往往对应”某个请求撞上资源争抢/ NUMA 远端/ 调度延迟”,修掉它业务就少一批超时。所以评估优化要用多分位看:p50 看常态、p99 看普遍体验、p999 看极端毛刺。只看 p99 会漏掉”p999 改善但 p99 不动”的好优化。

基线纪律的延伸:基线还要区分”自己抖动”和”外部波动”。用对照窗口(改前 N 天 vs 改后 N 天同时间段)抵消周季节性;用 A/B(同一机器分流量)抵消流量变化。没有对照的”数字变好”在统计学上叫”没有控制变量”,结论不可靠——这也是为什么第 0 章序里强调”实测驱动 vs 玄学调参”:基线和对照,是这两者唯一的分界线。