上个月帮朋友排查一台离线计算节点的抖动问题,现象很典型:跑批任务的时候整机响应突然变慢,iowait拉高,几个进程卡在D状态里十几分钟不动,事后看监控,内存曲线没什么大问题,磁盘写入却像脉冲一样一阵一阵地冲。折腾了一晚上,最后落点不在应用代码,而在/proc/sys/vm/下面那几个数字上——dirty_ratio偏高,导致大量脏页在内存里堆积,等到阈值触发才集中回刷,把整条 I/O 队列堵死。
这件事之后我把Linux vm 内核参数调优这件事重新梳理了一遍。这里说的 vm,是内核里的 memory management 子系统,路径就是/proc/sys/vm/,跟虚拟机没有半点关系,很多人第一次看到vm.swappiness会以为是虚拟化相关的开关,其实它管的是内核在内存紧张时对匿名页的回收倾向。这一组参数决定了内核怎么分配内存、什么时候开始回收、脏页在内存里能待多久、进程能申请到多少虚拟地址空间。调好了,机器在压力下依然稳;调歪了,轻则性能打折,重则 OOM 直接把关键进程干掉。
下面这些内容适合谁看:手上管着物理机、云主机或者容器宿主机,跑着数据库、缓存、消息队列、大数据计算这类对内存和 I/O 敏感的服务;或者你只是想让自己的开发机在编译大型项目时不至于卡成幻灯片。不需要你读得懂内核源码,但至少得知道free -m和/proc/meminfo里那些字段是什么意思。
1. 先把边界划清楚:vm 子系统到底在管什么
1.1 别把 /proc/sys/vm 当成虚拟机管理器
这个误解太常见了,我见过有人把vm.前缀的参数当成虚拟化平台的调优项,跑去问为什么改了没效果。/proc/sys/vm/这个目录是内核暴露出来的内存管理旋钮集合,它跟 KVM、容器运行时、虚拟化平台统统无关,哪怕你把机器上所有虚拟化组件都卸了,这些参数照样存在、照样生效。
从内核视角看,内存管理要解决的核心矛盾只有一个:物理内存是有限的,而进程对内存的需求是弹性的、突发的。于是内核设计了一整套机制来缓冲这个矛盾——page cache 把磁盘数据缓存起来加速读,脏页延迟回写减少随机写,swap 把不常用的匿名页挪到磁盘腾出空间,OOM killer 在实在没辙的时候挑一个进程牺牲掉。/proc/sys/vm/下的每一个参数,本质上都是在调节这套机制的"松紧程度"。
理解了这一点,你就能明白为什么调参不能抄作业。同样是vm.swappiness=10,在一台只跑无状态 Web 服务的机器上几乎无感,在一台内存 512G、跑着大型内存数据库的机器上可能直接改变业务表现。参数只是旋钮,拧多少取决于你机器上跑的是什么、你更在意延迟还是吞吐。
1.2 参数分三层:全局、cgroup、进程级
很多人只知道sysctl这一层,实际上内存相关的控制至少分三个层级,搞混了就会出现"明明改了却不生效"的困惑。
第一层是全局的/proc/sys/vm/,通过sysctl或者直接写/proc文件修改,作用于整台机器,这是本文的主战场。
第二层是 cgroup 的内存控制器。cgroup v2 下有memory.max、memory.high、memory.low、memory.min、memory.swap.max这些文件,它们约束的是某个控制组内所有进程的内存使用上限和回收保护。注意,cgroup 层没有swappiness这种东西,它只能决定"这个组最多能用多少",不能决定"回收时先动谁的页"。所以容器场景下你经常需要两层配合:宿主机的全局参数定基调,容器的 cgroup 限额划边界。
第三层是进程级的,比如madvise()系统调用可以让进程主动告诉内核某段内存的使用模式(MADV_DONTNEED、MADV_HUGEPAGE、MADV_COLD之类),mlock()可以把关键内存锁在物理内存里不被换出。这一层通常由应用自己控制,运维插不上手,但知道它存在,排查问题时能多一个思路——有时候不是系统参数不对,是应用自己把一大块内存mlock住了。
提示:容器内看到的
/proc/sys/vm/大多数情况下和宿主机是同一份(除非运行时做了特殊挂载),也就是说容器里改vm.swappiness会影响到整台宿主机上的所有容器。这个坑后面还会细说。
1.3 调优前先定目标:延迟、吞吐和稳定性的三角
我见过太多人上来就问"这几个参数最佳值是多少",这个问题本身就没法回答,因为不存在脱离场景的最佳值。调优之前你至少得想清楚三个问题:这台机器跑的是什么类型的负载、我最不能接受的是什么、出问题时我能承受多大的爆炸半径。
延迟敏感型(在线 API、交易系统)最怕的是长尾抖动,一次直接内存回收或者一次集中脏页回刷就可能让 P99 飙到几百毫秒,这类机器要把水位留足、把脏页阈值压死,宁可多花一点 CPU 做后台回收。吞吐优先型(离线计算、日志处理、批量导入)更在意单位时间能处理多少数据,允许一定程度的延迟波动,可以把 page cache 用得更激进、脏页阈值放得更宽,让内核攒一批再写。稳定性优先型(容器宿主机、混部集群)则要考虑最坏情况,不能让某一个进程把整机内存吃干,这时候overcommit_memory、min_free_kbytes、cgroup 限额的组合就比单点参数重要得多。
顺带说一句,调优之前先做基线观测。vmstat 1、sar -B 1、iostat -x 1跑上十几分钟,把正常状态下的si/so、bi/bo、await、%util记下来。没有基线,你改完参数之后根本判断不了是变好了还是变坏了,只能凭感觉,那跟不调没区别。
2. 核心参数逐个拆解:含义、默认值和调整方向
2.1 回收倾向与预留水位:swappiness、min_free_kbytes、watermark_scale_factor
vm.swappiness大概是知名度最高的一个,默认 60,取值范围在新一些的内核里放宽到了 0 到 200。它的作用不是"是否使用 swap"的开关,而是调节内核在回收内存时,对匿名页(进程的堆栈、malloc 出来的内存)和文件页(page cache)的回收权重。值越高,内核越倾向于把匿名页换出到 swap;值越低,越倾向于丢弃 page cache。
这里有个流传很广的误解:swappiness=0就是不用 swap。实际上在多数内核版本里,即使设成 0,当内存压力大到一定程度、匿名页和文件页都回收不动的时候,内核仍然会动用 swap,不然就只能 OOM 了。所以指望靠这个参数彻底禁掉 swap 是不现实的,真想禁就swapoff。我一般给数据库类负载设 1 到 10,给计算型负载保持默认或者 30 左右,纯内存型的服务会设成 1 并且把 swap 关掉。
vm.min_free_kbytes是给内核预留的空闲内存下限,默认值在内核启动时根据物理内存算出来,大致是内存的一个很小比例。这个值直接决定了内存回收的三级水位线:min、low、high。当空闲内存降到low水位时唤醒kswapd后台回收,降到min水位时才会触发直接回收(也就是会让申请内存的进程自己卡住去回收),high水位则是kswapd停止回收的界线。
这个参数调小了,后台回收启动太晚,容易频繁掉进直接回收,进程出现卡顿;调大了,预留的空闲内存变多,可用内存变少,反而更容易触发回收。内核里对它做了硬编码的上下限约束,超出范围会被夹到边界值,你写个 999999 也不会真的生效。一般建议在默认值基础上往上抬一点,物理内存大的机器可以到几百 MB 级别,具体算法后面第 3 节会讲。
vm.watermark_scale_factor是控制水位线间距的,默认 10,单位是万分之零点一。它和min_free_kbytes有联动关系:low水位约等于min加上内存总量的watermark_scale_factor/10000,high水位再往上加同样的量。把它调大,等于把kswapd的"提前量"拉宽,后台回收启动更早、停止更晚,代价是多占一点内存、多消耗一点 CPU。在内存大、延迟敏感的机器上,我通常会把min_free_kbytes设到合适值之后再把这个系数调到 100 到 200,让回收更平滑。
另外还有个vm.watermark_boost_factor,默认 15000,相对新一些的内核才有。它的作用是当出现内存碎片、高阶分配失败时,临时把水位线抬高,触发更积极的回收和内存规整。多数场景保持默认就行,除非你在跑大量需要连续大页的负载。
2.2 脏页回写:四组参数决定内存里能堆多少脏数据
这一组是我认为最值得花时间调、也最容易调出事故的参数。所谓脏页,就是已经被进程修改、但还没写到磁盘上的 page cache。内核不会每次write()都立刻落盘,而是攒着,等攒到一定量或者超过一定时间再批量回写,这样能大幅减少随机 I/O、提升吞吐。但攒得越多,两个风险越大:一是掉电丢数据的窗口变长;二是集中回刷时 I/O 队列被瞬间打满,其他读写请求全被拖慢。
控制这件事的是两对参数:
vm.dirty_background_ratio,默认 10,脏页占到内存的多少比例时,唤醒后台回写线程开始慢慢刷。vm.dirty_ratio,默认 20,脏页占到多少比例时,写数据的进程会被强制同步回写,直到脏页降下去。vm.dirty_expire_centisecs,默认 3000,也就是 30 秒,脏页在内存里待超过这个时间就会被标记为需要回写。vm.dirty_writeback_centisecs,默认 500,也就是 5 秒,后台回写线程的唤醒周期。
这四个参数的关系可以这么理解:dirty_background_ratio是"开始慢慢刷"的线,dirty_ratio是"强制刹车"的线,dirty_expire_centisecs是"最多能赖多久",dirty_writeback_centisecs是"多久检查一次有没有超时的"。后台回写是异步的,不影响业务进程;强制回写是同步的,会让write()调用直接卡住,这就是很多人看到"写操作突然变慢"的根因。
还有个配套的vm.dirty_background_bytes和vm.dirty_bytes,用绝对值而不是百分比来设定阈值。注意这两组是互斥的:一旦你设置了dirty_bytes,dirty_ratio会自动变成 0 并且失效,反过来也一样。这一点在文档里写得很小,我见过有人两个都设了,然后困惑为什么百分比那组完全不起作用。
为什么百分比在大内存机器上很危险?算一笔账就明白了。一台 256G 内存的机器,dirty_ratio=20,意味着脏页最多能堆到 51.2G。哪怕你用万兆盘、RAID 阵列,把 51.2G 一次性刷下去也得几十秒,这段时间里所有新的写入请求都在排队。所以内存越大,越应该改用绝对值的dirty_bytes来设阈值,把脏页总量控制在一个跟磁盘实际带宽匹配的量级上。具体的换算方法第 3 节会展开。
2.3 过量提交与地址空间:overcommit_memory、max_map_count
vm.overcommit_memory管的是内核对malloc这种内存申请请求的态度,有三个取值。
0 是默认的启发式策略,内核会根据当前可用内存做一个粗略估算,觉得明显不可能满足就拒绝,但总体上比较宽松,绝大多数应用都能正常申请。1 是"永远说可以",不管实际有没有内存,申请就答应下来,等真正用到的时候再触发 OOM——这个模式在跑某些内存占用远超物理内存、但实际只会触碰一小部分的应用时有用,风险是 OOM 来得更突然。2 是严格模式,内核会把总承诺量控制在一个上限内,超出就直接拒绝申请,返回ENOMEM。
严格模式下的上限由vm.overcommit_ratio决定,默认 50。计算公式大致是CommitLimit = swap 总量 + 物理内存 * overcommit_ratio / 100。一台 64G 内存、16G swap、ratio 50 的机器,总承诺上限就是 16 + 32 = 48G。这个模型对于想要精确控制内存分配的团队很有用,但坑也很明确:它统计的是虚拟地址空间的"承诺",而不是真实占用,很多程序会预留大量地址空间但实际只用一点点,在模式 2 下很容易被误杀,出现malloc失败、进程起不来的情况。除非你的业务模型非常清晰,否则我不建议动这个值。
vm.max_map_count是另一个高频出问题的参数,默认 65530,限制的是单个进程能拥有的内存映射区域数量。每次mmap、每次线程栈、某些内存分配器划出来的 arena,都会消耗一个映射计数。现代应用很容易触顶——尤其是那些频繁申请释放内存、用大量线程、或者自己管理堆外内存的服务,跑一段时间就会出现"cannot allocate memory"或者提示max virtual memory areas ... is too low, increase to at least ...的报错。
我的经验是,只要机器上有跑时间比较长、内存使用复杂的大服务,直接把这个值调到 262144,代价几乎可以忽略(它只是限制计数,不是预分配),收益是避免了那种毫无征兆的启动失败或运行中断。要注意这个改动需要重启进程才能生效,已经跑着的进程不会因为系统参数改了就把计数器清零。
2.4 缓存回收倾向与 NUMA:vfs_cache_pressure、zone_reclaim_mode
vm.vfs_cache_pressure默认 100,控制的是内核回收 dentry 和 inode 缓存的积极程度。设成 100 表示与回收 page cache 保持同等力度,小于 100 表示更倾向于保留目录项和 inode 缓存,大于 100 表示更容易回收它们。
这个参数什么时候有用?典型场景是文件系统上存在海量小文件、目录层级很深、访问比较随机的时候,dentry cache 命中率决定了很多操作的快慢。把它调低(比如 50)可以让这部分缓存留得更久,代价是内存压力大时 page cache 被优先挤掉。反过来,如果机器上是大量顺序读写大文件、目录结构简单的负载,调高到 200 甚至更高可以让内存更多留给数据页。不过实际测试下来,这个参数敏感度不高,多数场景保持默认即可,把它当成一个微调项而不是主力手段。
vm.zone_reclaim_mode跟 NUMA 架构相关,默认 0。在多路服务器上,每个 CPU 插槽对应一个 NUMA 节点,进程访问本地节点的内存快、跨节点访问慢。当本节点内存不够时,默认策略是优先从其他节点借内存(跨节点访问,慢但能用),而把这个参数设成 1 或更大,表示内核在本地节点内存不足时先做本地回收(丢弃本地 page cache),尽量不跨节点。对于延迟敏感、又能接受丢缓存的负载,调这个值有收益;对于缓存价值很高、跨节点访问代价可接受的负载,保持 0 更好。这个参数是否生效取决于硬件拓扑,在单节点机器上改了也没意义,判断方法是看/sys/devices/system/node/下有没有多个nodeX目录。
顺便提一下vm.zone_reclaim_mode的邻居——透明大页。较新内核里是madvise和always两种模式,通过/sys/kernel/mm/transparent_hugepage/enabled控制。这一项不在/proc/sys/vm下,但它造成的性能差异比很多 vm 参数都大:always模式下内核会自动把 4K 页合并成 2M 页,减少 TLB miss,但对数据库这类有明确内存管理策略的应用可能造成分配延迟抖动。我的习惯是数据库机器设成madvise,让应用自己决定;纯计算和 Java 类服务可以尝试always,用压测数据说话。
| 参数 | 默认值 | 主要作用 | 常见调整方向 |
|---|---|---|---|
vm.swappiness | 60 | 匿名页与文件页的回收权重 | 数据库/内存型 1-10,通用 30-60 |
vm.min_free_kbytes | 内核计算 | 预留空闲内存,决定水位线 | 大内存机器适当抬高 |
vm.watermark_scale_factor | 10 | 水位线间距 | 延迟敏感场景调到 100-200 |
vm.dirty_ratio | 20 | 强制同步回写的脏页比例 | 大内存机器改用dirty_bytes |
vm.dirty_background_ratio | 10 | 后台回写启动比例 | 同上,配合调整 |
vm.dirty_expire_centisecs | 3000 | 脏页最长驻留时间 | 按数据安全要求缩短 |
vm.overcommit_memory | 0 | 内存申请策略 | 一般不动,特殊场景才设 2 |
vm.max_map_count | 65530 | 单进程映射区域上限 | 复杂服务调到 262144 |
vm.vfs_cache_pressure | 100 | 目录/inode 缓存回收力度 | 微调项,50-200 |
vm.zone_reclaim_mode | 0 | NUMA 本地回收策略 | 多路机器按负载评估 |
3. 取值不是拍脑袋:三个常见参数的计算过程
3.1 dirty_ratio 换成字节能省多少事
先说脏页阈值。假设一台机器有 128G 物理内存,磁盘阵列的实际持续写带宽测下来是 800MB/s,业务能容忍的写入卡顿不超过 2 秒。那么脏页总量最好控制在800MB/s × 2s = 1.6G左右,超过这个量,一次回刷就必然超过容忍窗口。
换算成字节参数,vm.dirty_bytes设1610612736(1.5G),vm.dirty_background_bytes设它的四分之一到三分之一,比如402653184(384M)。这样后台回写在脏页达到 384M 时就开始慢慢刷,等到 1.5G 时强制刹车,两档之间有足够缓冲,不会出现"刚启动后台回写就撞到强制阈值"的尴尬。
如果是按比例设置的老机器,同样的 128G 内存想达到类似效果,dirty_ratio需要设成1.5/128 ≈ 1.2%,dirty_background_ratio设成 0.3% 左右。问题在于这两个参数是整数百分比,1% 和 0% 之间的粒度太粗,而且内核对这些值还有下限保护,设成 0 通常不生效。所以在内存超过 64G 的机器上,字节模式几乎是唯一靠谱的选择。这也是我一直建议大内存机器优先用dirty_bytes的原因——百分比在小内存时代够用,现在是历史包袱。
再补充一个细节:dirty_expire_centisecs和dirty_writeback_centisecs的单位是厘秒,30 秒要写 3000,5 秒要写 500,写错一个数量级的情况我见过不止一次。有人想设 300 秒结果写成 300,实际是 3 秒,然后发现磁盘几乎在疯狂空转。
3.2 min_free_kbytes 和三级水位的换算关系
min_free_kbytes的影响通过水位线传导,所以光看它本身没意义,要看它换算出来的min、low、high三个值。在watermark_scale_factor为默认 10 的情况下,low水位大概是min的 1.25 倍,high水位大概是min的 1.5 倍。你可以从/proc/zoneinfo里直接读到每个内存 zone 的这三个值。
举个例子,一台 64G 内存的机器把min_free_kbytes设成 1048576(1G),那么low水位大约 1.25G,high大约 1.5G。这意味着空闲内存降到 1.25G 时后台回收线程就开始干活,降到 1G 时才会让进程自己去做直接回收。对一个正常运行的机器来说,1G 到 1.5G 的缓冲区间是够用的。
如果把它设成 4G,那low就是 5G,high6G,这相当于凭空从可用内存里挖走 4G 到 6G,回报是回收动作会非常提前、非常平滑,几乎不会出现直接回收。对延迟极端敏感的机器,这么做有道理;对内存本来就紧张的机器,这就是自杀行为,因为可用内存变少之后,进程申请内存时反而更容易触发回收。
我的经验公式是:物理内存在 16G 以下,保持默认或用默认值乘以 2;16G 到 128G,设在 512M 到 1G 之间;128G 以上,可以到 1G 到 2G。同时把watermark_scale_factor调到 100 到 150,让kswapd的调节区间更宽,避免在水位线附近反复横跳。这个值不需要精确,因为它是缓冲不是硬约束,宁可留一点余量。
判断设得合不合适,看两个指标就行:/proc/vmstat里的allocstall如果持续增长,说明直接回收发生得太频繁,水位可能设小了;pgscan_kswapd如果涨得离谱,说明后台回收过于积极,可能设大了或者watermark_scale_factor太激进。
3.3 max_map_count 该给多少
这个参数的合理值没法精确计算,但可以估算。一个进程的内存映射区域大致来自几部分:可执行文件和动态库的映射(每个几到几十个)、堆区(通常一两个,但用了多个 arena 的分配器会成倍增加)、每个线程的栈(每线程一个映射)、mmap直接映射的文件或匿名内存(这部分波动最大)、以及内存分配器为大块内存保留的映射。
粗略算法:线程数 × 1 + 动态库数量 + 应用自己控制的 mmap 数量 × 安全系数。一个有 500 个线程、加载 100 个动态库、内部用 mmap 管理缓存的进程,保守估计需要 1000 个以上的映射,如果它的缓存管理逻辑会频繁划出小段 mmap,几千甚至上万都正常。默认的 65530 看起来很大,实际上对这种程序来说未必够。
实际排查方法是直接看:cat /proc/<pid>/maps | wc -l能数出当前进程的映射数量,跑一段时间之后观察这个数字的增长趋势。如果它已经接近 65530,或者涨势明显,就该考虑上调了。另外sysctl vm.max_map_count改的是上限,不影响已有进程的当前计数,改完需要重启对应服务才生效——这一点和ulimit类似。
4. 落地实操:从临时验证到永久固化
4.1 临时改、永久改、重启后不生效的三种原因
临时验证最简单,sysctl -w vm.swappiness=10或者直接echo 10 > /proc/sys/vm/swappiness,立刻生效,重启失效。适合压测阶段反复试参数。
永久化推荐写到/etc/sysctl.d/下,别直接改/etc/sysctl.conf。原因是现在的系统上,sysctl.conf本身也是通过/etc/sysctl.d/里一个链接被加载的,加载顺序按文件名字典序排列,同样的参数后面的文件覆盖前面的。如果你在sysctl.conf里写了一个值,又在sysctl.d/50-xxx.conf里写了另一个,最后生效的是谁取决于加载顺序,很容易搞混。
# /etc/sysctl.d/99-vm-tuning.conf vm.swappiness = 10 vm.min_free_kbytes = 1048576 vm.watermark_scale_factor = 150 vm.dirty_bytes = 1610612736 vm.dirty_background_bytes = 402653184 vm.dirty_expire_centisecs = 1500 vm.dirty_writeback_centisecs = 500 vm.max_map_count = 262144 vm.vfs_cache_pressure = 50写完执行sysctl --system让所有配置文件重新加载,再用sysctl vm.dirty_bytes之类的命令逐项核对。别偷懒只检查一两个,我踩过设置dirty_bytes之后dirty_ratio被自动清零、然后监控大盘上的指标突然变成 0 的坑。
"改了重启后不生效"通常有三个原因。一是文件放错位置,比如放进/etc/sysctl.conf但系统实际只读sysctl.d(某些精简发行版会这样)。二是被更高优先级的配置覆盖,容器基础镜像里常见。三是被其他工具接管,某些性能调优组件会在启动时把自己的参数写回去,覆盖你的设置——排查方法是重启后grep一遍所有sysctl.d文件,看看有没有重复项,再看相关服务的启动脚本里有没有sysctl -w。
注意:容器里修改
/proc/sys/vm/下的参数,多数情况下影响的是宿主机全局,因为这些参数没有做命名空间隔离。在编排平台里,这类参数通常被归为"不安全"类别,默认不允许容器自行设置,需要显式授权或者以特权模式运行。给业务容器开放这类权限之前,务必确认它不会把宿主机拖下水。
4.2 三类典型场景的配置模板
配置从来不是通用的,下面这几套是我在几类常见负载上验证过、相对稳的组合,可以直接拿去当起点,再根据压测结果微调。
第一类是关系型数据库服务器。这类负载的特点是内存大、有自己的缓存管理、对 I/O 延迟敏感、非常讨厌 swap。
vm.swappiness = 1 vm.min_free_kbytes = 2097152 vm.watermark_scale_factor = 150 vm.dirty_bytes = 1073741824 vm.dirty_background_bytes = 268435456 vm.dirty_expire_centisecs = 1000 vm.dirty_writeback_centisecs = 500 vm.overcommit_memory = 0 vm.max_map_count = 262144关键点是swappiness压到 1,把脏页总量卡在 1G 以内,缩短脏页驻留时间。min_free_kbytes给到 2G 是为了留足回收缓冲,避免数据库在大事务提交时撞上直接回收。
第二类是容器宿主机或者混部节点。这类机器上跑着五花八门的服务,目标是整体稳定、防止单个服务把内存吃干。
vm.swappiness = 30 vm.min_free_kbytes = 1048576 vm.watermark_scale_factor = 100 vm.dirty_bytes = 2147483648 vm.dirty_background_bytes = 536870912 vm.overcommit_memory = 0 vm.max_map_count = 262144 vm.zone_reclaim_mode = 0swappiness保持一个中间值,是因为混部环境下内存压力波动大,完全禁用 swap 反而容易 OOM。真正的隔离靠 cgroup 限额来做,系统参数只负责兜底。
第三类是离线计算或者日志处理节点。这类负载追求吞吐,容忍一定的延迟波动,可以把 page cache 用得更充分。
vm.swappiness = 60 vm.min_free_kbytes = 524288 vm.vfs_cache_pressure = 100 vm.dirty_bytes = 4294967296 vm.dirty_background_bytes = 1073741824 vm.dirty_expire_centisecs = 3000 vm.dirty_writeback_centisecs = 1000脏页阈值放宽到 4G,回写周期也拉长,让内核攒更多再写,减少随机 I/O。这类机器上通常还会把watermark_scale_factor保持默认,因为计算任务的延迟不是核心指标。
4.3 改完之后怎么看效果
改参数不难,难的是判断改对了没有。我一般看四个地方。
/proc/meminfo看整体水位,重点盯MemAvailable、Dirty、Writeback、Cached、AnonPages这几个字段。Dirty如果长期贴着阈值上限,说明写入速度持续超过磁盘消化能力,这时候调参数没用,得换盘或者改业务写入模式。
/proc/vmstat看回收行为,重点是allocstall(直接回收次数)、pgscan_direct(直接扫描页数)、pgscan_kswapd(后台扫描页数)、pgsteal_kswapd、nr_dirty_threshold、nr_dirty_background_threshold。allocstall持续增长是最直接的"内存压力过大"信号,这时候优先看min_free_kbytes和实际内存占用。
vmstat 1看si/so(swap 换入换出)、bi/bo(块设备读写)、wa(I/O 等待)。so长期非零说明在持续换出,如果这不是你想要的,就该动swappiness或者直接关 swap。
iostat -x 1看磁盘的await、aqu-sz、%util。后台回写期间%util上升是正常的,但如果长期贴着 100%、await持续走高,说明磁盘已经饱和,回写参数调得再细也没意义。
有条件的话,用bpftrace直接观测直接回收的调用栈,bpftrace -e 'kprobe:try_to_free_pages { @[stack] = count(); }'跑一分钟,就能看出是哪些代码路径在触发回收。这个手段比翻监控看曲线高效得多,尤其是在定位"到底是哪个服务在制造内存压力"的时候。
5. 踩坑记录与排查速查
5.1 常见问题速查表
| 现象 | 可能原因 | 排查手段 | 处理方向 |
|---|---|---|---|
| 写入突然卡顿数秒 | 脏页集中回刷,撞上强制阈值 | 看Dirty是否长时间贴顶,iostat的await尖峰 | 改用dirty_bytes压低阈值 |
| 进程偶发卡在 D 状态 | 直接内存回收 | vmstat看allocstall增长,bpftrace 抓调用栈 | 抬高min_free_kbytes,调大watermark_scale_factor |
| 服务启动报映射数超限 | max_map_count触顶 | cat /proc/<pid>/maps | wc -l | 上调到 262144 并重启服务 |
| 大量 swap 换入换出 | swappiness偏高或内存真的不够 | vmstat看si/so,free看剩余 | 内存型服务降swappiness,必要时关 swap |
| OOM 随机杀进程 | 内存确实超了,或水位设置不合理 | dmesg看 OOM 日志里的total-vm、rss | 结合 cgroup 限额,排查内存泄漏 |
| 改了 sysctl 但值没变 | 被其他配置文件或工具覆盖 | 逐一grep配置目录,检查启动脚本 | 调整文件命名顺序,禁用冲突工具 |
| 容器里改参数影响宿主机 | 多数vm.*无命名空间隔离 | cat /proc/sys/vm/xxx对比宿主与容器 | 不要给业务容器开放此类权限 |
| 目录遍历性能差 | dentry/inode 缓存被频繁回收 | 观察Cached与SReclaimable | 适当下调vfs_cache_pressure |
5.2 几条文档里不会写的经验
第一,drop_caches不是调优手段,是应急手段。echo 3 > /proc/sys/vm/drop_caches会把 page cache、dentry、inode 全清掉,跑完之后机器的读性能会在一段时间内跌到谷底,因为所有热点数据都得重新从磁盘加载。我见过有人把它写进定时任务里,美其名曰"释放内存",实际上是在持续摧毁系统性能。MemAvailable才是判断内存够不够的正确指标,free里那个free字段小是正常的。
第二,swappiness设成 0 不等于关闭 swap,前面提过一次,这里再强调一遍,因为这个问题我被问过太多次。想彻底不用 swap 就swapoff -a并从/etc/fstab里注释掉,别指望参数。反过来说,完全关闭 swap 在某些场景下是有风险的——没有 swap 作为缓冲,内存压力来临时内核只能选择 OOM,而 OOM 一旦触发,被杀的是谁完全不受控。
第三,参数之间会互相打架。最典型的是同时设置dirty_ratio和dirty_bytes,后者会把前者清零;还有overcommit_memory=2和overcommit_ratio的配合,改了模式不改 ratio 等于用默认值算上限;以及min_free_kbytes和watermark_scale_factor都往上调,叠加效果可能超出预期。我的习惯是每次只动一到两个参数,压测一轮,记录数据,再动下一组。一次性改十个参数然后观察"效果",实际上你什么也学不到。
第四,容器场景要把系统参数和 cgroup 参数分开看。宿主机层面的vm.*决定回收策略,cgroup 的memory.max决定这个组能用多少。如果某个容器被 OOM 杀掉,先看 cgroup 限额是不是太小,再看宿主机整体内存是否吃紧,最后才轮到调vm.*。顺序反了,容易在错误的地方使劲。
第五,调参记录比参数本身重要。我现在习惯在每台机器的配置注释里写清楚:这个值是谁在什么时间因为什么问题改的、依据是什么、压测数据在哪。半年后接手的人(很可能是未来的自己)看到一堆孤零零的数字会非常崩溃。参数不是越"优化"越好,能被理解和维护的配置才是好配置。
第六,别忽视应用层。vm.*参数调得再精细,也架不住应用本身的内存使用模式有问题——比如无节制地缓存、频繁申请大块内存又立刻释放、线程数失控。系统参数是兜底和微调,应用行为才是根本。我处理过的内存问题里,真正需要靠内核参数解决的不到三成,剩下七成最后都定位到了代码或者架构上。
我在实际使用中的体会是,这套参数没有"最佳实践",只有"当前场景下的合理选择"。每次调整之前先问自己三件事:现在的瓶颈到底在哪、我打算用哪个参数去影响它、改完之后用什么指标来验证。三个问题答不上来,就先别改,把监控看明白再说。真正稳的机器,往往不是参数调得最激进的,而是参数和负载匹配、并且有人持续盯着的那台。