第 13 章 vm 场景化用法——你的负载长什么样,参数就怎么配
第 12 章把
/proc/sys/vm每个参数拆开了。但参数是组合用的——你不会单独调swappiness而不管dirty_ratio。 这一章反过来:从负载画像出发。你是哪种服务,vm 参数就长成哪种样子。我给你几套”可以直接抄、但知道为什么”的组合,每一套都讲清:赌的是什么、换的是什么、哪些是铁律别碰。
一、先立一个总纲:没有”最优”,只有”匹配你的画像”
墨:老哥,先泼盆冷水。网上所有”Linux 服务器最优 vm 参数”帖子,本质都是在讲某一个特定负载画像下的组合,然后被不懂的人当成”通用最优”到处抄。结果就是:抄数据库的参数去跑 Web 前端,抄容器的参数去跑 HPC,全盘皆错。
你:所以它们都是对的,又都是错的?
墨:对了一半——对”那个画像”是对的,对你未必。 我给你五套画像,你先对号入座,再决定抄哪套。抄之前问自己一句:“我的负载,和这套画像像吗?”
| 画像 | 核心矛盾 | vm 调参主旋律 |
|---|---|---|
| A 数据库/缓存(MySQL/Redis/PG) | 怕 OOM 杀、怕延迟抖动 | 保内存低延迟、给免死金牌、限制脏页 |
| B 文件服务器/高吞吐写(存储/日志/对象) | 写吞吐 vs 故障丢脏数据 | 调脏页节奏、保 page cache |
| C 低延迟交易/实时(行情/风控) | 任何回收卡顿=事故 | 留足水位、几乎不 swap、限 reclaim |
| D 容器/云原生(混部多租户) | 单容器炸不影响邻居、cgroup 隔离 | overcommit 配合 cgroup、限内存上限 |
| E NUMA 大内存(256G+ 单机) | 跨 node 访问慢、局部 reclaim 毛刺 | 绑 node、关 zone_reclaim、调水位带 |
二、画像 A:数据库 / 缓存服务
墨:这是最常见的”被抄错”对象。数据库的特点:内存就是它的命——InnoDB buffer pool、Redis 数据集,全在内存里;一旦被 OOM 或 swap 拖慢,直接是线上事故。
推荐组合:
# 1) 几乎不 swap:保匿名页(数据库数据集)在内存
vm.swappiness = 1 # 不是 0!留一点点余地(第12章讲过)
# 2) 脏页别攒太多:宕机丢的脏数据少,且避免前台 sync 卡
vm.dirty_ratio = 10 # 前台被逼刷的阈值压低
vm.dirty_background_ratio = 5
# 3) 大内存机器水位带加宽,避免 direct reclaim 卡
vm.watermark_scale_factor = 100 # 256G 上变 2.5G 缓冲带
# 4) per-process 免死金牌(不在 vm 下!)
# 给 mysqld / redis-server 设 oom_score_adj = -500
echo -500 > /proc/<pid>/oom_score_adj赌的是什么 / 换的是什么:
- swappiness=1 赌”内存够,几乎不 swap”,换”万一真不够,有 1% 余地让内核腾 page cache 而非硬卡”。
- dirty_ratio 压低赌”宁可频繁后台刷,也不让宕机时丢几秒脏数据”,换”写吞吐略降”。
- oom_score_adj=-500 赌”数据库比别的进程更该活”,换”真 OOM 时可能杀掉隔壁无辜进程”。
墨:铁律:数据库的”内存上限”(buffer pool size / maxmemory)必须 小于物理内存留足余量,否则它自己吃光,触发全局 reclaim/ OOM,设 swappiness 也没用。参数调的是”挤兑时的行为”,不是”让 32G 的 buffer pool 跑在 16G 机器上”。
三、画像 B:文件服务器 / 高吞吐写
墨:反过来——对象存储、日志归集、数据导出这类,写吞吐是命,脏数据丢了能重来。它们的 vm 调法和数据库几乎相反。
推荐组合:
vm.dirty_ratio = 40 # 允许攒更多脏页,突发写吞吐高
vm.dirty_background_ratio = 20 # 后台刷也放宽
vm.dirty_expire_centisecs = 6000 # 脏页可滞留更久(1分钟)
vm.swappiness = 10 # 仍保低延迟,但比数据库略放宽
# 大内存机器同样加宽水位带
vm.watermark_scale_factor = 100赌的是什么 / 换的是什么:
- dirty 调大赌”写突发时先全进 page cache,后台慢慢刷,吞吐拉满”,换”机器宕机时丢的脏数据更多(最多约 dirty_ratio 比例的内存量)“。对”能重来”的日志/导出,这个赌值得;对”不能丢”的数据库,这个赌要命。
- 你立刻看出:dirty_ratio 该大该小,完全取决于”脏数据丢了能不能重来”。这就是”匹配画像”四个字的全部含义。
你:那如果我是”既要高吞吐写、又不能丢”怎么办?比如消息队列?
墨:消息队列(Kafka/RocketMQ)是混合画像——它把消息当日志顺序写(像 B),但丢了就是事故(像 A)。所以它们的做法不是靠 vm 参数,而是自己控制刷盘策略(同步刷 vs 副本确认),vm 上走 A 的保守路线 + 依赖顺序写的高吞吐。参数解决不了”既快又零丢”,那是应用层设计(副本、ACK、fsync 频率)的事。别指望拧 vm 一个旋钮就两全。
四、画像 C:低延迟交易 / 实时
墨:行情、风控、撮合——这类服务对延迟抖动零容忍,一次 10ms 的 direct reclaim 卡顿就是事故。调法核心是:让回收永远提前、异步发生,绝不落到”前台同步回收”。
推荐组合:
vm.swappiness = 1
vm.watermark_scale_factor = 1000 # 10%!水位带极宽,kswapd 早早异步回收
vm.min_free_kbytes = 调大(按内存,常设 1~2G 级别)
vm.dirty_ratio = 5 # 脏页峰值压到极低,绝不前台 sync
vm.dirty_background_ratio = 2
# 配合:透明大页 THP 关掉(第23章讲,THP 的合页会引入偶发卡顿)
# 配合:把服务绑到固定 CPU + 固定 NUMA node,避免跨 node 访问赌的是什么 / 换的是什么:
- watermark_scale_factor=1000 赌”用更多常驻内存做缓冲,买’回收永远提前’“,换”可用内存明显变少”(10% 内存常驻做缓冲在 64G 上是 6.4G,代价不小,但交易系统内存通常宽裕,值得)。
- 关 THP 赌”避免合页的偶发毫秒级卡顿”,换”TLB 命中率略降”(小页 TLB 覆盖小)。
墨:这类场景的真相:延迟抖动 90% 来自”平时不回收、临界才慌”。把水位带拉到极宽,等于”让 kswapd 一直在后台温柔地干,前台永远不被迫同步回收”。这是用内存换确定性的经典打法。
五、画像 D:容器 / 云原生(混部多租户)
墨:容器改变了游戏规则——你看到的”机器内存”是宿主的,但你被 cgroup 限制了。vm 参数是宿主机的全局设置,容器里调它影响所有邻居。所以容器场景的 vm 调参,主角其实是宿主 + cgroup 内存上限的配合。
宿主推荐(给容器平台的 SRE):
vm.overcommit_memory = 0 # 启发式,别设 1(容器密集 malloc 会撑爆)
vm.swappiness = 10 # 容器平台适度,避免 page cache 被挤
vm.watermark_scale_factor = 100
# 关键不在 vm,在 cgroup:
# 每个容器设 memory.limit_in_bytes,超出被 cgroup OOM(比全局 OOM 温柔、可定位)
# 且开启 memory.stat 监控,超限告警容器内的误区:
- 在容器里
sysctl -w vm.swappiness=...—— 通常无效或需特权,且影响宿主全局,别干。 - 容器内看到”内存满了”其实是 cgroup 限额到了,看
/sys/fs/cgroup/memory/memory.stat而非free(free看的是宿主)。 - 过度设
overcommit_memory=2在容器平台是灾难:每个容器都按各自视角算 commit,叠加起来远超物理,反而大 malloc 频繁 EAGAIN。
墨:容器场景的铁律:内存隔离靠 cgroup,不靠 vm 参数。 vm 参数是宿主的”全家桶设置”,按”这台宿主跑什么混合负载”调一次就行,别让每个容器去拧它。
六、画像 E:NUMA 大内存(256G+ 单机)
墨:NUMA(非统一内存访问)是大内存机器的隐形坑。每个 CPU 有自己的本地内存 node,访问本地快、跨 node 慢(慢 1.5~2 倍)。zone_reclaim_mode 这个参数(第 12 章提过)在 NUMA 下极其坑。
推荐组合:
vm.zone_reclaim_mode = 0 # 关键!允许跨 node 分配,别优先本地回收
vm.watermark_scale_factor = 100
# 配合:numactl --interleave=all 启服务,或绑 node
# 配合:内核启动参数 numa_balancing 按需开赌的是什么 / 换的是什么:
zone_reclaim_mode=0赌”跨 node 访问的慢,比本地 reclaim 的卡顿更可接受”,换”内存分布不均(某 node 满、某 node 空)“。设 1 的诱惑是”保证本地性”,但本地内存紧时 kernel 会优先在本地做 reclaim,引发剧烈局部卡顿,Redis/MySQL 在 NUMA 机器上莫名其妙的毛刺,八成是它。- 最佳实践:要么
numactl --interleave=all让内存均匀分布在各 node(牺牲一点本地性换均衡),要么明确绑 node(进程和内存都在同一 node,吃满本地性)。最忌讳的是”不绑 node + zone_reclaim_mode=1”——既没本地性收益,又吃 reclaim 卡顿。
七、一个场景调参的完整迭代环(画像 A 实战)
现象 → 推断(可能推错)→ 调整 → 再观察 → 改进
7.1 现象
一台 64G 内存的 MySQL 机器,free 看 cache 占了 40G,buffer pool 设 40G。业务偶发”查询突然慢 1~2 秒”,但 CPU、磁盘 %util 都正常。DBA 第一反应是”加索引”。
7.2 第一推断(推错):SQL 问题
你:慢查询嘛,先 slow log 抓 SQL,EXPLAIN 看执行计划。结果:慢的那几秒里,所有 SQL 都慢,不是某条特定的。排除单条 SQL。
7.3 第二推断(接近):swap 导致
你:vmstat 看 si/so——发现 so(换出)在慢的时刻有跳动,swappiness=60 默认,MySQL 的匿名页(连接缓冲区、临时表)被换出去了,查询时换回来卡 1~2 秒。
调整:设 vm.swappiness=1。观察:换出基本消失,但慢查询没完全消失,只是频率降了。
7.4 第三推断(命中):direct reclaim 卡顿
你:继续查 vmstat 的 allocstall(direct reclaim 次数)——在慢的时刻飙升。因为 buffer pool 40G + OS 其他占用,free 经常逼近水位,分配时触发 direct reclaim 卡住 MySQL 线程。swappiness 只管”换不换”,不管”分配时回收卡不卡”。
调整:
vm.watermark_scale_factor = 100(64G 上缓冲带从 64MB 变 640MB,kswapd 提前异步回收);- 略增
vm.min_free_kbytes(保原子分配); - 给
mysqld设oom_score_adj = -500(防万一 OOM 被杀); - 根因:buffer pool 40G 在 64G 机器上太满,留给 OS 的太少。把 buffer pool 降到 44G,留 20G 给 OS 和其他进程。
观察:慢查询消失,allocstall 归零,P99 回到 10ms 级。四步:SQL 推错 → swap 对一半 → direct reclaim 命中 → 根因是 buffer pool 占太满。
墨:这个案例把第 13 章的”画像 A 组合”焊死了:swappiness 只管一半,direct reclaim 才是真凶,而根因是”应用内存规划超过物理余量”。参数能治”挤兑时的行为”,治不了”本来就没留余量”。
八、🔧 思考题(都配参考答案)
思考题 1(基础): 为什么说”网上所有’Linux 最优 vm 参数’帖子都是对一半错一半”?你抄参数前必须问自己的一个问题是什么?
【参考答案】
那些帖子讲的都是某一个特定负载画像下的组合(多为数据库/通用服务器),被不懂的人当成”通用最优”到处抄,于是”数据库参数跑 Web 前端、容器参数跑 HPC”全盘皆错。抄之前必须问:“我的负载,和这套画像像吗?”——核心矛盾是否一致(怕 OOM/怕延迟抖动?写吞吐优先?零丢失?)。参数组合是”匹配画像”的解,不是”放之四海皆准”的解。对号入座后再抄,且知道每项的赌注与代价,而非无脑粘贴。
思考题 2(深入): 消息队列(Kafka)既”顺序写高吞吐(像画像 B)“又”丢消息即事故(像画像 A)“。为什么我说”靠 vm 参数解决不了它的’既快又零丢’”?它靠什么解决?
【参考答案】
因为”高吞吐写”和”零丢失”在 vm 层是冲突的:dirty_ratio 调大换吞吐、但宕机丢更多脏数据;调小保数据、但吞吐降。vm 参数只能二选一赌一边,无法两全。Kafka 的”既快又零丢”靠的是应用层设计:① 副本机制(消息复制到多个 broker,单点宕机不丢);② producer 的 ACK 级别(acks=all 等多数副本确认才返回成功);③ 自身的刷盘/副本同步策略。vm 上它走画像 A 的保守路线(脏页压低、swappiness 低),把”零丢”交给副本与 ACK,把”快”交给顺序写天然高吞吐。结论:参数边界内的冲突,要用架构设计在边界外化解。
思考题 3(NUMA 实战): 一台 2 socket 的 512G 服务器跑 Redis,出现”偶发几百毫秒毛刺”,vmstat/iostat/top 都正常。你怀疑 NUMA。请指出现象对应的 NUMA 根因、该查什么、怎么治。
【参考答案】
根因大概率是 vm.zone_reclaim_mode=1 + 未绑 node:Redis 的内存被分配到某个 NUMA node 本地,该 node 内存紧张时内核**优先在本地做内存回收(reclaim)**而非跨 node 取,引发剧烈局部卡顿(几百 ms 毛刺),且这种行为在全局监控里不明显(top/vmstat 看的是整机均值)。排查:numactl --hardware 看 node 分布、cat /proc/sys/vm/zone_reclaim_mode 是否=1、numastat 看跨 node 分配/命中。治:① vm.zone_reclaim_mode=0(允许跨 node,避免本地 reclaim);② 更好:numactl --interleave=all 或绑单 node(numactl --cpunodebind=0 --membind=0)启动 Redis,让内存均匀或全在本 node,吃满本地性且不触发 reclaim。最忌”不绑 node + zone_reclaim=1”。
思考题 4(进阶总账): 综合第 12、13 章,给你五套画像 A~E 各一句话”核心矛盾 + 最该动的两个 vm 参数”,作为你的速查卡。
【参考答案】
- A 数据库/缓存:怕 OOM/怕抖动 →
swappiness=1+watermark_scale_factor=100(再配合 per-processoom_score_adj=-500、buffer pool 留余量)。 - B 文件/高吞吐写:写吞吐 vs 丢脏数据 →
dirty_ratio↑(如 40) +dirty_background_ratio↑(如 20)(赌”能重来”)。 - C 低延迟实时:零抖动 →
watermark_scale_factor=1000(极宽缓冲带,回收永远提前) +dirty_ratio↓(如 5)(绝不前台 sync),并关 THP、绑 CPU/NUMA。 - D 容器/混部:隔离靠 cgroup 不靠 vm → 宿主
overcommit_memory=0+swappiness=10;容器内别拧 vm,看 cgroupmemory.stat。 - E NUMA 大内存:跨 node 慢/局部 reclaim →
zone_reclaim_mode=0+numactl绑或 interleave(不绑 node + reclaim=1 是毛刺元凶)。
总纲:参数组合=匹配画像;先定画像,再选组合,且知道每项的赌注。
九、常见负载的参数冲突与架构化解
墨:第 12 章讲了 vm 参数之间的联动。这一节再升一层:有些目标在 vm 参数的”边界内”根本无法两全,硬调只会左右挨耳光——这种冲突,要靠架构设计在边界外化解。 这是”参数不是万能”的最高级表达。
冲突 1:高吞吐写 vs 零丢失(消息队列类)。 dirty_ratio 调大 → 写吞吐高,但宕机时丢的脏数据多;调小 → 丢得少,但前台 sync 卡顿、吞吐降。vm 只能二选一赌一边。像 Kafka/RocketMQ 这种”既要顺序写高吞吐、又丢消息即事故”的,靠架构化解:① 副本机制(消息复制到多 broker,单点宕机不丢);② producer acks=all(多数副本确认才返回成功);③ 自身刷盘/副本同步策略。vm 上它走”保守路线”(脏页压低、swappiness 低),把”零丢”交给副本与 ACK,把”快”交给顺序写天然高吞吐。结论:参数边界内的冲突,用架构在边界外化解。
冲突 2:低延迟 vs 大页收益(数据库类)。 THP defrag=always 自动吃大页(TLB 命中高),但同步 compaction 冻进程(第 18/23 章);关 defrag → 不冻,但丢 THP 收益。靠显式 HugeTLB 给 buffer pool 化解:预留 nr_hugepages,既吃大页收益又无 compaction 开销。
冲突 3:内存超卖(密度) vs 稳定(容器类)。 overcommit=1 超卖换部署密度,但 OOM 更猛更不可预测;=2 稳定但可能 malloc 失败起不来。靠 cgroup memory.max 分组隔离 + 程序优雅处理失败 化解:密度由分组限额保证,稳定由”该失败的提前失败”保证,而不是靠全局 overcommit 赌。
你:所以”参数解决不了的问题”,都该往架构层想?
墨:对。参数管的是”内核这个通用机制在你的负载下的行为”,它天然有取舍边界。当你的需求踩在边界内的冲突上,唯一的出路是把冲突拆到应用/架构层去平衡。 这也是为什么全书的母题是”通用机制 + 专家逃生口”——逃生口是给专家在机制内微调,而架构化解是在机制外重构。
十、监控 vm 的正确姿势(看哪些计数器)
墨:第 22 章讲过”别看 free 看 available”,这里给你一套完整监控 vm 健康的计数器清单,免得你告警又去盯 used。
/proc/vmstat 关键项(最该入库的):
allocstall(含allocstall_dma/allocstall_normal):直接内存回收次数。它的增量是 direct reclaim 卡顿的前兆,比任何内存百分比都先报警(第 18 章铁证之一)。compact_stall(含compact_stall_*):因碎片做同步 compaction 而卡住的分配次数。暴涨 = THP/大页碎片整理冻进程(第 18/23 章)。pswpin/pswpout:换页进/出页数。长期 >0 且跳动 = swap 颠簸(第 18 章)。pgpgin/pgpgout:页换入换出总计数(看 I/O 量)。numa_hit/numa_miss/numa_foreign:本地/跨 node 访问计数。numa_miss高 = NUMA 本地性破坏(第 23 章)。oom_kill:OOM 杀进程次数(第 19 章)。
/proc/meminfo 关键字段: MemAvailable(真话,别看 MemFree)、Active(anon/file) 与 Inactive(anon/file)(匿名 vs 文件、活跃 vs 可回收)、Dirty(当前脏页字节)、Slab(内核对象缓存)。
历史采集: sar -B(页换入换出/回收统计)、sar -W(swap 进出)、sar -r(内存含 kbavail)。压力失速: /proc/pressure/memory 的 full.avg60 > 1% 才告警(第 22 章 PSI)。
墨:把这套装进监控,你的内存告警就从”free 低就叫”升级成”allocstall 增量 + PSI.full + MemAvailable 趋势”三维判断——假告警和低内存真失速都能精准区分。
十一、监控告警误报:把 page cache 当”内存泄漏”——告警风暴淹没有问题
墨:第 13 章第十节给了”监控 vm 的正确姿势”,这一节用一个真实告警事故把它焊死:监控看错指标,比不监控更糟——它会让你对狼来了脱敏,真出事反而看不见。
📦 案例:某团队给所有机器配了"已用内存 > 85% 持续 5 分钟就告警"。上线后,一台文件服务每天告警几十次,值班疲于确认,直到某天真 OOM,告警被淹没在噪音里没人当真。
现象(第 0 层)
告警面板每天几十次”内存泄漏”告警,但业务从没真崩过。值班同学点进去看 free,used 确实长期 85%+,但 available(第 22 章)还有 30%+。top 里没单个进程吃内存异常。
推断 1:真有内存泄漏?(推错)
你:used 高嘛,先怀疑泄漏。valgrind/pprof 抓应用——没泄漏;看进程 RSS 总和,也没离谱。且 used 高得”稳定”,不像泄漏那样单调涨。排除应用泄漏。
命中:used 把 page cache 算进去了 + swappiness 太低让 cache 刚性
墨:根因两步:
- 看错指标:
free的used= 总内存 −free−buff/cache的剩余,但很多老监控直接采集used,而used把** page cache 也算作”已用”(因为free命令的used是MemTotal - MemFree - Buffers - Cached的近似,现代版本有available才准)。这台文件服务就是读大量文件、page cache 自然占满 80%+**,used自然高——这根本不是泄漏,是 cache 在正常工作(第 07 章)。 - 雪上加霜:前任为了”降延迟”把
vm.swappiness设成了 0/1,导致 page cache 一旦占满,内核几乎不换匿名页去腾 cache,cache 变成”刚性”的高占用,used长期顶在 85%,告警天天响。
调整
墨:两剂药:
- 告警改指标:把”内存告警”从
used改为MemAvailable(或 available 占比),并叠加 PSI 的memory.full.avg60 > 1%(第 22 章)作为”真失速”判据。MemAvailable高的机器,哪怕 cache 占 90%,也不是问题。 - 让 cache 可回收:把
swappiness从 0/1 提到 10(第 12、13 章讲的甜点),让内核在内存紧时有一点点换出余地,cache 不再刚性,used会随压力自然回落,告警噪音消失。 - 根因:文件服务本就该让 cache 多吃,告警阈值不该用
used。
改进:告警风暴平息,收口
墨:改指标 + swappiness=10 后,误报告警从每天几十次降到 0,真 OOM 时 PSI 告警立刻顶上来、被当真。复盘:监控 vm 的性命攸关一条——盯 available 和 PSI,别盯 used。used 高 + available 高 = “cache 正常工作,放心”;used 高 + available 低 + PSI.full 涨 = “真要 OOM,快去看”。这条把第 13 章第十节的监控清单,从一个建议变成了你亲手抓到的反面教材。
实战补遗一:脏页节奏与 swappiness 联动推错,Kafka 消费 lag 雪崩
墨:第 13 章讲过”画像 B(高吞吐写)该把 dirty_ratio 调大、画像 A(数据库)该调小”。这一节给一个把两个画像搞反、还叠加 swappiness 误调的真实事故——它把第 13 章的”匹配画像”和”参数联动”一次性焊成惨案。
📦 案例:某消息平台,Kafka broker 集群消费 lag 半夜周期性暴涨(从 0 飙到几百万),消费者明明有空闲,就是追不上。值班先怀疑消费者代码/分区不均,折腾两天无果。
补遗.1 现象与推错
你:lag 暴涨嘛,先怀疑消费者——是不是 Rebalance 频繁?是不是单分区热点?是不是下游写入慢?
墨:查 kafka-consumer-groups 的 CURRENT-OFFSET 推进——消费者确实在消费,但速度周期性断崖(每几分钟卡几秒不推进);查下游(写 HDFS/ES)延迟正常。不是消费者代码,是 broker 自身”卡了一下”。
补遗.2 第二推断:broker 磁盘写被冻住
你:broker 是顺序写日志(append-only,第 13 章说的”像画像 B”)。顺序写本该极快,为什么卡?查 broker 所在机器的磁盘:iostat 看到卡顿时写入 await 飙高、但 %util 不算满(第 20 章:%util 低也能是瓶颈,看 await/aqu-sz)。再查 vmstat 的 bo(写回块数)在卡顿时突变为 0——写被冻住了几秒。
补遗.3 命中:脏页节奏 + swappiness 双重误调
墨:对齐时间,看 dirty_ratio 和 swappiness:
- 这台 broker 之前被一个”数据库调优帖”动过:
vm.dirty_ratio=10(数据库画像 A 的值,第 13 章)、dirty_background_ratio=5。但 broker 是日志型顺序写(画像 B),脏页该”多攒快刷”才对(第 13 章画像 B 用 40/20)。dirty_ratio=10太低 → broker 一有突发写入就触顶前台 sync,整个刷盘线程被冻几秒,lag 因此断崖。 - 叠加
vm.swappiness=1(也是 DB 帖带来的):突发写时 page cache 紧张,内核不换匿名页、只能回收干净缓存或 direct reclaim,又引入额外卡顿,和脏页冻写叠加。
一句话:把”数据库画像 A”的参数,抄到了”日志画像 B”的 broker 上,还顺手把 swappiness 也按 DB 调了——第 13 章”抄错画像”+“联动推错”的合体。
补遗.4 调整
墨:两剂药,都回到画像 B 的本分:
vm.dirty_ratio = 40+vm.dirty_background_ratio = 20(第 13 章画像 B 组合,脏页多攒快刷,前台不被逼 sync);vm.swappiness = 10(不是 1,给内核一点点余地,避免 page cache 被挤引发的附加回收);- 根因:“broker 是画像 B,不能用 DB 的画像 A 参数”——这张标签,从今往后贴在每台机器上。
补遗.5 改进:lag 归零,收口
墨:改回画像 B 组合后,消费 lag 的周期性断崖消失,稳定追平。复盘:这是第 13 章”总纲:参数组合=匹配画像”最痛的一课——Kafka broker 的”顺序写高吞吐”,本质就是画像 B;你把它当数据库(画像 A)调,等于让一个长跑运动员按举重运动员的饮食表吃饭。再叠加 swappiness 的联动误调,雪崩是必然。排障口诀:消息/日志类中间件,先看它是画像 B 还是 A,再决定 dirty 和 swappiness 怎么配;抄 DB 帖调 broker,是这本书点名批评的”对一半错一半”。
实战补遗二:内存密集服务的 vm 组合拳——以 Redis 类缓存为例走一遍
墨:你要在虚拟机上跑一个 Redis 缓存,老板说”别让它被 OOM 杀、也别让它 swap”。你一口气把 swappiness=0、overcommit=1、transparent_hugepage=never 都设了。我说:慢着,这里面有个会反噬的。
你:这不都是”保护内存”的标准操作吗?
墨:swappiness=0 + overcommit=1 在 Redis 上是个经典组合拳,但顺序和配套没做对,会反噬。我带你以 Redis 为例,把第 13 章”场景化”真走一遍。
场景:VM 上的 Redis,要做”不 swap、不被 OOM”
Redis 是纯内存 KV,理想状态:所有数据在内存、不 swap、宿主机 OOM 时它不是首选冤死鬼。
组合拳逐参数推
- **
vm.overcommit_memory=1(总是允许 overcommit):**Redis 启动fork子进程做bgsave/AOF rewrite时,子进程用 COW 共享父内存,理论不额外占内存,但内核的 commit 会计在=0(默认启发式)下可能因”看起来要翻倍”而拒绝 fork →bgsave失败。设=1让 fork 必成功。但代价:整机可能 overcommit 到真 OOM,因为=1不做任何会计——所以必须配memory.max(cgroup)或admin_reserve_kbytes兜底,否则 Redis 自己也可能被真 OOM 杀。 - **
vm.swappiness=1(几乎不 swap,但留一线):**Redis 数据不该 swap,=0太硬(连紧急回收都不做,可能直接 OOM),=1是社区对 DB 类推荐值——只在极度内存压力时才 swap 一点,避免硬 OOM。注意:=0在老内核上”绝不 swap”,新内核=0仍有特殊回收路径,语义有坑,故用=1更稳。 - **
transparent_hugepage:**Redis 短命小对象多,THP 合并反而增加 TLB 管理开销 + 可能khugepaged毛刺(第 18/23 章),延迟敏感设never或madvise。 - **
vm.min_free_kbytes:**给整机留原子分配余量(网卡/中断),Redis 自己不耗,但同机其他进程(如网卡软中断)受益,避免丢包。 - **
vm.admin_reserve_kbytes:**OOM 时还能ssh上去查 Redis 为啥挂(第 14 章)。
真实坑:只设 swappiness=0 不设 overcommit,bgsave 偶发失败
某 Redis 设了 swappiness=0 但 overcommit=0(默认)。平时稳,但数据量大到一半内存时,bgsave 的 fork 被内核启发式拒绝(Cannot allocate memory),备份断断续续。运维以为是内存不够,加内存后才发现是 overcommit 会计——改 overcommit=1 + cgroup memory.max 兜底,bgsave 稳。
处方(组合拳,不是单点)
overcommit_memory=1+ cgroupmemory.max(或admin_reserve)兜底;swappiness=1;- THP
never(延迟敏感)或madvise; min_free_kbytes留余量;- 全部持久化到
sysctl.d/99-redis.conf(呼应第 0 章顺序坑)。
墨叔一句
参数要”组合”看,别”单点”调。 swappiness=0 单独看是保护,配错 overcommit 就反噬。第 13 章的核心就是:场景决定组合,组合决定安全。
🔧 思考题:为什么 Redis 推荐 swappiness=1 而不是 0?=0 在什么内核上反而会”该 swap 时不 swap 导致 OOM”?
【参考答案】
swappiness=1 给紧急回收留一线:极端内存压力下做极少量 swap,避免直接 OOM 杀 Redis(对缓存而言,OOM 杀 = 全量击穿数据库,比慢一点 swap 更惨)。=0 在老内核语义是”绝不 swap”,但某些场景(如匿名页无处回收)会跳过所有缓和手段直接 OOM;新内核(≈3.5+)对 =0 放宽了回收路径,但文档仍建议 DB 用 =1 取”几乎不 swap 但别硬 OOM”的平衡。所以 =1 是”保护 + 留后路”,=0 是”硬刚、可能反噬”。这也体现组合拳思维:单看 =0 更”保护”,但配合 Redis 的 fork/极端压力,=1 整体更安全——参数从不是孤立的”越小越安全”。
实战补遗三:网络密集服务的 vm—以 Envoy/Nginx 网关为例走一遍
墨:第 13 章用 Redis 讲了内存密集的组合拳。网络密集的网关(Envoy/Nginx)呢?它的 vm 该怎么守?是不是和 Redis 差不多?
你:都是服务,差不多吧?swappiness 调 1、overcommit 调 1?
墨:差很多。网关的命门是**“不丢包、不卡转发”,它不自己吃内存(主要转发),但网卡 DMA 要原子分配**——所以 min_free_kbytes 对它是生死线,而 overcommit/THP 反而不那么关键。我带你走一遍。
场景:VM 上的网关,要”高并发不丢包、低延迟转发”
网关转发海量短连接,内存占用中等(主要是连接表/缓冲区),CPU 和网络是瓶颈,磁盘几乎不碰。
组合拳逐参数推
vm.min_free_kbytes调大(关键中的关键):网卡收包靠 DMA 把帧放进内核skb缓冲区,这要原子分配(不能等回收)。min_free_kbytes太小 → 原子分配失败 → 网卡丢包(ethtool -S看rx_missed/rx_drop)、转发延迟抖。网关必须留够(数百 MB~1G,按网卡带宽/队列定)。这呼应第 14 章”min_free 太小连网卡都丢包”。- **
vm.swappiness=1:**转发缓冲区不该被换出,换了转发就卡。但网关内存占用中等,=1足够(不用0的硬刚)。 - **
transparent_hugepage=never:**网关延迟敏感,THP/khugepaged毛刺(第 18/23 章)要避免。 - **
overcommit_memory:**网关不fork大进程(Go 网关不靠fork),=0默认即可,不必=1(Redis 那种 fork 场景才需要)。 - 其余(
dirty_*):网关几乎不写盘,默认即可。
真实坑:只调 somaxconn 不调 min_free_kbytes,网关仍丢包
某 Nginx 网关调了大 somaxconn(第 15 章),但 min_free_kbytes 默认 64M。高并发时网卡 DMA 原子分配失败,rx_drop 涨,客户端超时。运维只盯 tcp 参数,没看 min_free——调大 min_free 后 rx_drop 归零。
处方(网关组合拳)
min_free_kbytes留够(网卡不丢包)← 最关键;swappiness=1+ THPnever(转发不卡);overcommit=0默认(不必 1);- 全部持久化
sysctl.d/99-gateway.conf。
墨叔一句
同样的 vm 参数,在 Redis 和网关上”谁最关键”完全不同。 这就是场景化:先定”你的命门是哪一级”,再守那级的参数。
🔧 思考题:网关调 min_free_kbytes 大,会牺牲什么?这个值是不是越大越好?
【参考答案】
min_free_kbytes 大 = 从”可用内存池”永久扣掉一块当”原子分配 reserved”,等于整机可用内存变少,其他进程更早触发回收/OOM。不是越大越好:过大(如 4G)在 16G 机器上把可用内存砍 25%,纯浪费(网卡根本用不到 4G reserved)。按”网卡带宽 × 缓冲区需求”定:万兆网卡 + 多队列,数百 MB~1G 通常够;调大后用 ethtool -S 看 rx_drop 是否归零来验证”够不够”,别无脑堆。这也体现组合拳思维:min_free 是网关的命门参数,但也要权衡它对整机可用内存的占用——第 12 章画像里”网络密集”那栏,min_free 排第一不是白排的:它守的是”网卡收包”这一级,而这正是网关的关键路径。守错级(去调 swappiness/overcommit)或守过量(把 min_free 堆到 4G),都是组合拳没打对。
实战补遗四:Go 程序员的专属坑——THP 与 GC 的”暗中作对”
墨:你是 Go 程序员,前面讲的 vm 参数里,有两个对你来说是”专属陷阱”:透明大页(THP)和 vm.swappiness。别的语言也许无所谓,Go 的运行时模型和它们会暗中作对。先说 THP。
你:Go 不用手动管内存啊,Go 自己有 GC,THP 跟它有什么关系?
墨:关系大了。THP 是内核层”自动把 4K 页合并成 2M 大页”,而 Go 的 runtime 自己管理堆——它用 mmap 向内核要一大片匿名内存,自己切成 span、自己 GC 回收。两层内存管理器叠在一起,会出怪事:
- GC 时的”假缺页风暴”:Go GC 是 mark-sweep,要扫描整个堆。如果堆被 THP 合并成了 2M 大页,GC 扫描触发大量”首次访问大页”的缺页(大页未真正分配物理页时,访问它要补一次大页分配),GC 暂停时间(
STW虽短,但 mark 阶段的用户 goroutine 仍会被缺页拖慢)出现偶发尖刺。 khugepaged后台合并的卡顿:前面第 18、23 章说过,khugepaged搬内存合并大页时会短暂持锁、冻结相关页。Go 程序堆大、访问密,正是它最爱”照顾”的对象——结果就是 Go 服务出现几十毫秒的无规律卡顿,pprof里看不出业务原因,因为卡在内核合并大页。
真实事故:一个 Go 网关,P99 延迟偶发从 5ms 跳到 60ms,毫无规律,pprof/业务日志都干净。最后 cat /proc/vmstat | grep thp 看 thp_fault_alloc(THP 分配次数)在卡顿时刻暴涨,才锁定 THP。
调整:对 Go(以及 JVM、Redis 这类自管内存 + 要稳延迟的服务),关 THP,用手动大页或纯 4K 页:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag再观察:P99 卡顿尖刺消失,稳定在 5–6ms。
再说 swappiness:前面第 0 章讲了”设 0 翻车”。对 Go 额外要命的是——Go runtime 把堆当匿名内存,swappiness 高时内核会优先把”最近没访问的 Go 堆页”swap 出去(因为 GC 的 mark 阶段之外,很多堆页确实”最近没碰”)。等下次 GC 或业务访问这些页,从 swap 读回来,延迟爆表。所以 Go 服务 swappiness 设 1(留兜底)是共识,千万别设 0(第 0 章翻车教训),也别留默认 60。
你:那 Go 程序要不要用手动 HugePages 提 TLB 命中率(第 3 章说的)?
墨:可以,但要 Go 配合。Go 1.14+ 支持 GODEBUG=madvdontneed=1 等微调,且可以用 mmap 自己管理大页区给特定热数据结构。但多数 Go 业务不上大页、只关 THP 就够稳了——因为 Go 堆访问分散、局部性不如数据库缓冲池,大页的 TLB 收益没那么夸张,反而 THP 的合并卡顿副作用是实打实的痛。权衡:
| 选择 | 对 Go 的收益 | 对 Go 的风险 |
|---|---|---|
| THP 开 | 几乎无(堆访问分散) | khugepaged 卡顿、GC 缺页尖刺 |
| THP 关 | 失去一点理论 TLB 收益 | 消除偶发卡顿,延迟稳 |
| 手动 HugePages | TLB 命中提升(热数据明显) | 要改代码 mmap、管理复杂 |
🔧 思考题:你有个 Go 服务,堆 30G,延迟敏感。运维说”按惯例开 THP”,你该怎么用一条证据说服他关掉?
【参考答案】
拿 thp_fault_alloc/thp_collapse_alloc 和卡顿时间的关联当证据:在卡顿窗口前后 cat /proc/vmstat | grep -E 'thp_fault_alloc|thp_collapse_alloc',如果卡顿时刻这两个计数飙升,且 khugepaged 进程(top 看 %sys 高、或 /proc/<khugepaged>/status 的 voluntary_ctxt_switches 活跃)在忙,就证明卡顿来自内核合并大页,不是你的代码。再补一刀:Go 官方和 Red Hat、Datadog 等的生产建议都是”对 GC 型语言运行时关 THP”,这是行业共识不是个人偏好。
关掉后复测 P99:如果尖刺消失、均值回归,就坐实了。最后给个替代方案(显式 HugePages 给真正的超大热结构,或纯 4K 页),别只说”关”,显得你懂取舍。这样运维没法用”惯例”压你,因为你的证据是机器自己吐的数字,不是论坛帖子。
实战补遗五:容器里改 vm 参数”看不到效果”——因为那是宿主的旋钮
墨:第 13 章讲了五套负载画像的 vm 参数组合。但这里有个 Go 程序员在容器时代必踩的坑:你在 pod 里 sysctl -w vm.swappiness=1,以为给这个服务设好了,其实你改的是宿主全机的。
你:容器不是隔离的吗?为什么 vm 参数能穿透到宿主?
墨:因为 vm.*(以及大多数 net.* 内核参数)是内核全局的,而容器共享宿主内核——容器没有自己的内核,所以 sysctl -w 改的就是那一个内核的全局值。换句话说:
- 你在 pod A 里设
vm.swappiness=1,pod B、pod C、宿主上所有进程全部变成 swappiness=1; - 你在 pod A 里设
vm.dirty_ratio=5,整台机器所有进程的脏页池都变浅。
这不是”隔离”,是”一户人家的电闸,谁都能拉,拉了全楼黑”。
真实事故:一个团队在两个 pod 里分别按”自己服务的负载性格”设 vm 参数——pod A(数据库,延迟型)设 dirty_ratio=5,pod B(日志,吞吐型)设 dirty_ratio=40。两个 pod 在不同时间被调度、各自 sysctl -w,最后谁后设谁赢:若 B 后设,数据库 pod A 也跟着 dirty_ratio=40,脏页池变深,前台写开始卡(第 14 章的事故复现);若 A 后设,日志 pod B 吞吐掉(第 14 章另一个事故)。两个服务互相踩,且都没报错。
推断:容器里”改了 vm 参数没生效 / 或生效了但别的容器变怪”——根因是 vm 参数是宿主级,容器间不隔离。验证:在 pod A 里 sysctl -w vm.swappiness=1,然后 ssh 到宿主、sysctl vm.swappiness 看也是 1,再到 pod B 里看也是 1——证实全机共享一份。
调整(正确姿势):① vm 参数统一在节点 init 阶段设好(节点初始化脚本 / sysctl.d/99-node-tuning.conf),不靠容器运行时去改;② 节点按”这台机器主要跑什么负载”设一个折中且安全的全局值(如 swappiness=1 对所有服务都安全、dirty_ratio 取 10 这种通用值);③ 容器侧绝不手动 sysctl -w vm.*(K8s 默认也不允许,除非 --privileged,那是安全违规);④ 真正的”容器级内存性格”用 cgroup(memory.max/memory.high/memory.swappiness 部分支持)去表达,而不是 sysctl。再观察:节点统一设好、容器不碰 vm,两个 pod 不再互相踩,各按 cgroup 限额稳定运行。
你:那有没有”容器级”的 vm 旋钮?cgroup 能管内存回收策略吗?
墨:部分能,cgroup v2 提供了有限的内存回收控制,但和 sysctl 不是一回事:
memory.swappiness(cgroup v2 支持!):可以给单个 cgroup 设 swappiness,且不会穿透到宿主(它 scope 在该 cgroup 内)。这是少数”容器级”的 swappiness 旋钮——和全局vm.swappiness共存,但只约束该 cgroup 的回收倾向。注意:它控制的是”该 cgroup 内回收时匿名页 vs 文件页的偏好”,不是全局开关。memory.high/memory.max:软/硬内存上限(第 17 章),间接影响”这个 cgroup 内存紧时怎么回收”(超限就节流/杀,而非去动全局 vm)。- 但
dirty_ratio/dirty_background_ratio/vfs_cache_pressure这些没有 cgroup 版——它们是全局的,只能节点级设。一个 cgroup 写脏页,仍受全局脏页池约束,这就是”混部机型要压低全局 dirty_ratio”的原因(第 14 章讲过)。
所以结论:少数 vm 参数有 cgroup 对应物(如 memory.swappiness),多数没有,只能节点级统一设。容器时代调 vm 的正确层级是”节点 init”而非”pod 运行时”,这和前面 zone_reclaim_mode、read_ahead_kb、IRQ 亲和、sysctl 加载是一脉相承的——分清”节点级 / cgroup 级 / 进程级”作用域,是所有内核调优的命门。
🔧 思考题:K8s 里 pod.securityContext.sysctls 能设哪些?为什么 vm.swappiness 不在默认白名单里?
【参考答案】
K8s 把 sysctl 分两类:
- 安全可容器化(sysctl.unsafe=[] 白名单):如
net.ipv4.ip_local_port_range、net.ipv4.tcp_keepalive_time、kernel.shm*。这些要么 per-namespace/per-netns 有隔离语义,要么影响范围可控,允许 pod 通过securityContext.sysctls声明。 - 不安全(默认禁止):
vm.swappiness、vm.dirty_*、kernel.*大多数——因为它们改的是宿主全局内核状态,无命名空间隔离,一个 pod 改了全节点所有 pod 受害。K8s 出于多租户安全,默认禁止 pod 设这些,必须管理员显式加进sysctl.unsafe白名单才放行(且放行即代表你接受”它影响全节点”)。
vm.swappiness 不在白名单,正是因为它穿透宿主、破坏隔离——K8s 的设计哲学是”容器不该能拉全楼电闸”。真想按 pod 调 swappiness,用 cgroup v2 的 memory.swappiness(有 scope、不影响别人),而不是 sysctl。口诀:K8s 的 sysctl 白名单本质是按”有没有命名空间隔离”划分的——有隔离(per-pod 生效)才安全放行,无隔离(改全局)默认禁止。遇到”pod 里 sysctl -w 报 permission denied”,先想”这个参数是不是根本就不该在 pod 里设”,而不是去加 unsafe 白名单(加了等于给 pod 开了拉电闸的权限)。
实战补遗六:套 vm 模板——数据库 / 缓存 / 消息队列该分别怎么设
墨:老哥,你给一台机器调 vm 参数,会”一套模板打天下”吗?MySQL、Redis、Kafka 用同一组 swappiness、dirty_*?
你:……之前确实差不多,都是套的”服务器通用模板”。
墨:这就是坑。不同负载对内存的”脾气”完全不同,一套模板必然有人吃亏。我给你三套真实模板,讲清为什么这么配,而不是丢你三个数字。
现象:同规格机器,A 跑 MySQL、B 跑 Redis、C 跑 Kafka,用同一套”swappiness=1, dirty_ratio=10”模板。结果 MySQL 偶发写卡、Redis 内存回收毛刺、Kafka 消费 lag 周期暴涨(这个在 13 章脏页联动那节讲过,这里补全三套对照)。
推断:既然都是”内存型服务”,一套模板应该都合适。
可能推错:三类负载的”内存访问模式”天差地别——MySQL 有大量随机小写下盘 + 依赖 page cache 加速读;Redis 是纯内存、几乎不依赖 page cache、但 fork 多;Kafka 是顺序大块写 + 顺序读、重度依赖 page cache 和脏页缓冲。一套参数不可能同时讨好三者。
调整:拆成三套:
① MySQL(事务库)
vm.swappiness=1:库要稳,别偷偷换页。vm.dirty_ratio=15、vm.dirty_background_ratio=5:库自己有 redo log/双写缓冲,脏页别堆太多,否则 checkpoint 时集中回写卡 IO。vm.overcommit_memory=0(默认):库对 fork 不敏感,默认即可;但注意vm.overcommit_ratio留够。- 依赖 page cache 加速热数据读,所以别把 swappiness 设 0 反而让 cache 被过早回收(0 会让内核更激进回收 file cache 来避免 swap,有时反而伤读)。
② Redis(内存 KV)
vm.overcommit_memory=1:Redis 做 BGSAVE 要 fork,必须允许 overcommit,否则 fork 因内存检查失败直接崩(这是 Redis 官方明确要求)。vm.swappiness=1:Redis 数据绝不该进 swap,进了就慢死。- THP 设
never(fork COW 粒度,详见第 12 章):避免 fork 后拷贝量暴涨。 - 不依赖 page cache(数据全在自身内存),所以
vfs_cache_pressure可略调高回收 dentry/inode 缓存,省给 Redis。
③ Kafka(消息队列)
vm.dirty_ratio反而可以调大(如 20~40):Kafka 是顺序写、靠脏页缓冲把随机写聚成顺序写,脏页多反而吞吐高;但配dirty_expire_centisecs别太长,避免 broker 故障丢太多未落盘消息。vm.swappiness=1:同理别换页。- page cache 是 Kafka 的命根(消费端大量顺序读命中 cache),所以绝对别设 swappiness=0 去过度回收 file cache;
vfs_cache_pressure保持默认或偏低。 vm.min_free_kbytes留够,防网卡 DMA 原子分配失败丢包(详见第 12 章)。
再观察:同一模板打三家的根本问题是”把 swappiness=0 当万能药”——对 Kafka/MySQL 读缓存反而是毒药。正确的”脾气”是:谁依赖 page cache,谁就不能激进回收 file cache。
改进:按负载类型选模板;overcommit_memory 和 THP 跟 fork 模型强相关(Redis 要 1+never,MySQL/Go 可 madvise);dirty_* 跟”写模式”强相关(Kafka 顺序写可大,MySQL 随机写要小)。
🔧 思考题:为什么”swappiness=0”对 Kafka 和 MySQL 可能有害?它到底是”禁 swap”还是”禁回收 file cache”?这两个动作是一回事吗?
【参考答案】
这是个流传极广的误解。swappiness 控制的是”内核在回收内存时,更倾向于换出匿名页(swap)还是回收文件页(page cache)“的权重,取值 0~100。设 0 是”极端不愿意 swap”,但副作用是:当内存压力出现、又几乎没 swap 可换时,内核为了满足分配,会更激进地回收 page cache(file-backed 页)。对 Kafka/MySQL 这种重度依赖 page cache 加速读的服务,cache 被过早回收 = 热数据读命中率掉 = 磁盘 IO 暴涨 = 延迟抖。所以它们要的是 swappiness=1(几乎不 swap,但保留回收 file cache 的余地),而不是 0。
注意 swappiness=0 在较新内核(3.14+)语义已变:0 表示”除非完全没有 file 页可回收才 swap”,较温和;老内核 0 是真”绝不 swap 除非 OOM”。所以”设 0 有害”在老内核更明显,新内核影响小但仍不推荐用于缓存依赖型服务。
区分两个动作:
- 换出匿名页(swap):把进程堆/栈挪到磁盘,访问时巨慢。Redis/库都不想要。
- 回收文件页(page cache):清掉可重建的缓存,下次读要走磁盘。Kafka/MySQL 读依赖它,回收太狠伤读。
swappiness 同时影响两者倾向,所以它不是单纯”禁 swap 开关”,而是”swap vs cache 回收的旋钮”。正确姿态:缓存依赖型服务用 1(保 cache 回收能力),纯内存且怕 swap 的服务(Redis)用 1 配 overcommit_memory=1+THP never。没有万能值。
下一章预告
墨:第 12、13 章把”vm 参数是什么”和”vm 参数怎么配”讲透了。但纸上得来终觉浅——第 14 章我们上真实事故:4 个 vm 相关的线上故障,每个走完整”现象→推断→可能推错→调整→观察→改进”循环。你会看到 overcommit=2 把服务卡得起不来、swappiness=0 反而更抖、min_free_kbytes 太小连网卡都丢包、drop_caches 定时脚本把 I/O 打爆。这一章是把前面两章的”知识”变成”肌肉记忆”的地方。
本章完。vm 实战见第 14 章。