1. 从一次“内存告急”的误判说起
前阵子帮一位朋友看他那台跑得好好的服务器,监控面板上内存使用率常年贴着 90% 以上,他一口咬定是程序有内存泄漏,准备连夜重启。我让他先别急,敲了free -h一看,used那一栏确实吓人,但紧跟着的buff/cache也占了一大块,而available还剩好几个 G。问题根本不在泄漏,而是他把Buffer 与 Cache当成了“被吃掉的可用内存”。这个误会我在一线见过太多次,几乎每隔一段时间就要给人解释一遍。
Linux 内存里的Buffer和Cache,说白了就是内核拿来“借”的缓存空间:Buffer 偏向块设备层面的原始数据块缓存,Cache 偏向文件内容页缓存,两者合起来在free命令里通常显示为buff/cache。它们能做什么?能极大减少磁盘 I/O、加速文件读写、让系统在内存富余时把闲置资源利用起来;当应用真正需要内存时,内核又会主动回收这部分缓存。它解决的核心问题是“内存闲着也是闲着,不如拿来提速”,同时保证“真要用的时候能还回去”。这篇内容适合谁看?刚接触 Linux 运维的新手、被监控面板吓到过的后端开发、以及想搞明白free输出到底怎么读的同行。我会从设计思路讲到实操排查,把这块最容易踩坑的知识点掰开揉碎。
2. 整体设计思路:内核为什么要把内存“借”出去
2.1 闲置内存是浪费,缓存是双赢
理解 Buffer 与 Cache 的第一步,是接受一个前提:物理内存闲着不用,本身就是一种浪费。磁盘的读写速度和内存差着好几个数量级,一次机械盘随机读可能是毫秒级,而内存访问是纳秒级。如果每次读文件都老老实实走磁盘,系统性能会被拖垮。内核的算盘打得很精——把读过的文件页、写过的数据块留在内存里,下次再访问同一份数据直接命中内存,省掉一次磁盘往返。这就是 Cache 的本质:用空间换时间。
那为什么还要分 Buffer 和 Cache 两个概念?因为它们缓存的对象不同。Buffer 缓存的是块设备上的原始数据块,比如文件系统元数据、目录项、超级块这类底层结构,它不关心文件内容长什么样,只按块来搬运;Cache 缓存的是文件内容页,也就是你cat一个文件时真正读到的那些字节。早期内核里这两者界限清晰,后来随着页缓存(page cache)统一管理,很多场景下它们已经融合,但free命令依然习惯性地把它们合并展示为buff/cache。你只要记住一句话:它们都是可回收的缓存,不是被程序占死的内存。
2.2 可回收是底线,OOM 之前先“挤水分”
设计上最关键的一条约束是:缓存必须可回收。内核在分配缓存时用的是“低优先级”策略,一旦应用通过malloc、mmap等方式真正申请内存,而空闲内存不够,内核就会触发回收(reclaim),把干净的缓存页直接丢弃,脏页先回写磁盘再丢弃。这个过程对应用是透明的,应用只会感觉到“内存申请成功了”,并不会感知到背后缓存被腾挪。
正因为有这层机制,free命令才专门提供了available这一列。available估算的是“在不触发交换的前提下,还能给新应用用多少内存”,它已经把大部分可回收的buff/cache算进去了。很多人只看used就慌,其实应该看available。我个人的经验是:只要available还有余量,buff/cache再高都不用管;反过来,如果available逼近零、同时swap开始频繁读写,那才是真需要介入的信号。这个判断逻辑,比盯着used百分比靠谱得多。
2.3 为什么不干脆把缓存限制死
有人会问:既然缓存会让人误判,为什么不设个上限,别让它涨那么高?这就涉及取舍了。如果给缓存设一个很小的硬上限,内存富余时也无法充分利用,等于白白浪费提速机会;设得太大又失去限制意义。内核选择的是动态平衡:平时尽量多缓存,压力大时按 LRU(最近最少使用)等策略回收。这种“弹性借用”的思路,和数据库连接池、线程池的设计哲学其实是一回事——资源池化、按需分配、闲时复用。理解了这层设计意图,你就不会再把buff/cache当成敌人,而是把它看作内核帮你打工的成果。
3. 核心细节解析:Buffer 与 Cache 到底缓存了什么
3.1 Buffer 与 Cache 的分工对照
虽然现代内核里两者在实现上高度融合,但从概念和观测角度区分它们仍然有价值。下面这张表是我平时给人讲解时常用的对照,帮你快速建立印象:
| 维度 | Buffer | Cache |
|---|---|---|
| 缓存对象 | 块设备原始数据块、文件系统元数据 | 文件内容页、可执行文件代码页 |
| 典型场景 | 目录遍历、inode 读取、超级块操作 | 读写普通文件、加载动态库 |
| 回收优先级 | 相对更易回收(元数据可重建) | 脏页需回写后回收,干净页直接丢 |
| 观测手段 | free合并显示,/proc/meminfo有Buffers | /proc/meminfo的Cached字段 |
| 生活类比 | 图书馆的索引卡片 | 图书馆里被翻开的书页 |
需要说明的是,/proc/meminfo里的Buffers和Cached是两个独立字段,而free命令默认把Buffers + Cached + SReclaimable合并成buff/cache展示。SReclaimable是可回收的 slab 内存(比如 dentry、inode 缓存),也属于“能还回去”的部分。所以你在free里看到的数字,往往比单纯Buffers + Cached还要大一些,这是正常的。
3.2 页缓存(page cache)才是大头
实际生产环境里,占buff/cache绝大部分的是页缓存。你读一个大日志文件、跑一次数据库全表扫描、加载一个几百兆的模型文件,这些内容都会被页缓存留下来。页缓存以“页”为单位管理,常见页大小是 4KB,内核用基数树(radix tree)等结构快速定位某个文件偏移对应的页是否在缓存中。命中就返回,未命中才发起磁盘 I/O。
这里有个容易被忽略的细节:写操作也会进缓存。你echo一段内容到文件,数据先写进页缓存并标记为“脏页”,然后由内核的后台回写线程(如flush内核线程)择机刷盘。这就是为什么突然断电可能丢最近写入的数据——它们还在缓存里没落盘。理解这一点,你就明白sync命令的意义了:强制把脏页刷到磁盘。我见过有人写脚本批量生成文件后立刻断电测试,结果数据丢失还怪文件系统,其实就是没等回写完成。
3.3 缓存回收的触发条件与顺序
回收不是随便乱丢的,内核有一套优先级。大致顺序是:先回收干净的页缓存(丢弃即可,无成本),再回收可回收的 slab,然后回写脏页再回收,最后才考虑换出匿名页到 swap。这个顺序背后的逻辑很直白——回收成本越低越先做。干净页丢弃零成本,脏页要写磁盘有 I/O 成本,匿名页换出到磁盘成本更高,所以排在最后。
触发回收的时机主要有两个:一是内存分配时发现空闲页低于水位线(watermark),二是内核后台线程定期检查。水位线分min、low、high三档,低于low就开始积极回收,低于min就进入较激进的回收甚至触发 OOM。你在/proc/zoneinfo里能看到这些水位值。日常排查时如果发现系统频繁回收但available仍够,通常说明缓存被反复建立又丢弃,可能存在“缓存颠簸”,这时候要考虑是不是某个程序在反复读大文件却只用一次。
4. 实操过程:从观测到验证的完整链路
4.1 用 free 和 /proc/meminfo 读懂内存现状
第一步永远是观测。free -h是最常用的入口,但很多人只扫一眼used就下结论。正确的读法是重点看三列:buff/cache、available、swap。我通常还会加-w参数看宽格式,把buff和cache分开显示:
free -hw输出里buff和cache会分列,方便你判断到底是元数据缓存多还是文件页缓存多。接着看/proc/meminfo拿更细的字段:
grep -E "MemTotal|MemFree|MemAvailable|Buffers|^Cached|Dirty|Writeback|SReclaimable" /proc/meminfo这里几个字段值得盯:Dirty是待回写的脏页量,长期偏高说明磁盘写入跟不上;Writeback是正在回写的量;SReclaimable是可回收 slab。我一般会连续采样几次,看Dirty是否持续增长,如果一直涨而不降,可能是磁盘 I/O 瓶颈或者回写参数不合理。
4.2 制造缓存并观察其增长
光看不动手印象不深,我建议你亲手做一次实验。找一个几百兆的文件(没有就临时生成一个),先清一下缓存再读它,观察buff/cache的变化。清缓存的命令需要谨慎,生产环境别乱敲:
# 仅测试环境使用,生产慎用 sync echo 3 > /proc/sys/vm/drop_caches free -hdrop_caches写 1 清页缓存,写 2 清可回收 slab,写 3 两者都清。清完你会看到buff/cache骤降、free上升。然后读一个大文件:
dd if=/dev/zero of=/tmp/testfile bs=1M count=500 cat /tmp/testfile > /dev/null free -h再对比一次,buff/cache应该明显回升。这个实验能让你直观感受到“缓存是读出来的”。注意drop_caches只是清理缓存,不会影响正在运行的程序数据,但会短暂降低性能,所以只适合测试环境验证,别写进定时任务。
4.3 用 vmstat 和 iostat 交叉验证
单看内存不够,要结合 I/O 指标判断缓存是否在起作用。vmstat 1里的bi、bo是块设备读写速率,si、so是 swap 换入换出。如果buff/cache很高而bi很低,说明读操作大量命中缓存,这是好事;如果bi持续很高、buff/cache却涨不上去,可能是缓存被频繁回收,或者读的是大量一次性数据。再配合iostat -x 1看磁盘%util和await,就能判断瓶颈到底在内存还是磁盘。
我常用的一个判断口诀:缓存高 + I/O 低 = 健康;缓存低 + I/O 高 = 可能缺缓存或数据一次性;缓存高 + swap 活跃 = 真内存压力。这套组合拳比单看一个数字可靠得多。
4.4 调整缓存行为的几个关键参数
如果确实需要干预缓存行为,/proc/sys/vm/下有几个参数值得了解。vm.swappiness控制换出匿名页的倾向,默认 60,数据库类场景常调到 10 甚至更低,减少不必要的 swap;vm.dirty_ratio和vm.dirty_background_ratio控制脏页比例阈值,前者是进程同步回写的触发点,后者是后台回写的触发点,写密集型场景适当调低可以避免一次性大量回写造成卡顿;vm.vfs_cache_pressure控制回收 inode/dentry 缓存的倾向,默认 100,调高会更积极回收元数据缓存。
这些参数没有放之四海皆准的值,得结合业务读写模式来调。我的建议是先观测再调参,一次只改一个,改完观察至少一个业务周期。见过太多人一口气改五六个参数,出问题根本不知道是哪个引起的。
5. 常见问题与排查技巧实录
5.1 内存使用率高但服务正常,要不要处理
这是最高频的问题。答案很明确:看available,不看used。只要available还有合理余量、swap没有频繁换入换出、业务延迟正常,buff/cache高就是健康状态,不需要任何处理。强行drop_caches反而会让后续读操作重新走磁盘,性能下降。我一般会跟人说:缓存高说明内核在帮你干活,你该高兴才对。
5.2 缓存涨到把内存吃满,是不是泄漏
缓存理论上不会“吃满到无法回收”,因为回收机制一直在。如果真出现available极低且缓存不降,要排查两个方向:一是是否有大量脏页堆积导致回收受阻(看Dirty是否很高、磁盘是否写不动),二是是否有程序通过mmap等方式持有大量不可回收的映射。前者调回写参数或排查磁盘,后者得看具体进程的smaps。单纯把锅甩给缓存是不对的。
5.3 drop_caches 之后性能反而变差
这几乎是必然的。清掉缓存后,原本命中内存的读操作全部回落到磁盘,短时间 I/O 飙升、延迟上升。所以drop_caches只应作为测试手段或极端排障手段,绝不能当成“释放内存”的常规操作。我见过有运维脚本定时清缓存,结果数据库查询延迟周期性抖动,查了半天才发现是这个脚本干的。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 | 处理建议 |
|---|---|---|---|
used高但服务正常 | 缓存占用 | 看available、swap | 无需处理 |
available极低、swap 活跃 | 真内存压力 | vmstat看si/so | 扩容或优化程序内存 |
Dirty持续偏高 | 回写跟不上 | iostat看磁盘 | 调低 dirty 比例或换盘 |
| 缓存反复涨落 | 缓存颠簸 | 看访问模式 | 优化读策略或加内存 |
| 清缓存后延迟抖动 | 缓存被清 | 确认是否有定时任务 | 移除 drop_caches 定时任务 |
5.5 一个容易忽略的坑:容器里的内存统计
在容器环境里,free看到的往往是宿主机视角或者 cgroup 限制后的视角,容易误判。容器内更应该看 cgroup 的memory.stat,里面的file字段对应页缓存,anon对应匿名内存,total_cache等字段能更准确反映容器实际内存构成。很多“容器内存超限被杀”的案例,其实是把可回收的 file cache 算进了限制里,需要结合memory.limit_in_bytes和回收行为一起看。这块细节多,但方向就是:容器里别只看 free,要看 cgroup 统计。
6. 我在实际排查中的几点体会
踩过几次坑之后,我对 Buffer 与 Cache 的态度从“警惕”变成了“信任但有边界”。信任是因为它的回收机制确实可靠,绝大多数所谓“内存告急”都是误读;有边界是因为脏页回写、缓存颠簸、容器统计这些细节确实会咬人。我现在看一台机器的内存,习惯先free -hw扫一眼available,再vmstat 1看几秒 I/O 和 swap,两个动作基本就能定性。真要深挖再上/proc/meminfo和 cgroup 统计。这套流程不复杂,但比盯着一个百分比靠谱太多。另外提一句,drop_caches这个命令我建议你在测试环境多练、生产环境少碰,把它当成诊断工具而不是运维手段,心态就对了。