
最近在处理一台老服务器的性能问题时我发现系统可用内存告急但进程列表里却看不出哪个应用在吃内存。这种“内存去哪了”的悬案多半和Linux的页面缓存Page Cache脱不了干系。今天就把这套排查和清理思路完整梳理一遍也算是对自己踩坑经历的一次归档。1. 内容整体设计与思路拆解1.1 核心需求解析服务器运行时间一长free -h 看到的可用内存越来越少但 top 里查进程内存占用却又都正常。这种现象十有八九是 Linux 内核把空闲内存用作了缓存。内核这么做本身是好事因为读写过的文件数据会被暂存在内存里下次访问就能直接从内存命中速度能快好几个数量级。可问题是当某个程序突然需要申请大块内存时内核虽然有回收机制但如果回收不及时或者回收能力有限就会表现为“内存不足”或者系统卡顿。所以本篇的核心需求可以拆成三个层面第一搞清楚系统内存到底用在哪里区分“真的不够用”和“只是被缓存占着”第二掌握手工释放内存缓存的操作方法知道什么时候该清、什么时候不能乱清第三学会在清理后观察系统表现确认清理动作是否有效同时避免因为误操作引发性能回退。1.2 方案选型背后的取舍我第一次遇到这种问题直接就是一连串的 echo 3 /proc/sys/vm/drop_caches把页缓存、目录项缓存、inode 缓存全给清了一遍。当时确实立竿见影内存蹭蹭往上涨但代价也很明显——缓存命中率降到零后续 IO 负载飙升数据库查询明显变慢。后来才慢慢意识到清理内存缓存不是简单地跑几条命令而是要在“腾出空闲内存”和“保留热数据缓存”之间做权衡。如果只是临时运行大任务那可以清得彻底一些如果是生产环境跑着数据库或高频 Web 服务清完缓存后的短暂性能波动可能比内存不足更难受。所以方案设计的核心思路是先准确判断内存状态再决定是否清理最后选择合理的清理等级。不是所有场景都适合一刀切也不是清得越狠越好。1.3 适用场景与目标读者这篇内容主要面向被 free 告警和 OOM 困扰的系统运维同学自己搭服务器、跑应用但对内核内存机制不太熟的开发者以及想深入理解 /proc 内存文件、想搞明白缓存回收原理的 Linux 爱好者。读完之后你至少能回答这几个问题内存都去哪了怎么判断要不要手动清理清理时选 1 还是选 2 还是选 3清理完如何验证效果2. 内存缓存的原理与瓶颈剖析2.1 理解 Linux 的“空闲内存”哲学Linux 的设计哲学里有一条内存闲着也是闲着不如拿来缓存。你在系统里每读一个文件内核都会把文件内容放进 Page Cache每访问一个目录目录项也会被缓存起来这就是 dentry cache文件属性信息则存在 inode cache 里。这些缓存在大大加速文件访问的同时也让 free 命令输出里 available 和 free 的差距显得特别大。来看一个典型的 free -h 输出$ free -h total used free shared buff/cache available Mem: 7.6G 2.1G 1.2G 145M 4.3G 5.1G Swap: 2.0G 0B 2.0G这里 used 是 2.1Gfree 只有 1.2G看着像内存不够。但 buff/cache 有 4.3Gavailable 是 5.1G。available 是内核估算的“在不触发 swap 的情况下还能分给新进程的内存”它考虑了缓存的可回收性。也就是说系统其实并不缺内存缺的只是“没被用于缓存的空闲内存”。但这个逻辑在遇到突发内存申请时有隐患。万一某个进程瞬间申请大内存内核回收缓存需要时间极端情况下就把 OOM Killer 给招惹出来了。所以理解 available 和 free 的区别是判断是否要手动清理的第一道分水岭。2.2 buff/cache 的构成拆解buff/cache 这个数字是一个集合进一步拆开包括Page Cache文件页缓存最核心的部分读文件、写文件都会用到dentry cache目录项缓存保存目录和文件名到 inode 的映射关系inode cache文件元数据缓存Buffer Cache早期的块设备缓冲现在很多已经融入 Page Cache但在一些块设备读写场景里仍有体现。在 /proc/meminfo 里有对应的字段$ grep -E ^(Buffers|Cached|SReclaimable|Shmem) /proc/meminfo Buffers: 110336 kB Cached: 3486764 kB SReclaimable: 420392 kB Shmem: 145408 kBCached 主要就是 Page Cache 的大小SReclaimable 是内核中可回收的 slab 内存Shmem 是 tmpfs 占用的内存。tmpfs 文件占用的内存实际上算在 Cached 里但这部分没法通过 drop_caches 释放因为你得先把文件删掉。2.3 回收机制的触发条件内核回收内存缓存主要靠两条路径一条是内存压力触发。当系统可用内存低于某个水位线watermarkkswapd 内核线程会开始回收页缓存、压缩内存碎片、换出匿名页另一条是直接回收。进程申请内存时如果 kswapd 来不及回收会同步进入直接内存回收路径这时进程的申请延迟会显著增加。手动执行 drop_caches 其实相当于绕过水位线机制强制让内核立即丢弃缓存。但这不等于内核的自动回收失效只是你给了它一个“现在就要”的信号。生产环境下如果频繁手动清缓存反而会掩盖真实的自动回收问题。3. 实战三种场景下的内存缓存清理方案3.1 查看当前内存和缓存状态动手清理前务必先确认现状。我常用的命令组合是# 以人类可读方式查看内存概览 free -h # 查看 /proc/meminfo 关键字段 grep -E ^(MemTotal|MemFree|MemAvailable|Buffers|Cached|Shmem) /proc/meminfo # 查看当前缓存相关参数 cat /proc/sys/vm/drop_caches/proc/sys/vm/drop_caches 默认值一般是 0。它的取值含义是值含义0不清除任何缓存默认1仅清除 Page Cache2清除 dentry 和 inode 缓存3同时清除 Page Cache、dentry 和 inode 缓存如果 available 比 free 大好几个 G说明缓存很充沛根本不用清只有当 available 已经接近物理内存总量、free 非常低、且系统开始出现换页或性能异常的时候才需要考虑手动干预。3.2 场景一临时运行大任务前的缓存清理假设你马上要在服务器上跑一个内存需求量很大的任务比如编译大型项目、加载大规模数据集而当前可用内存确实紧张。这时可以先做缓存清理给新任务腾出空间。第一步先同步脏页把尚未落盘的缓存数据写回磁盘。这一步很关键否则直接丢缓存可能造成数据丢失sync第二步清理页缓存echo 1 /proc/sys/vm/drop_caches第三步验证效果free -h在这个场景里我会先清 1再看结果。如果内存还是不够再考虑清 2 甚至清 3。因为页缓存是占内存的大头先把大头处理掉通常就够了。3.3 场景二生产环境的高效清理保留热数据生产环境清缓存最怕的就是误伤热数据。一个折中做法是只清 dentry 和 inode 缓存不动页缓存。因为目录项和 inode 缓存占用的内存相对较少清完之后的性能回退远小于清页缓存。sync echo 2 /proc/sys/vm/drop_caches这样做的好处是那些频繁读写的文件数据还留在 Page Cache 里对数据库、Web 服务而言热数据缓存不丢性能波动很小。缺点也很明显——释放的内存总量有限。所以这个方法适合“数据库可用内存偏低但还没到危险线”的场合不适合“内存彻底不够”的紧急情况。还有一种更精细的做法就是写一段小脚本把 drop_caches 和阈值判断结合起来#! /bin/bash AVAILABLE$(awk /MemAvailable/{printf %d, $2/1024} /proc/meminfo) TOTAL$(awk /MemTotal/{printf %d, $2/1024} /proc/meminfo) THRESHOLD$((TOTAL * 2 / 10)) if [ $AVAILABLE -lt $THRESHOLD ]; then sync echo 1 /proc/sys/vm/drop_caches echo $(date): memory available ${AVAILABLE}MB below threshold ${THRESHOLD}MB, cache dropped /var/log/cache-clean.log fi这段脚本的意思是当可用内存低于总内存 20% 时自动执行一次缓存清理。你可以用 crontab 把它挂到每 5 分钟跑一次充当一道自动回收之外的补充保险。不过要记住这只是治标手段真正的问题还是得从应用和内核参数层面找。3.4 场景三彻底释放内存的强制清理如果系统已经出现交换分区占用持续增长、free 几乎为零、甚至 OOM 事件开始出现这时候就别手下留情了直接清到最彻底sync echo 3 /proc/sys/vm/drop_caches清理完的瞬间可用内存会明显上升。可以配合一下内存压缩开关的调整让后续回收更积极# 提高内存压缩倾向默认值通常是 20 echo 60 /proc/sys/vm/vfs_cache_pressurevfs_cache_pressure 控制内核回收 dentry/inode 缓存的倾向性。默认 100 表示内核倾向于保持缓存数值越大回收越积极。设成 60 到 80 之间既能避免目录缓存被过早丢弃又能在内存紧张时更快回收。但有一点要特别强调echo 3 绝不是万能解药。有些内存根本不在这些可回收缓存里——比如不可回收的 slab 内存、内核栈、每个进程自己的匿名页。如果 drop_caches 之后内存依然紧张说明系统瓶颈在别处得从进程和内核参数入手排查而不是反复跑 echo 3。4. 关键参数解析与性能影响分析4.1 drop_caches 文件的内核实现逻辑/proc/sys/vm/drop_caches 是一个伪文件写入数值并不会真的立即释放内存而是给内核一个回收提示。内核的收缩机制会遍历所有 zone 的 LRU 链表把对应的页面移出缓存。这里有个很多人不知道的细节写入 drop_caches 后free 命令显示的内存量是逐渐下降的因为内核回收页面的过程是异步的尤其在系统本来就忙碌的情况下回收可能分好几轮才完成。我之前刚写完 echo 3马上看 free 发现没变化还以为命令没生效其实是内核还在忙活。稍等 10 到 30 秒再观察比较稳。4.2 不同参数的实际效果差异表为了直观看出三种参数的差异我用同一台压力测试机分别跑了一轮对比操作执行前 available执行后 available粗略释放量echo 11.8G3.6G约 1.8Gecho 23.5G4.1G约 0.6Gecho 31.7G4.4G约 2.7Gecho 1 清的是文件内容缓存释放效果最明显echo 2 清的是目录和元数据缓存占用小但影响文件访问路径echo 3 是双管齐下。所以如果是普通文件服务器echo 1 性价比最高如果是 Web 服务器或代码仓库目录项缓存也很重要动 echo 2 要小心只有在批量任务或者压测场景里我才会用 echo 3 求一个清净。4.3 缓存清理后为何性能反而下降这是一个很反直觉的现象但非常重要清理缓存本身会降低后续性能。清缓存前进程读取一个文件如果文件内容还在页缓存里那么 read 操作直接从内存返回快如闪电。清完缓存后同一份文件必须从磁盘重新读入内存磁盘 IO 延迟是内存延迟的好几个数量级程序响应速度自然就掉下来了。对数据库这种重度随机读负载来说清一次 Page Cache最直接的后果就是查询延迟从毫秒级飙到几十毫秒级直到热数据页重新被加载回缓存。所以我的建议是缓存清理只适合在几个场景里用应用已经停止服务或即将重启缓存留着也是浪费准备运行与现有服务无关的大任务需要优先保证新任务内存系统即将 OOM需要时间窗口去排查根治问题。日常运行中宁可让缓存占着也不要手贱去清。内核的自动回收机制在绝大多数场景里都比手动干预靠谱。5. 常见问题与排查技巧实录5.1 问题一为什么 free 显示可用内存很少但进程内存正常有一次同事发来一台机器的截图free 显示 available 才 1G 出头但是 ps aux --sort-%mem 看下来top5 进程加起来也不到 2G很奇怪。排查过程是这样的# 先看是不是共享内存或 tmpfs 占了大头 grep Shmem /proc/meminfo # 再看 slab 缓存不可回收的部分有多少 grep -E ^(SReclaimable|SUnreclaim) /proc/meminfo结果发现是 tmpfs 文件占用了几百 MB加上不可回收的 slab 有 700 多 MB。tmpfs 的文件占用的内存会计入 Cached但通过 drop_caches 释放不掉只能删文件。SUnreclaim 有一部分是内核数据结构同样无法回收得靠内核模块或长时间运行后自动整理。后来我又发现一个常见原因某个进程大量读写小文件生成了海量 inode 缓存。这些缓存看着小数量一多就积少成多。解决思路是关掉不必要的文件访问监控、日志索引类服务或者考虑给系统加内存单纯清理缓存只能临时缓解。5.2 问题二执行 drop_caches 后缓存会自动重建吗会而且重建速度往往很快。如果你清完 1 分钟后看 free发现 buff/cache 又涨回去了这不是没清干净而是系统在正常工作持续的文件读写又把缓存填了回来。只要系统还在运行数据被访问过内核就会重新缓存。这是正常现象。有一个细节值得关注drop_caches 对“正在被写入脏页缓存”的影响很小因为脏页必须先写回磁盘才能释放。如果你通过 echo 3 强制清理时系统还挂着一批没写完的脏页回收过程实际上会先触发一次 flush等落盘后再释放。这就是为什么我强烈建议前面先跑 sync 的原因——把落盘动作提前释放过程会更干脆。5.3 问题三清理缓存会不会影响正在运行的数据库分两层说。从数据安全角度drop_caches 只回收干净页缓存和元数据缓存不会把进程已经 mmap 的私有页清掉更不会动数据库的事务日志和缓冲池。数据库自身的 Buffer Pool 是进程内存的一部分跟内核页缓存是两码事所以清内核缓存不会丢数据库数据。从性能角度如果数据库重度依赖操作系统文件缓存比如某些小型数据库配置里 innodb_flush_method 设成默认值文件读写走 Page Cache那清掉内核页缓存意味着所有文件读取都得重新走磁盘短时间内的延迟飙升不可避免。更麻烦的是清完缓存后系统 IO 压力骤然加大磁盘队列深度会涨一波可能引发连锁性能问题。建议的操作方式是选择业务低峰期执行清完后盯着 vmstat 和 iostat 观察几分钟确认 IO 没有长时间持续高负载。5.4 问题四如何确认清理是否生效推荐看三个指标而不是单看 free# 1. 查看页缓存变化 grep -E ^(Cached|Buffers) /proc/meminfo # 2. 观察系统交换情况确认没在换页 vmstat 5 # 3. 看 Page Cache 命中情况需要 perf 或 cachestat 工具 cachestat 1 5cachestat 来自 perf-tools 工具集安装后可以直观看到缓存命中率。如果清理前命中率 95% 以上清理后跌到 60% 以下说明热数据还没重新加载这时候业务如果对性能敏感可以考虑预加载关键数据文件来加速回温。5.5 我的独家避坑清单不要在频繁读写的高峰期清缓存宁可等业务峰谷永远先 sync再 echo顺序不能反echo 的值尽量按需选择一般不需要一上来就上 3设置 vfs_cache_pressure 要考虑持久化写进 /etc/sysctl.conf 才是永久生效多留意 /proc/meminfo 里的 Shmemtmpfs 类内存靠清理缓存是释放不了的最重要的是drop_caches 是应急手段不是日常调优方案。如果服务器长期依赖手动清缓存才能维持运行那要解决的是应用内存泄露、文件缓存策略或者物理内存扩容而不是反复按压这个“刷新键”。6. 从清理到预防一套可落地的内存观察流程解决问题不能只停留在“按一下按钮”。我现在的习惯是给每台重要服务器都配一个简单的内存巡检脚本每天记录内存状态然后落到日志文件里。脚本大概长这样#! /bin/bash LOG_FILE/var/log/memory_daily.log { echo date %Y-%m-%d %H:%M:%S free -h grep -E ^(MemTotal|MemFree|MemAvailable|Buffers|Cached|Shmem) /proc/meminfo echo Top 5 memory processes: ps aux --sort-%mem | head -6 } $LOG_FILE用 cron 每天早上 8 点跑一次。这样一来连续观察几天你就能看出内存是不是持续被缓存吃满、有没有进程内存逐步走高、available 是不是长时间贴着红线。有了这个基线数据你就能判断到底需不需要调整 vm.swappiness、vm.vfs_cache_pressure 这些内核参数而不是等到收到告警才手忙脚乱去清缓存。另外一个实用的预防手段是调整 swappiness。默认值 60 表示系统在内存可用率低于 60% 时就开始考虑把匿名页换到 swap。对服务器来说这个值可以调到 10 甚至 0让它更倾向于保留匿名页减少不必要的交换。sysctl vm.swappiness10 echo vm.swappiness 10 /etc/sysctl.conf但注意swappiness 改小后内存压力下内核更偏爱回收文件缓存而不是换出匿名页这可能导致文件读性能下降。这又是一个需要结合业务形态去权衡的参数。7. 扩展实操结合 systemd 实现自动化缓存管理如果你懒得每天手工敲命令用 systemd 定时器做一个自动化缓存清理服务也是一个不错的选择。先写一个清理脚本比如放在 /usr/local/bin/drop-cache.sh#! /bin/bash sync echo 1 /proc/sys/vm/drop_caches再创建一个 systemd service 文件 /etc/systemd/system/cache-clean.service[Unit] DescriptionDrop page cache when memory is low Aftermulti-user.target [Service] Typeoneshot ExecStart/usr/local/bin/drop-cache.sh再创建对应的 timer[Unit] DescriptionRun cache-clean every hour [Timer] OnCalendarhourly Persistenttrue [Install] WantedBytimers.target最后启用systemctl daemon-reload systemctl enable cache-clean.timer systemctl start cache-clean.timer这时候系统每小时清一次页缓存。不过说实话这种方案有点粗暴我建议只在开发测试机上这么搞。生产环境还是用之前那种带内存阈值的脚本更稳妥配合 systemd timer 同样可以实现定时检查、按需清理。现在我的习惯是先靠自动回收机制解决 90% 的问题再用巡检脚本观察趋势偶尔用一次手动清理处理特殊情况。这样既不会让内存缓存失控也不会因为频繁清理拖累性能。希望这套从原理到实操的完整思路能让你下次再看到“内存又满了”的告警时心里有数手上有招。