1. 这个项目到底在解决什么问题前阵子在做一套内部存储系统的内存专项优化被同事问到一个看似基础、其实很要命的问题服务器明明配了 128GB 内存free看第一行却只有几百 MB 的 free这内存是不是不够用这个问题背后藏着一个很多人没认真对待的工作方向——空闲内存的构建。它不是简单地把内存空出来吃灰而是根据业务真实负载有意识地在系统里规划、保留出一块可用内存资源让系统面对突发流量时不至于因为内存分配不及时而卡顿、换页甚至被 OOM 干掉。这篇文章就把我从指标解读、内核参数调整到压测验证的完整过程拆开说清楚。不管你是做运维、后端开发、SRE 还是 DBA只要你手上有需要长时间稳定运行的服务器这套思路都能直接套用。1.1 被误读的空闲内存先说个最常见的误区。很多人判断内存紧不紧张习惯性只看free命令第一行的 free 字段。看到 free 只有几十 MB就紧张得不行急着去清理缓存、杀进程。其实这是把内存管理机制想简单了。Linux 内核的设计哲学是内存闲着就是浪费。你刚读了一个文件内核会把文件内容留在 page cache 里你访问了一个目录inode 和 dentry 会缓存在 Slab 里。这些内存从数值上看是被占用了但它们随时可以被回收、重新分配给其他用途。所以真正需要关注的是available这个字段也就是在不触发明显 swap 的前提下还能给新程序分配多少内存。说个直观的类比free 字段像是演唱会现场已经空出来的座位而 available 是包括走廊、休息区在内可以临时加座的空间。你盯着已经划定的座位数没用得看整个场馆还能塞下多少人这才是关键。所以构建空闲内存的第一步是先纠正所有人对内存指标的判断标准——不要被第一行的数字带偏要建立以 available 为核心的内存健康度认知。1.2 为什么空闲内存需要专门构建既然内核本身有回收机制为什么还要构建空闲内存直接把业务跑起来内核不就会自动管理吗理论上确实会但实际运行中问题很多。核心矛盾在于内核回收内存是需要时间的。当某个进程突然申请一大块内存而系统发现现有空闲页不够时它会走内存回收路径把 page cache 里的脏页写回磁盘、把匿名页 swap 出去。这段时间里申请内存的进程可能会被阻塞延迟从几十毫秒飙升到几百毫秒甚至秒级。对于高并发服务来说这种延迟抖动往往是致命的。更麻烦的是某些应用申请内存时根本不会提前告知。比如 Java 应用在 YGC 或扩容时突然需要几十 GB 堆内存数据库连接池在高峰期需要为大量新连接分配缓冲——这些场景如果之前没有预留足够空闲内存系统就会在响应速度上栽跟头。构建空闲内存本质上是在内核默认的内存管理策略之上叠加一层水位管理和回收策略的人工干预。目的不是禁止内核使用内存而是确保在任何突发情况下系统都还有一块可以快速分配出去的缓冲区。这和开车保持安全车距是一个道理——你不希望每次刹车都踩到 ABS 抱死。1.3 哪些场景最需要这份空闲不是所有服务器都需要大力构建空闲内存但它对以下几类场景几乎属于刚需场景类型典型表现缺少空闲内存的后果高并发 Web 服务流量瞬时上涨连接数翻倍新连接建立慢接口 P99 延迟飙升数据库 / 缓存系统连接池扩展、索引加载、buffer pool 扩容查询变慢主从同步延迟增大数据分析批处理任务同一时刻多个 Spark/Flink 任务拉起Executor 申请内存失败任务失败重试容器节点Pod 数量快速增加Pod 启动被阻塞节点 NotReady虚拟化宿主机VM 批量迁移或启动内存锁定失败虚拟机启动超时如果你的服务器属于上面任意一类那就值得把构建空闲内存当成一个正式项目来做而不是让它听天由命。2. 构建空闲内存的关键指标与调参细节想构建空闲内存先得把系统当前状态看清楚。我不建议一上来就改内核参数那是本末倒置。正确顺序是先读懂指标再理解参数原理最后才动手调。2.1 先把内存指标读准我最常用的命令组合是free -m配合cat /proc/meminfo。前者适合快速扫一眼后者适合深挖细节。free -m total used free shared buff/cache available Mem: 128441 21051 482 1803 106907 105672 Swap: 4095 0 4095注意看上面这个输出。free 字段只有 482MB旁边 buff/cache 占了 106GB但 available 还有 105GB。这个系统其实非常健康因为那 106GB 的 buff/cache 大部分都能在需要时立刻回收。/proc/meminfo里我最常盯的几个字段字段含义判断要点MemTotal物理内存总量基线值MemFree完全空闲的物理页单独看没意义MemAvailable估算可用内存核心指标建议监控这个Buffers块设备缓冲可回收Cachedpage cache大部分可回收SReclaimable可回收的 Slab 缓存inode/dentry可回收SwapFreeswap 剩余空间如果业务禁止 swap要重点看提示MemAvailable 是内核根据多种因素估算出来的不是简单把 free 加 cache 相加。估算算法涉及内存水位、可回收页比重等我们不需要自己重算直接用内核给出的结果就行。实际判断时我一般看两个维度。一是 available 占总内存的百分比低于 20% 就要警惕二是在业务高峰时段观察 available 是否会跌到某个绝对阈值以下。这两个维度结合比单看 free 靠谱得多。2.2 核心内核参数以及调整原理读懂指标之后才轮到参数层面。构建空闲内存牵扯到的内核参数主要是这几个参数默认值作用构建空闲内存时常见调整vm.swappiness60控制内核回收内存时对匿名页和页缓存的倾向程度调整为 10~30降低业务进程被换出的概率vm.vfs_cache_pressure100控制内核回收 inode/dentry 等 VFS 缓存的速度可调为 150~200加速回收 Slab 缓存vm.min_free_kbytes动态计算强制预留的最小空闲内存水位调整为物理内存的 0.5%~1%vm.overcommit_memory0控制内存超额分配策略保持 0或在严格规划时设为 2vm.drop_caches0手动清理缓存只在调试时使用不作为日常手段逐个说原理。vm.swappiness可能是被误解最多的参数。它不等于系统使用 swap 的阈值而是影响内核在回收内存时更倾向于回收匿名页还是页缓存。数值越高内核越积极把不常用的匿名页换到 swap 去数值越低内核越倾向于优先回收 page cache保住进程内存。默认 60 在服务器场景下偏激进很多业务进程的 heap 会被换到 swap导致时延抖动。我在构建空闲内存时通常会调到 20 左右既能避免 swap又不会让 page cache 的回收变得迟钝。vm.vfs_cache_pressure很多人不熟悉但它对 Slab 内存高的问题非常有效。如果你用slabtop看到大量 dentry 和 inode 缓存占着好几个 GB而且这些内存不大好回收可以适当提高这个值。比如设成 150内核回收 VFS 缓存的速度就会比默认快 50%。不过别贪心设成 500 甚至更高会频繁回收这些缓存导致文件访问性能下降。vm.min_free_kbytes是构建空闲内存最直接的一招。它决定了内核在内存分配路径上强制保留的最低空闲页数量。这个值设得合理可以避免系统在极端情况下出现内存分配死锁或直接回收风暴。但也不是越大越好——它每大一点available 就实实在在少一点因为这部分内存是系统锁定的谁都不能用。vm.overcommit_memory则决定了系统允许进程超额申请多少内存。默认的 0 是启发式策略内核会根据当前空余内存和申请大小粗略判断。对于高确定性场景我建议先保持默认重点做好前三个参数避免一下子改动太多导致应用启动失败。2.3 一个关于参数计算的实际例子光看参数定义不够我来演示一遍实际怎么算。假设服务器物理内存是 64GB我们想预留出足够的空闲水位。先看min_free_kbytes。经验公式是最低空闲内存控制在物理内存的 0.5% 到 1% 之间。64GB 换算成 KB64 × 1024 × 1024 67108864 KB。取 1% 就是 67108864 × 0.01 671088 KB约 655MB。取 0.8% 大约是 536870 KB约 524MB。考虑到这台机器跑的是长时间在线业务我一般取 512MB 左右作为安全水位。注意不要把这数值拍脑袋设成几 GB否则你会把大量可用内存白白锁死。配置完成后可以通过/proc/zoneinfo观察各 zone 的 min、low、high 水位变化确认调整生效。再看 swappiness。这要看业务特点。如果服务是网络应用内存里放着热点数据我建议设 20如果业务跑的是批处理任务换出一些不常用进程也无所谓可以保留默认或调高一点。但构建空闲内存的核心目标是保住业务进程的响应速度所以大多数场景我都会压到 20 附近。至于 vfs_cache_pressure我会先执行slabtop看当前 Slab 总量。如果 Slab 占用超过物理内存的 2%再考虑调到 150否则保持在 100 就好没必要为了调参而调参。注意任何内核参数调整都要在低峰期进行。改完之后观察一个完整业务周期确认 available 和回收延迟都比调整前更稳定再决定是否固化到配置文件里。3. 从摸底到落地的完整实操流程理论讲清楚了下面是我的实操路径。整个过程我习惯分成四个阶段摸底、参数落地、进程规划、压测验证。每个阶段都有可复现的命令和配置你可以照着走一遍。3.1 摸底先把系统内存现状看清楚动手改任何东西之前先摸清家底。这一步不能省否则你后面调参就是盲调。基础摸底命令free -m cat /proc/meminfo | grep -E MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapTotal|SwapFree|SReclaimable vmstat 1 5这三个命令能快速告诉你当前内存分布和 swap 活动。如果vmstat里 si/so 持续非零说明系统一直在换页这是内存偏紧的直接信号。接下来要看历史趋势单看当前时刻不够sar -r 1 # 查看历史内存占用需要 sysstat 包sar -r能看到过去每天、每小时的内存使用记录。重点看两个信息一是有没有内存用满的时间段二是可用内存全天最低值是多少。这两个数据直接决定你要预留多少空闲内存。还要看进程级别的内存占用找出哪些进程在持续膨胀ps aux --sort-rss | head -20这里列出的前 20 个进程就是内存大户。注意看有没有某个进程的 RSS 持续增长、从不清降——这种就是潜在的内存泄漏光靠构建空闲内存是兜不住的得先处理。摸完底我会在表格里记下当前基线项目数值备注总内存64GB物理内存高峰 available 最低值6GB来自 sar -rSlab 总量1.2GBSReclaimable SUnreclaimswap 使用率0%业务上要求尽量不用最大进程 RSS32GB某数据库进程有了这张表后面预留多少空闲、参数怎么定就有依据了。3.2 参数落地把空闲变成可控状态根据摸底结果我在 64GB 内存的服务器上这样配置内核参数。创建配置文件cat /etc/sysctl.d/90-idle-memory.conf文件内容vm.swappiness 20 vm.vfs_cache_pressure 150 vm.min_free_kbytes 524288说明一下每个值怎么来的。swappiness 20是因为这台机器跑的是数据库服务任何一点 swap 都会引起延迟波动所以优先保住匿名页。vfs_cache_pressure 150是因为摸底时看到 Slab 占了不少内存而且服务热点文件也不多可以放心加快 VFS 缓存回收。min_free_kbytes 524288就是 512MB对应前面说的 0.8% 水位。加载配置sysctl --system加载后立即验证sysctl vm.swappiness vm.vfs_cache_pressure vm.min_free_kbytes cat /proc/zoneinfo | grep -A5 Node 0, zone Normal/proc/zoneinfo里的 min、low、high 三个水位会相应变化说明配置生效了。这里有个经验sysctl 命令只对当前运行状态生效重启后失效。所以要确保配置文件放在/etc/sysctl.d/下并在加载路径中。如果需要回滚直接把配置文件删除再执行一次sysctl --system就能恢复默认。3.3 进程与容器层面的内存规划内核参数只是第一层防线进程和容器层面的规划同样重要。我见过很多系统内核参数调得挺漂亮但业务进程一下子把内存全部吃光最后照样出问题。先讲进程层面。如果服务是用 systemd 托管的可以在 unit 文件里加上内存限制[Service] MemoryHigh8G MemoryMax10GMemoryHigh是软限制超过后内核会积极回收该 cgroup 的内存MemoryMax是硬限制不允许超过。这两个值要根据服务的实际内存画像来定不要拍脑袋。比如数据库进程平时占用 6GB峰值 8GB那 High 设 10GB、Max 设 12GB 才有意义否则反而限制了业务扩展。对于 Java 应用-Xmx不要顶着物理内存设。给堆外内存、线程栈、元空间留出余地这个余地就是空闲内存的一部分。我见过有人把-Xmx设成物理内存的 90%结果 JVM 一扩张系统直接 OOM。容器层面同理。如果是 Kubernetes 或裸容器通过 cgroup 限制内存docker run --memory8g --memory-swap8g my-app:latest这里的--memory-swap设成和--memory一样意味着禁止容器使用 swap。这样容器内存被限制在 8GB 以内超出就 OOM不会拖垮整个宿主机。进程规划完成后再次核对空闲内存的计算逻辑物理内存减去所有进程可能占用的最大值剩下的才是真正的空闲。这个值应该不小于你在内核层预留的水位。如果小于就说明给进程的配额给多了需要调整。3.4 验证模拟压力下观察指标变化配置改完、进程限制设好不能直接说完事必须压测验证。我先用 stress 工具模拟内存压力yum install stress # 或 apt install stress stress --vm 2 --vm-bytes 8G --vm-hang 10s --timeout 60s这条命令会拉起两个进程各自申请 8GB 内存并保持 10 秒持续 60 秒。执行期间另开一个终端观察free -h vmstat 1 5 grep -E pgscan_direct|pgscan_kswapd|pswpin|pswpout /proc/vmstat重点看几个指标。第一available 是不是直线下降到接近预留水位后保持稳定。如果 available 直接清零说明预留空间计算有误。第二pswpin/pswpout有没有大幅上涨。如果压力期间 swap 开始剧烈活动说明swappiness或者进程配额没调好。第三pgscan_direct代表内存分配路径上的直接回收次数。这个数值如果急剧上升说明系统频繁走直接回收路径即内存不够用进程申请内存被阻塞这时要检查是不是预留的空间太小了。除了 stress我更推荐直接用业务流量压测。把线上真实请求复制到测试环境流量逐步加大观察 available 曲线的变化。比如正常 QPS 是 1000压到 3000、5000 时系统是否还能保持至少 512MB 的可用内存。如果 available 在压力下跌破预留水位就要回头调大min_free_kbytes或者收紧进程内存配额。验证通过的标准我总结为三条压力期不出现明显 swap、available 不触底、内存分配延迟基本平稳。4. 监控规则与常见问题排查调参和压测只是项目的前半段。参数上线后真正的挑战是持续观察和排查问题。我在这部分把日常巡检要盯的指标和踩过的坑一并整理出来。4.1 日常巡检应该盯哪些指标构建空闲内存不是一次性动作它需要持续监控来确认状态没有漂移。我常用的监控项和告警阈值如下指标采集方式建议告警阈值MemAvailablefree或cat /proc/meminfo低于总内存的 20% 或低于预留值 1.5 倍swap 使用率free或vmstatsi/so使用率持续 0内存回收事件/proc/vmstat的 pgscan_*pgscan_direct 明显增长进程 RSS 排行ps aux --sort-rss某进程 RSS 超过设定阈值OOM 事件dmesg -Tgrep -i oom如果要自动化可以写一个简单的巡检脚本放到 crontab 里定时执行#!/bin/bash # 空闲内存巡检 AVAILABLE$(awk /MemAvailable/{print $2} /proc/meminfo) # 单位为 KB if [ $AVAILABLE -lt 1048576 ]; then echo $(date) 警告: available内存低于1GB当前${AVAILABLE}KB /var/log/mem_check.log fi SWAP_USED$(free -k | awk /Swap:/{print $3}) if [ $SWAP_USED -gt 0 ]; then echo $(date) 警告: swap已使用 ${SWAP_USED}KB /var/log/mem_check.log fi这个脚本的核心逻辑很简单available 低于预设阈值就告警swap 一旦使用就告警。不要嫌简单很多时候内核参数的异常漂移就是靠这种基础巡检抓出来的。另外有条件的话把指标接入可视化监控系统绘制 available 和 swap 的七日趋势图。空余内存的健康状态看单点数值没用要看趋势线是不是平稳。如果 available 逐日下降说明系统里有进程在持续吃掉内存要马上排查。4.2 高频问题与排查思路下面这几个问题是我在实际操作中反复遇到、也帮别人排查过多次的按出现频率排序。第一free 显示很小但系统完全不卡要不要处理不要处理。遇到这个问题的人往往是把 free 当成了唯一指标。只要 available 充足、swap 没动、业务无延迟告警这就是内核缓存策略的正常表现。缓存不是为了占地方是为了提升性能强行清掉反而会拖慢服务。第二cache 居高不下总觉得碍眼频繁清理缓存有用吗短期内有用长期看是治标不治本。page cache 就是内核拿来加速文件读取的它占得越多说明你在读写的文件越多。如果业务规律就是频繁读取大量文件cache 高反而是好事。真正要警惕的不是 cache 高而是 cache 高且回收慢、回收时导致业务停顿。这种情况要查的是vfs_cache_pressure和磁盘 IO 能力不是简单地清缓存。第三swappiness 都调到 20 了为什么还在 swap这是最容易被误判的情况。要理解 swappiness 只是调整回收倾向不是禁掉 swap。如果系统在瞬间收到大量内存申请而 available 不足以满足内核照样会走 swap 路径。排查时先看是谁在申请内存用dmesg查有没有进程触发大规模内存分配再检查 cgroup 内存限制是否被冲破。如果瞬时申请量确实巨大需要做的是增加预留内存或限制进程内存上限而不是继续调 swappiness。第四系统一直 OOM但 dmesg 里看不到 OOM 进程这种情况通常发生在容器或 cgroup 场景里。宿主机没有 OOM但某个 cgroup 触发了 OOM。排查步骤是先查 cgroup 事件systemd-cgls cat /sys/fs/cgroup/memory/memory.events如果是容器平台查看容器的事件日志。cgroup 的 OOM 记录里会明确告诉你哪个 cgroup 超限而不是哪个进程。第五修改了参数重启后全部恢复默认这就是没有把配置写进持久化文件导致的。sysctl -w只对当前运行内存有效重启就丢失。正确做法是写入/etc/sysctl.d/下的 conf 文件并确保文件名数字前缀在加载顺序里靠后避免被默认配置覆盖。4.3 关于清理缓存的正确认知最后一定要单独说说清理缓存这件事因为这是运维人员最容易操作失误的地方。drop_caches的完整用法是sync echo 1 /proc/sys/vm/drop_caches其中echo 1清理 page cacheecho 2清理目录项和 inodeecho 3全部清理。这组操作被很多人当成调优工具每当看到 free 第一行很小就执行一次。但我要强调drop_caches只是一个调试工具不是日常运维手段。为什么因为 page cache 的作用是缓存你经常访问的文件内容。你把缓存清掉下次读取同一个文件时内核必须重新从磁盘加载读盘延时瞬间放大线上服务很容易出现慢查询或接口超时。我见过有人为了把 free 数值修好看天天在高峰期执行 drop_caches结果业务 P99 延迟翻了好几倍。如果确实需要在一台内存非常紧、你又不想重启服务的机器上临时腾空间执行drop_caches之前必须满足两个前提一是业务处于低峰期二是这些缓存里的文件不是业务高频访问的热点文件。构建空闲内存的正确方式永远是靠水位管理、回收策略和进程限制这套组合拳而不是靠手动清缓存来挤内存。这套项目做下来我最大的体会是很多人追着 free 第一行数字纠结其实是在和内核的内存管理机制对抗方向就错了。真正要做的是理解内核如何评估可用内存、如何回收内存然后顺着它的逻辑给它设定边界。空闲内存不是靠清理出来的是靠规划出来的——规划物理容量、规划进程配额、规划回收水位把万一不够用的情况提前想清楚系统才能在突发流量面前真的稳住。