
1. 先把 vm 这个词掰开——调优之前必须对齐的三件事聊 Linux vm 内核参数调优第一件要做的事不是背参数而是统一认知。我这里说的 vm指的是内核里的虚拟内存子系统也就是/proc/sys/vm/这一整棵目录树下的可调旋钮而不是某些人第一反应想到的虚拟机管理程序。这两个东西在搜索时经常被混在一起导致很多人搜vm 参数调优出来一堆和 KVM 相关的内容方向直接跑偏。这篇内容只谈内存管理子系统的参数涉及虚拟化宿主时会单独说明它的特殊性。这套参数解决什么问题简单说它决定了内核在内存不够时先回收什么、脏数据什么时候往磁盘刷、进程申请内存时内核是立刻给还是先开空头支票、大页要不要自动启用。调好了同样一台机器能扛住更高的并发、更平稳的 IO 抖动调歪了轻则周期性卡顿、重则 OOM 杀进程甚至整机失联。它适合谁看我觉得分三类一是负责一批线上服务器的运维和 SRE日常被内存告警和 IO 毛刺折磨的二是做后端服务开发、需要判断这到底是我的程序问题还是系统配置问题的工程师三是自己折腾开发机、容器宿主和家用小主机的爱好者。前两类关乎责任事故第三类关乎心情都很重要。我见过太多团队把调优做成了抄作业从某篇几年前的博客里复制一段sysctl.conf五六个参数一把梭进去重启之后也没人再管。问题在于那些数值往往是为某种特定硬件大概率是机械盘和某种特定负载大概率是文件服务器准备的搬到今天一块 NVMe 上、跑着一个内存数据库可能完全是反向优化。所以下面我讲的不是背下来这几个值而是每个值背后在算什么账。1.1 vm 子系统在管什么边界在哪从内核视角看虚拟内存子系统负责的事情比大多数人想的要宽。它管物理页框的分配与回收伙伴系统、管匿名页和文件页的取舍LRU 链表、管脏页的回写节奏flusher 线程、管内存映射mmap 与页表、管交换swap 与 zswap、管内存压缩与大页compaction、THP。你调/proc/sys/vm/下的任何一个值影响的都是这条链路上的某一环。但它的边界也很清楚很多你以为是内存问题的现象其实不归它管。比如 cgroup 里的容器被 OOM 杀掉那是 memory cgroup 的memory.max触发的vm.*这些全局参数根本不参与决策再比如 Java 堆内的 GC 卡顿那是应用自己的内存管理跟 swappiness 没半毛钱关系。搞清楚这条边界能省掉大量无效调优——我个人的经验是接到内存有问题的工单先分清楚是全局内存压力、cgroup 限额、还是应用自身的问题三类走完全不同的处理路径vm.*只对第一类直接有效。还有一点容易忽略vm.*大多数参数是全局的一台机器上一个值所有进程共享。你在容器里echo一个值进去要么写不进去只读挂载要么改了宿主机的全局行为影响其他容器。这是容器时代做内存调优最容易踩的地雷后面会单独展开。1.2 默认值不是最佳值而是最少惊讶内核维护者选默认值时考虑的是通用性和安全性不是性能。vm.swappiness 60这个数在二十年前设计时目标是在内存吃紧的桌面场景下让交换及时发生避免用户界面卡死vm.dirty_ratio 20是为了保证脏页不会长期堆积导致数据丢失窗口过大。这些默认值在绝不惹事这个维度上是优秀的在跑得最快这个维度上则基本没什么追求。反过来说这也意味着很多默认值其实相当保守给调优留了很大的空间。一块能跑几百 MB/s 持续写的 NVMe让内核攒到 20% 内存的脏页再一次性刷出去那个瞬间的 IO 洪峰完全没必要。反过来某些值在某些内核版本上又偏激进比如透明大页默认always曾让无数数据库出现几十毫秒级的延迟尖峰。所以我的态度一直是默认值是一个安全的起点不是一个可以闭眼信任的终点但你要改它就得知道自己为什么改。1.3 一条铁律先观测再动手一次只改一个这条我放在最前面强调因为它决定了后面所有工作的价值。调优的本质是针对可测量的瓶颈做定向修正不是把所有看起来更激进的参数都往上调。我定下的规矩是任何一次参数变更前必须有一份基线数据变更后必须有同等时长的对照数据一次只改一个参数或者一组强相关参数其他的不动。基线数据具体看什么后面第 6 章会列清单。这里先说一个反直觉的点大多数机器的内存参数其实不需要调。真的。一台跑着普通 Web 服务、内存使用率 40% 上下、没有明显 IO 毛刺的机器你把vm.*翻个底朝天也不会快 1%。调优的收益只在存在真实瓶颈时才显现没有瓶颈时的调优纯粹是在给自己制造风险。我见过有人把swappiness设成 0 之后机器在内存压力下不进 swap、直接触发 OOM killer杀掉了一个本来只是慢一点的核心服务——这就是典型的无病吃药吃出病来。2. 脏页回写四件套绝大多数 IO 抖动都出在这里如果只能挑一组参数来讲我会毫不犹豫选脏页回写这组。原因很实在我在实际排查中遇到的IO 莫名其妙周期性飙高写入延迟有规律地抖动磁盘利用率间歇性打满这类问题十有七八跟这四个参数有关而且它们之间的相互作用非常微妙光看单个参数的文档说明是看不懂的。2.1 四个参数各自的角色别搞反了先明确脏页是什么。进程调用write()往文件里写数据内核不会立刻同步落盘而是先写进 page cache 的页里把这个页标记成脏。这个设计是整条 IO 路径性能的来源——顺序写变成延迟写、小写合并成大写——但它也意味着内存里随时躺着不知道多少还没落盘的数据。回写writeback就是把这些脏页刷到块设备上的过程。四个核心参数参数默认值作用调错后的典型症状vm.dirty_background_ratio10脏页占可用内存超过此比例后台 flusher 开始异步回写调太大 → 回写启动太晚最终被迫同步vm.dirty_ratio20脏页占比超过此值写进程被阻塞变为同步回写调太大 → IO 洪峰调太小 → 写入吞吐崩vm.dirty_expire_centisecs3000脏页存活超过 30 秒后后台线程必须写回调太大 → 数据丢失窗口变长vm.dirty_writeback_centisecs500后台 flusher 线程每隔 5 秒醒来一次检查调太大 → 脏页检查不及时关键要理解的是两个阈值构成两级触发机制。脏页占比越过dirty_background_ratio后台开始慢慢刷此时应用完全不受影响只管继续写占比继续涨到dirty_ratio内核就在balance_dirty_pages()里把这几个进程的write()卡住强制它自己参与回写。所以dirty_ratio实际上是缓冲区满了的红灯红灯一亮应用的写入延迟立刻从微秒级跳到毫秒甚至秒级。这就解释了一个很常见的现象一台机器写入压力大脏页一路涨到 20%然后应用写入被卡住内核开始猛刷脏页迅速掉下来应用恢复再涨到 20%再卡。表现出来就是周期性的写入延迟尖峰。你去看监控磁盘 util 每隔十几秒冲到 100%其他时间接近空闲非常规律。2.2 ratio 还是 bytes具体怎么选怎么算从内核 2.6.30 起这组参数多了一个以字节为单位的版本vm.dirty_background_bytes和vm.dirty_bytes。它们和 ratio 版本是互斥的——你设置 bytes 版本对应的 ratio 会被自动置 0反之亦然。这个设计经常让人踩坑写了 bytes 之后忘记了后来看 ratio 显示 0以为出问题了。那到底选哪个我的判断标准是内存规格差异大的机器用 bytes规格统一的机器用 ratio 也行。道理很直白ratio 是相对值同样的 20%在 8GB 机器上是 1.6GB在 256GB 机器上是 51GB。后者意味着内核可以攒 51GB 脏页再开始卡进程这在任何存储设备上都是灾难。bytes 是绝对值可控性强得多。具体数值怎么定我一般用一个设备带宽 × 期望回写窗口的粗算公式dirty_bytes ≈ 存储设备持续写带宽 × 期望一次性回写耗时 dirty_background_bytes ≈ dirty_bytes / 3 ~ dirty_bytes / 2举例一块持续写能跑到 800MB/s 的 NVMe我希望每次集中回写控制在 2 秒内完成那就是 800MB/s × 2s ≈ 1.6GB取vm.dirty_bytes 1610612736。后台阈值取 1/4 到 1/3也就是 400~500MB写成vm.dirty_background_bytes 536870912。提示机械盘和老式 SATA SSD 的随机写能力远低于顺序写按上式计算时建议把有效带宽打成三折甚至更低取个保守值。设备真实带宽用fio压一遍最准别信理论峰值。这套定法的逻辑是把回写打散成多次小批量而不是攒到临界点来一次大的。代价是后台线程会更频繁地工作但这些开销在现代存储上是可接受的换来的是写入延迟的方差显著变小——对在线服务来说延迟稳定性的价值远高于平均吞吐。2.3 一个真实的调优过程记录说个我印象比较深的案例。某台跑批量数据处理任务的机器配置是 128GB 内存、一块企业级 NVMe任务本身是每隔一段时间生成大批临时文件。监控上一直有个怪现象任务运行期间每 20 秒左右出现一次 1~2 秒的写入完全停顿任务整体耗时比预期多出三成。看/proc/meminfo就能定位Dirty字段一路爬到四十多 GB 然后断崖式下跌Writeback同步冲高。这不就是标准的攒够了一次性刷么。算一下默认dirty_ratio 20128GB 内存的 20% 就是 25.6GB但因为dirty_background_ratio 10也是 12.8GB所以后台应该在 12.8GB 就开始刷了。问题在于后台刷的速度赶不上应用写的速度应用写内存是真的快脏页继续涨到阈值然后就是暴力刷盘。改动方案把 ratio 换成 bytesdirty_background_bytes设 512MBdirty_bytes设 2GB。改完之后Dirty曲线变得平缓稳定在 1GB 上下小幅波动写入停顿消失任务耗时回到预期。这里的关键不是把值调小了而是把回写的触发点提前到了一个 flusher 真的能跟得上的量级让异步回写这件事真正异步起来。顺带说一个容易忽略的细节dirty_expire_centisecs单位是 1/100 秒默认 3000 就是 30 秒。这个值的含义是一页脏数据最多能在内存里躺 30 秒。凡是挂了业务的机器这个值我不建议往大调它直接决定了一旦掉电你会丢多少数据。真要为了极致的写吞吐那得靠应用层的批量提交设计而不是靠把落盘窗口拉长。3. swappiness 与内存回收被误解最深的一组参数vm.swappiness大概是全网被抄得最多、也被误解得最彻底的一个参数。网上有一种流传极广的说法是把它设成 0 就能禁用 swap这是彻头彻尾的错误——设成 0 之后内核依然会在内存严重不足时使用 swap只是把使用倾向降到最低。还有说法称服务器就该设 1这个 1 是怎么来的恐怕连传播者自己也说不清。3.1 swappiness 到底在调节什么准确的说法是它调节的是内核在回收内存时回收匿名页和回收文件页这两条路径之间的相对权重。匿名页就是进程malloc出来的堆内存没有对应的磁盘文件要回收只能先写到 swap 里文件页就是 page cache背后有文件回收时如果内容干净可以直接丢弃脏的话写回即可。swappiness越高内核越倾向于把匿名页挪到 swap 腾出空间越低越倾向于丢弃 page cache。这两个选择各有代价压低 swappiness 意味着在内存紧张时页缓存被大量丢弃原本命中的缓存全失效下次读文件全走磁盘你可能突然发现磁盘读 IO 飙高——那是缓存被清空后的回填而 swappiness 偏高意味着活跃进程的匿名页被换出等它下次访问时再换回来产生缺页延迟major fault对延迟敏感的服务是致命打击。所以正确的选择依据不是要不要用 swap而是这台机器上page cache 和进程工作集哪个更值钱。我的一般性判断关系型数据库这类自带大块共享内存缓冲区的服务进程工作集是它的命根子page cache 的收益相对小swappiness设 1~10 都合理文件服务器、静态资源服务、构建机这种高度依赖缓存命中的page cache 就是性能本身swappiness也可以压低让匿名页顺利换出腾位置内存严重超额、靠 swap 撑着的机器比如一堆小容器挤在一台机器上反而不能把 swappiness 压得太低否则内核被迫去清缓存容器之间互相拖累。注意swappiness只在系统存在可用 swap 时才真正起作用。机器上一块 swap 都没配、free里 Swap 全是 0你调这个参数纯属心理安慰内核根本没有第二条路可走。另外提一句新内核的变化从 5.8 开始swappiness的取值范围从 0~100 扩展到了 0~200。200 的含义是极度倾向用 swap匿名页和文件页回收力度按 2:1 配比实践中几乎没人用。这个扩展出现的背景是 zswap 和 zram 这类压缩内存后端的普及——当 swap 后端是压缩内存而非慢速磁盘时换出的代价低得多值可以适当调高。如果你用的是 zram 并且内存紧张把 swappiness 设在 100~150 之间反而是合理的这个思路很多老文章里根本没有。3.2 水位三兄弟与 min_free_kbytes 的算法这组参数是内存回收最底层的触发器也是最容易被忽略的。内核为每个内存 zone 维护三个水位线MIN、LOW、HIGH三个值都由min_free_kbytes推导出来WMARK_MIN min_free_kbytes WMARK_LOW WMARK_MIN × (100 watermark_scale_factor) / 100 WMARK_HIGH WMARK_MIN × (100 2 × watermark_scale_factor) / 100默认watermark_scale_factor 10那么 LOW 是 MIN 的 1.1 倍HIGH 是 1.2 倍三个线挨得非常近。这三条线各自对应什么行为内存降到 LOW 以下kswapd内核回收线程被唤醒开始后台异步回收降到 MIN 以下分配内存的进程被迫自己同步回收direct reclaim这一步是应用线程被卡住的真正来源回到 HIGH 以上kswapd才重新睡下。三个线太近会怎样kswapd被唤醒之后稍微回收一点就到 HIGH 了马上去睡觉然后内存又被消耗再次跌破 LOW再唤醒。这种启停启停的抖动在监控上表现为kswapdCPU 占用忽高忽低同时应用时不时吃一下 direct reclaim。所以在大内存机器上适当拉开三个水位的间距是有意义的方法就是提高watermark_scale_factor比如调到 100~200让 LOW 变成 MIN 的 2~3 倍给kswapd留出足够的提前量。再来看min_free_kbytes本身。它的默认值由内核按物理内存算出来公式大致是4 × sqrt(lowmem_kb)另外有上下限截断。一台 64GB 的机器算下来也就 32MB 左右。这个量级在大内存机器上偏小——32MB 的保留区稍微有点突发分配就能击穿然后全系统进 direct reclaim。我的经验取值是按内存的 1%~3%但有几个约束不低于 128MB不超过 1GB最终值最好用 2 的幂次附近的整数。64GB 机器我会给 512MB256GB 机器给 1GB。判断依据是机器上跑的东西——如果有一批会瞬间大量申请内存的进程比如并发编译、大数据任务的 exec 阶段保留区就要大一些如果内存使用非常平稳偏小也没关系。提示min_free_kbytes调得过大同样有害。保留区是任何进程都不许用的内存设得离谱会让可用内存凭空少一大块反而更容易触发 OOM。别为了安全往上堆1%~3% 这个区间是有大量实践支撑的。3.3 vfs_cache_pressure 与 slab 缓存治理vm.vfs_cache_pressure默认 100意思是回收 dentry 和 inode 缓存的倾向与回收 page cache 相同。这两个缓存是干什么的dentry 缓存存的是路径名到 inode 的映射inode 缓存存的是文件元数据。每次open()一个文件内核都要走一遍路径解析如果 dentry 缓存命中几乎是零成本不命中就要一层层读目录慢几十倍。所以判断标准很清楚如果你的负载有大量文件路径查找这个值就不该是 100。典型场景是静态资源服务、代码构建、海量小文件读写、容器镜像分层。这些场景下调到 50 甚至更低内核会更倾向于保留 dentry/inode 而优先回收 page cache路径解析的性能会很稳。反过来如果机器上 slab 里 dentry 缓存吃的内存大得离谱/proc/meminfo里SReclaimable字段很高而且这些缓存命中率其实很低比如做过一次全盘扫描之后留下的一堆垃圾那把vfs_cache_pressure调到 200 甚至更高让内核更积极地清掉它们能立刻回收出可观的内存。怎么判断命中率老实说没有特别直接的指标我的做法是看两个地方一是slabtop里dentry和inode_cache两项的量级二是业务侧的耗时分布如果open()相关路径的延迟有异常那就是缓存没兜住。4. overcommit 与 mmap决定能不能申请到的那组开关这一组参数管的事情跟前面不太一样。前面几组管的是内存怎么用这组管的是内存能不能申请到。听起来更底层但它引发的故障往往更戏剧化——进程明明malloc成功了写数据的时候却被 OOM killer 干掉或者反过来明明物理内存还有富余fork()却返回失败。4.1 overcommit_memory 三种模式的真实差别vm.overcommit_memory有三个取值行为差异非常大值模式行为0启发式默认内核凭经验估算明显过分的申请会被拒正常的都放行1总是允许来者不拒永远返回成功直到真正写内存时 OOM2严格限额所有申请的总额不得超过 commit limit模式 2 的 commit limit 计算方式是swap 大小 物理内存 × overcommit_ratio / 100默认 ratio 是 50。注意这里算的是承诺出去的内存总量包括所有进程已经malloc但还没真正写的部分。三种模式怎么选我的实践默认的 0 适合绝大多数机器尤其是你没法预知上面会跑什么的时候。它的启发式算法虽然粗糙但很少误伤。1 适合知道自己在干什么的场景。典型代表是那种会在运行期fork()出一份完整内存副本做持久化的服务——fork()之后子进程先共享父进程的页如果系统拒绝超量承诺这次fork()直接失败后台持久化就挂了。这类程序如果配上 swappiness 偏低、没有 swap就非常危险。所以我给这类服务所在机器的标配是overcommit_memory 1加上一块哪怕很小的 swap 兜底。2 适合边界清晰、需要严格控制内存总量的场景比如一台只跑特定服务的专用机你希望任何超额申请立刻失败而不是拖到后面炸。但用它的前提是你真的算得清 commit limit并且能接受某些程序因为限额而启动失败。注意模式 1 下申请成功不代表能用。程序malloc了 10GB 然后逐页写入如果物理内存和 swap 加起来不够OOM killer 会挑一个进程杀掉挑中的不一定是申请最多的那个。这是模式 1 最大的风险点不是把开关打开就万事大吉。4.2 max_map_count 与 mmap 类程序的隐形天花板vm.max_map_count默认 65530限制的是单个进程能拥有的内存映射区域VMA数量。这个限制在早年几乎不可能触及但现代程序把它撞破的频率越来越高JVM 的内存分配器会按区域做映射内存型数据库会把大文件分段映射全文检索类服务会把索引文件切成一个个段容器里跑这些程序再叠加 host 侧的映射很容易逼近上限。撞破的表现非常直白mmap返回ENOMEM程序报出诸如 Cannot allocate memory、 Map failed 之类的错误而此刻你去free看内存明明还剩一半。这种情况最容易被误判成内存不足。处理办法很简单把vm.max_map_count提到 262144 或者 1048576。这个值只是限制 VMA 数量本身不占内存调大没有副作用只是每个 VMA 在内核里有个几十字节的描述结构体百万级也就几十 MB。我的习惯是只要机器上跑的是容器宿主、或者跑着上述那几类服务直接提到 1048576一劳永逸。顺带说个实用的小参数vm.admin_reserve_kbytes默认取min(3% 的空闲内存, 8MB)。它的作用是给 root 权限的进程保留一块内存让系统在极端内存压力下管理员还能登录进来杀进程。8MB 对于现代系统登录操作来说有点紧——一个 shell 加几个工具就吃掉不少。我会把它提到 65536也就是 64MB。这个调整在平时毫无感知在出事故的时候可能就是能登上机器和只能硬重启的区别性价比极高。5. 透明大页、compaction 与碎片治理这一块是延迟敏感型服务的重点。透明大页THP的设计初衷是好的用 2MB 的大页代替 4KB 的小页页表项减少 512 倍TLB 命中率提升对遍历大块内存的负载比如大数组、大矩阵运算性能提升明显。问题在于它的自动管理策略在某些负载下会引入不可预测的延迟。5.1 THP 三种模式的取舍/sys/kernel/mm/transparent_hugepage/enabled有三个取值always内核尽最大努力给所有符合条件的匿名内存用大页madvise只对程序显式用madvise(MADV_HUGEPAGE)标记过的区域用大页never完全关闭。always的问题出在碎片整理上。系统长时间运行后物理内存会被切得七零八碎凑不出一块连续的 2MB。这时候内核要么阻塞当前线程去做 compact这就是那个几十毫秒甚至几百毫秒的延迟尖峰来源要么放弃。对于在线服务来说一个突发的 100ms 停顿足以让接口超时、让上游熔断。所以我的配置原则是数据库、消息中间件、延迟敏感的在线服务一律madvise让程序自己决定哪块内存值得用大页纯粹做大规模数值计算、跑批处理的机器可以留always确实用不上的比如一堆小进程never最省心。配合的还有/sys/kernel/mm/transparent_hugepage/defrag取值always / defer / defermadvise / madvise / never。如果保留了always的大页模式我建议 defrag 设成defer让整理发生在后台而不是阻塞分配路径。较新内核还有vm.compaction_proactiveness默认 20控制主动整理的积极程度设 0 可以完全关掉主动整理代价是长期运行后大页分配成功率下降。5.2 watermark_boost_factor 与碎片应对vm.watermark_boost_factor默认 15000是个相对冷门但挺有意思的参数。它的作用是当系统检测到严重的内存碎片化时临时把水位线上调一个百分比默认 150% 的 MIN 水位促使kswapd更积极地回收争取回收出连续的大块空闲区域。这个机制本质上是在碎片化和回收压力之间做权衡。调大它能提高大页分配成功率代价是更早地触发回收可能让 page cache 被清得更多设成 0 就完全禁用这个 boost系统行为更可预测但碎片化严重时大页分配会持续失败。我个人在延迟敏感的服务上会把它设成 0把一切都交给madvise控制在跑批的机器上保持默认。6. 落地从观测到持久化的完整操作流程前面聊的都是为什么这一章讲怎么做。我把这套流程固化成了一个模板每台机器都按同样的顺序走一遍好处是永远不会漏项也方便事后回溯。6.1 观测清单五处数据源二十分钟看完调优前的基线观测我固定看这几处# 一、内存总览与脏页 grep -E MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapTotal|SwapFree|Dirty|Writeback|AnonPages|Mapped|Slab|SReclaimable|SUnreclaim|PageTables /proc/meminfo # 二、回收压力与缺页 grep -E pgscan_|pgsteal_|pgmajfault|pgfault|allocstall|compact_ /proc/vmstat # 三、每个 zone 的水位线对比 cat /proc/zoneinfo | grep -E Node|zone|min|low|high|free # 四、内存压力指数PSI最直观的一个指标 cat /proc/pressure/memory # 五、谁在吃内存 ps aux --sort-rss | head -20 slabtop -o -s c | head -20其中/proc/pressure/memory是我认为最有价值、也最被低估的一个。它输出两行some和full各自带avg10/avg60/avg300三个时间窗的百分比含义是有任务因为内存不足而停顿的时间占比。这个数字比任何间接指标都直接——full的 avg10 如果长期在 5% 以上说明系统有实实在在的内存压力值得动手如果三个窗口全是 0.00那你调什么参数都是在瞎折腾。/proc/vmstat里的allocstall_*系列也很有用它统计的是各个 zone 发生 direct reclaim 的次数。这个值持续增长就说明应用线程在被卡着做回收这就是要动手的明确信号。6.2 写配置文件而不是堆在 sysctl.conf临时验证用sysctl -w没问题但持久化我坚决反对往/etc/sysctl.conf里追加。原因很简单这个文件是系统级的多个来源系统默认、包管理器、手工添加、某些软件的安装脚本都会往里面写东西时间一长就是一团乱麻谁也说不清某个值是谁加的、为什么加。正确做法是在/etc/sysctl.d/下建一个专属文件按用途命名# /etc/sysctl.d/90-vm-tuning.conf # 用途脏页回写平滑化适配 NVMe # 变更日期2024-xx-xx 变更人xxx # 回滚方式删除本文件后执行 sysctl --system vm.dirty_background_bytes 536870912 vm.dirty_bytes 2147483648 vm.dirty_expire_centisecs 1500 vm.dirty_writeback_centisecs 500 # 内存回收 vm.swappiness 10 vm.min_free_kbytes 524288 vm.watermark_scale_factor 150 # 大页与映射 vm.max_map_count 1048576 vm.admin_reserve_kbytes 65536命名用90-前缀是因为sysctl.d按文件名排序加载数字大的后加载会覆盖前面的。要覆盖系统默认值和包管理器加的值前缀就得排在它们后面。文件头部的注释是强迫自己写的——三个月后你或者你的同事看到这个文件得能立刻判断能不能删。生效命令是sysctl --system它会按顺序加载/etc/sysctl.d/*.conf、/run/sysctl.d/*.conf、/usr/lib/sysctl.d/*.conf和/etc/sysctl.conf。注意加载顺序有讲究同名参数以后加载的为准。提示sysctl -w改的值重启就没了sysctl --system只是重新加载配置文件。两个命令别混着用验证阶段用前者定稿后写文件再跑后者。6.3 灰度、回滚与变更记录参数变更也是变更该有的流程一样不能少。我的做法是第一步先在一台非核心机器上验证。如果是集群挑一台流量最小的。观察至少一个完整的业务周期——很多问题不是立刻暴露的比如dirty_expire_centisecs调大之后要等积累一段时间的脏页才会看出风险。第二步设定明确的观察指标和回滚阈值。比如写入 P99 延迟下降 30% 以上算成功PSI full avg10 上涨超过 3% 立刻回滚。没有阈值的观察等于没观察人在盯监控的时候特别容易自我说服好像好一点了。第三步回滚路径必须是随时可执行的。配置文件删掉、sysctl --system一跑就回去了这是最理想的形态。反过来说如果某个参数必须重启才能生效那灰度就要做得更谨慎。第四步记录基线。我习惯在变更前后各存一份/proc/meminfo、/proc/vmstat、/proc/zoneinfo的快照命名带上时间和机器名。这些文件在事后复盘时价值巨大比我记得当时好像是……强一万倍。7. 四类典型场景的配置模板参数没有通用最优解只有场景最优解。下面这几套是我在反复调整后留下来的模板可以直接当起点用但一定要结合自己机器的实际数据再改。7.1 数据库主机数据库的特点是大块共享内存缓冲区buffer pool 那类东西 对延迟极其敏感 自己的 IO 调度已经很讲究。vm.swappiness 1 vm.dirty_background_bytes 268435456 vm.dirty_bytes 1073741824 vm.dirty_expire_centisecs 1000 vm.overcommit_memory 2 vm.overcommit_ratio 80 vm.min_free_kbytes 1048576 vm.watermark_scale_factor 200 vm.vfs_cache_pressure 100swappiness 1的逻辑是保住进程工作集缓冲区一旦被换出查询延迟会直接翻倍。脏页阈值压得比较小是为了让每次 checkpoint 的 IO 更均匀——数据库本身有自己的刷盘策略内核再攒一大笔脏页只会互相打架。overcommit_memory 2配上ratio 80是为了让内存超配在启动阶段就暴露而不是运行到一半被 OOM 干掉。透明大页一定设madvise这一点比上面任何参数都重要。7.2 高并发接入层接入层反向代理、网关这类的特点是连接数巨大、单连接内存占用小、大量短生命周期的对象分配、高度依赖缓存命中。vm.swappiness 10 vm.min_free_kbytes 524288 vm.watermark_scale_factor 150 vm.vfs_cache_pressure 50 vm.max_map_count 1048576 vm.dirty_background_bytes 536870912 vm.dirty_bytes 2147483648vfs_cache_pressure 50是为了保留路径解析相关的缓存接入层大量做静态资源转发时这个收益明显。min_free_kbytes给足是因为接入层经常在流量高峰瞬间创建大量连接对象需要一个足够厚的缓冲垫避免直接进 direct reclaim。7.3 容器宿主容器宿主是最容易出问题的一类。核心矛盾在于vm.*是全局的而容器有各自的 cgroup 限额两者会互相干扰。vm.overcommit_memory 1 vm.max_map_count 1048576 vm.min_free_kbytes 1048576 vm.watermark_scale_factor 200 vm.panic_on_oom 0 vm.admin_reserve_kbytes 131072overcommit_memory 1在容器宿主上是常见选择——容器编排系统本身会按资源声明调度宿主再卡一道反而容易造成容器启动失败。panic_on_oom 0保证 OOM 时杀容器而不是整机 panic。admin_reserve_kbytes给到 128MB因为容器宿主在内存紧张时你需要能登上去docker/crictl之类地操作这些工具本身也要吃内存。注意不要在容器内部试图修改vm.*。绝大多数情况下/proc/sys/vm/在容器里是只读的改了会报错即便能改改的也是宿主的全局值会影响其他所有容器。容器内的内存控制靠memory.max和memory.high这是两个完全不同的机制。7.4 桌面与开发机开发机的调优目标跟服务器完全不同不用追求极限吞吐追求的是编译不卡、切换不卡、不会因为开了一堆浏览器标签就整机假死。vm.swappiness 30 vm.vfs_cache_pressure 50 vm.dirty_background_bytes 134217728 vm.dirty_bytes 536870912 vm.min_free_kbytes 65536vfs_cache_pressure 50对开发机收益特别明显——频繁切换项目、跑测试、读源码路径缓存命中率直接决定git status是秒回还是要转三秒。脏页阈值压低是为了避免笔记本一合盖走人时还在疯狂写盘也能减少掉电丢数据的窗口。8. 常见问题速查与踩坑记录8.1 排查速查表现象优先怀疑的参数排查动作周期性 IO 尖峰写入延迟规律抖动dirty_ratio/dirty_bytes看/proc/meminfo的 Dirty 曲线是否呈锯齿状内存还有富余但进程报 ENOMEMmax_map_count看/proc/pid/maps行数、程序日志里的 mmap 错误系统卡顿但内存使用率不高min_free_kbytes偏小看/proc/zoneinfo各 zone 的 free 是否贴着 min服务延迟出现毫秒级尖峰THPalways compaction关掉 THP 或改madvise后对比fork()失败但内存充足overcommit_memory 2限额太低看/proc/meminfo的 CommitLimit 与 Committed_ASOOM 杀了核心进程swappiness 0或没有 swap检查 SwapTotal 是否为 0、加一块小 swap 兜底缓存被清空导致读 IO 暴涨vfs_cache_pressure太高对比SReclaimable数值变化容器内写 sysctl 报只读命名空间隔离改宿主全局值容器内改用 cgroup 参数8.2 几个印象最深的坑第一个坑是把 swappiness 设成 0 就等于禁用 swap。这个错误的代价很实在一台机器通过这个设置优化过结果在一次内存突发时因为没有 swap 可用内核直接触发 OOM killer杀掉了一个跑了两天的批处理任务。后来加了一块 8GB 的 swap 并把 swappiness 设成 10同样的负载下不再被杀只是慢了一点。留一块 swap 作为缓冲垫几乎永远是划算的哪怕你永远不希望它被用上。第二个坑是脏页参数用 bytes 和 ratio 混着设。曾经有个配置文件里同时写了两行结果内核实际只认 bytes 那组ratio 被自动清零。后来有人看着dirty_ratio显示 0以为是配置漏了又补了一行 ratio 上去直接把 bytes 那组给覆盖了之前的调优全部白费问题重现了一周才查出来。现在我的规矩是同一个参数组只允许出现在一个配置文件里。第三个坑跟透明大页有关。某个延迟敏感服务上线后 P99 一直有规律的长尾查了三天代码没找到问题最后发现是 THP 在捣鬼。切到madvise之后长尾直接消失。这件事教会我一个判断方法如果延迟毛刺的分布没有明显规律但量级在几十毫秒且和流量无关先怀疑内核层的自动行为而不是业务代码。第四个坑是观测缺失导致的错误归因。有次优化完之后各项指标都变好了团队很开心两周后才发现是一个无关的配置变更带来的收益我们调的那组参数实际上什么都没改变——因为瓶颈根本不在内存。这也是我为什么在第 6 章反复强调基线数据没有对照的优化很容易变成自我安慰。最后一个想说的经验真正有效的内存调优往往不是调参数而是改架构。比如把一个大进程拆成几个小的加上 cgroup 限额比在全局参数上反复试探有效得多比如给内存密集型的任务单独划一台机器比在一台机器上跟人抢内存靠谱得多。参数调优是最后那 5% 的打磨前面 95% 靠的是容量规划和负载划分。我在实际工作中越来越倾向于先问这台机器真的应该跑这些吗而不是一上来就翻/proc/sys/vm/。