最近在处理一台老服务器的性能问题时,我发现系统可用内存告急,但进程列表里却看不出哪个应用在吃内存。这种“内存去哪了”的悬案,多半和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.1G,free 只有 1.2G,看着像内存不够。但 buff/cache 有 4.3G,available 是 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 回收机制的触发条件
内核回收内存缓存主要靠两条路径:
- 一条是内存压力触发。当系统可用内存低于某个水位线(watermark),kswapd 内核线程会开始回收页缓存、压缩内存碎片、换出匿名页;
- 另一条是直接回收。进程申请内存时如果 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 Cache |
| 2 | 清除 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 1 | 1.8G | 3.6G | 约 1.8G |
| echo 2 | 3.5G | 4.1G | 约 0.6G |
| echo 3 | 1.7G | 4.4G | 约 2.7G |
echo 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 里的 Shmem,tmpfs 类内存靠清理缓存是释放不了的;
- 最重要的是,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.swappiness=10 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] Description=Drop page cache when memory is low After=multi-user.target [Service] Type=oneshot ExecStart=/usr/local/bin/drop-cache.sh再创建对应的 timer:
[Unit] Description=Run cache-clean every hour [Timer] OnCalendar=hourly Persistent=true [Install] WantedBy=timers.target最后启用:
systemctl daemon-reload systemctl enable cache-clean.timer systemctl start cache-clean.timer这时候系统每小时清一次页缓存。不过说实话,这种方案有点粗暴,我建议只在开发测试机上这么搞。生产环境还是用之前那种带内存阈值的脚本更稳妥,配合 systemd timer 同样可以实现定时检查、按需清理。
现在我的习惯是,先靠自动回收机制解决 90% 的问题,再用巡检脚本观察趋势,偶尔用一次手动清理处理特殊情况。这样既不会让内存缓存失控,也不会因为频繁清理拖累性能。希望这套从原理到实操的完整思路,能让你下次再看到“内存又满了”的告警时,心里有数,手上有招。