侧边栏壁纸
  • 累计撰写 152 篇文章
  • 累计收到 2 条评论

Linux 内存 buff/cache 占满、free 见底?排查内存假耗尽、swappiness 调优与 drop_caches 我踩过的几个坑

2026-10-3 / 0 评论 / 7 阅读
AI 摘要由 AI 生成

文章介绍了Linux内存管理中的“buff/cache”与“free”的误解,指出在Linux系统中,内存管理机制与直觉不同,`free`内存数值降低是正常现象。分析了将`drop_caches`写入crontab导致的IO灾难,强调在生产环境中不当使用`drop_caches`可能导致CPU飙升、锁争用和严重的磁盘读穿透问题。文章建议基于`available`内存进行监控和调优,以避免不必要的性…

前两年接手过一台跑着好几个核心服务的虚机,监控大盘经常在半夜发出高危告警:物理内存 32G,free 剩余直接跌破 300MB,内存使用率曲线常年飙在 95% 以上。

当时刚接手业务的新同学急得不行,在跳板机上连夜敲了个脚本,放进 crontab 里面每十分钟跑一次:

sync && echo 3 > /proc/sys/vm/drop_caches

结果第二天早高峰刚过,监控群里炸锅了。数据库读延迟从几毫秒飙到两三秒,磁盘读 IO 直接拉出一条直线打满 100%,原本跑得好好的业务接口疯狂报超时。

后来排查下来,内存不仅根本没泄露,反而是这行自作聪明的定时清理脚本,把系统里正用来加速磁盘读写的页面缓存全给硬生生扬了。

Linux 的内存管理机制和很多人直觉里的 Windows 逻辑完全不一样。今天把关于 buff/cache、free、available、swappiness 以及 drop_caches 的几次回顾和调优经验理一遍。


别被 free 的数字吓到:free 与 available 的真实含义

在 Linux 上排查内存问题,第一步往往是敲下 free -h。典型的输出通常长这样:

               total        used        free      shared  buff/cache   available
Mem:            31Gi       9.8Gi       420Mi       1.2Gi        21Gi        19Gi
Swap:          8.0Gi       1.1Gi       6.9Gi

很多刚接触 Linux 运维的人看到 free 只有 420Mi,就判定服务器内存要爆了。

其实在 Linux 内核的设计哲学里,空闲的内存就是浪费的内存。如果内存一直空着,磁盘上的数据每次被读取都要走慢速的物理 I/O;所以内核会尽可能把空闲的物理内存挪来当 Page Cache(页面缓存)和 Buffer(块设备缓冲区)。

这里的关键区别在于:

  • free:内核当前完全没用到的、纯粹闲置的物理内存。在系统跑了一段时间后,这个数值变小非常正常。
  • buff/cache:正在用来缓存磁盘读写数据和元数据的内存。当系统中的应用程序需要申请新内存时,内核可以瞬间把其中没被锁定的干净页面回收出来,直接分给进程使用。
  • available:从用户态进程的角度来看,当前真正能够拿来申请分配的内存预估量。它等于 free 加上绝大部分可以被立即释放的 buff/cache,再扣除内核为了维持运转必须保留的水位阈值(如 min_free_kbytes)。

只要 available 的数值还很充裕,就算 free 跌到几十兆,系统也没有内存危机。


把 drop_caches 写进 crontab,亲手制造 IO 灾难

前面提到的真实事故,根源就在于很多人对 /proc/sys/vm/drop_caches 的用法有严重误解。

内核提供了三个等级的清理参数:

  • echo 1 > /proc/sys/vm/drop_caches:释放页缓存(PageCache)。
  • echo 2 > /proc/sys/vm/drop_caches:释放可回收的 slab 对象(包含 dentry 和 inode 缓存)。
  • echo 3 > /proc/sys/vm/drop_caches:同时释放页缓存和 slab 对象。

这套接口本来是内核开发者用来测试不同内存压力场景、或者做基准性能压测前重置状态用的,从来不是给生产环境常态化运维准备的。

一旦在生产环境强行清理 cache,会立刻引发两个连锁反应:

  1. 瞬时 CPU 飙升与锁争用:内核遍历全部 lru 链表和 slab 缓存进行内存回收,需要持有相关的全局锁,直接卡滞业务进程。
  2. 严重的磁盘读穿透(Cold Cache):原本数据库(MySQL、PostgreSQL)经常访问的热点索引、Nginx 静态文件,全部都在 Page Cache 里命中;一旦被抹去,下一秒所有的请求全部穿透到机械盘或云盘,瞬间将底层存储 IOPS 打到死线。

后来我们把 crontab 里的清理任务彻底注销,监控报警阈值从基于 free 改成基于 available,整个服务的 P99 延迟立刻从毛刺不断恢复成平稳的一条直线。


为什么执行了 drop_caches,buff/cache 一点没降?

很多排查现场还出现过另一种反常情况:有人执行了 sync && echo 3 > /proc/sys/vm/drop_caches,结果看 free -h,buff/cache 还是稳稳占着 15G,一兆都没少。

这是因为很多东西虽然统计在 buff/cache 里,但它们是不可回收的。

要看清这 15G 到底是什么,不能只看 free,得看 /proc/meminfo:

cat /proc/meminfo | grep -E "Cached|Buffers|Shmem|SUnreclaim|SReclaim"

典型输出会拆解成几部分:

Buffers:            42108 kB
Cached:          14892300 kB
Shmem:           12582912 kB
SReclaimable:      512040 kB
SUnreclaim:        204800 kB

注意那个高达 12G 的 Shmem(共享内存)。

在 Linux 内核中,Shmem 是被算在 Cached 统计项里的,进而也被归入 free 输出中的 buff/cache。但它实际上对应着:

  • /dev/shm(tmpfs 内存文件系统)里存放的文件。
  • 进程间通信使用的 SysV 或 POSIX 共享内存段。
  • Docker 容器内部往 /tmp 或挂载的 tmpfs 写入的大文件。

这些共享内存由于没有下挂任何真正的物理磁盘文件,是没办法通过丢弃页面来回收的。如果想释放它们,要么把它移到 swap 分区,要么必须找到对应的进程或 tmpfs 目录将文件物理删除。

可以用下面的命令找出到底是哪个目录或进程吃了大量内存挂载:

# 查看 tmpfs 和共享内存挂载点的占用情况
df -h -t tmpfs

# 查看是否有大型 IPC 共享内存段残留
ipcs -m

有一次排查发现,某位同事在容器里的 /dev/shm 跑深度学习加载模型,写了 10 个 G 的临时权重文件忘了删,整个系统的 buff/cache 就永远下不去。


还有很多 cache,为什么系统却拼命在读写 swap?

另一个最让人困惑的现象是:看 free -h 明明还有 10G 的 buff/cache,但系统却开始频繁把内存换入换出到 swap 分区,甚至导致机器卡顿。

这就涉及到 Linux 内核内存回收的分配天平:vm.swappiness。

很多人以为 swappiness = 0 的意思是“绝对不用 swap,除非物理内存被彻底耗尽”。这个理解在很早期的 2.6 内核可能接近,但在现代内核(3.5+ 之后)完全不是这样。

内核在发生内存压力做回收时,面临两类内存页的选择:

  1. 文件页(File-backed pages):对应 Page Cache。回收代价低,如果是只读数据,直接丢弃即可;下次需要再从磁盘读。
  2. 匿名页(Anonymous pages):进程自己申请的堆、栈、数据段等。要回收它们,必须把内容写到 swap 磁盘分区。

swappiness 的值(范围 0 到 200,默认通常是 60)并不是百分比,而是内核在决定“回收文件页”和“回收匿名页”时的倾向度比重权重。

公式大致可以理解为:

  • 扫描匿名页的权重正比于 swappiness
  • 扫描文件页的权重正比于 (200 - swappiness)

当 swappiness 设置为 60 时,如果系统里有大量长时间处于休眠状态的冷进程(虽然占着堆内存但从来不活跃),而同时有频繁的磁盘 I/O 需要 Page Cache,内核会权衡认为:把长期不用的匿名内存换出到 swap,留出物理内存给高频读写的文件当缓存,整体吞吐量其实更高。

但是,对于跑数据库(MySQL、PostgreSQL、Redis、Elasticsearch)的服务器来说,一旦进程的关键内存被换到 swap,一个随机查询过来就会触发慢速磁盘 swap in,造成毫秒级甚至秒级的延迟抖动。

生产环境调优建议

针对数据库和低延迟后台服务,建议把 swappiness 调低,但不要盲目设成 0:

# 临时生效
sudo sysctl vm.swappiness=10

# 写入配置文件持久化
echo "vm.swappiness = 10" | sudo tee -a /etc/sysctl.d/99-memory-tuning.conf
sudo sysctl -p /etc/sysctl.d/99-memory-tuning.conf

为什么设 10 而不是 0?因为设成 0 会让内核在处理突发内存尖刺时极度抗拒 swap,反而大大增加了触发 OOM Killer 直接杀掉关键数据库进程的概率。设成 10 可以在最大限度保留物理内存的同时,保留最后一点应急缓冲空间。


slab 占用失控:top 里查不到是谁吃了内存

有时会遇到这样一种诡异场景:used 很高,buff/cache 也很高,但把 top 或者 ps aux --sort=-%mem 里所有进程的 RSS 内存加起来,连物理内存的一半都不到。进程列表里没有任何大进程,剩下的内存凭空蒸发了。

这往往是内核的 Slab 分配器占用了大量内存。

Slab 是内核用来缓存自身数据结构的机制,比如文件目录项(dentry)和文件索引节点(inode)。

查看 slab 占用的命令:

# 按照占用内存降序查看内核 slab 缓存
sudo slabtop -s c

常见的输出如果出现以下特征:

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
892144 891200  99%    0.19K  42483       21      8.2G dentry
654210 653800  99%    0.58K  47820       14      5.1G ext4_inode_cache

dentry 和 ext4_inode_cache 吃了 13 个 G 的物理内存。

产生这种情况的典型诱因包括:

  1. 本地目录里积累了数百万个散落的小文件(如未做轮转清理的日志、爬虫临时目录、大量 session 文件)。
  2. 定时任务一直在遍历或查找不存在的路径,产生了大量负缓存(negative dentry)。

针对这种元数据缓存膨胀,除了清理小文件外,可以通过调整内核参数加速回收目录项:

# 默认值通常是 100。调高到 150 或 200 会让内核更积极地回收 dentry 和 inode 缓存
sudo sysctl vm.vfs_cache_pressure=150

生产环境排查与调优检查清单

遇到 Linux 内存疑似报警,建议按下面的检查步骤走:

  1. 先看 available,不要只看 free:
    运行 free -h。如果 available 占比超过 20%,系统通常处于健康状态,无需人为干预。
  2. 确认是否真的有 swap 读写抖动:
    运行 vmstat 1 5,观察 si(swap in)和 so(swap out)列。如果都是 0,即使 swap 已分配了部分空间,也代表当前没有活跃的换页发生,对业务性能没有影响。
  3. 查清 buff/cache 的具体成分:
    检查 /proc/meminfo 中的 Cached、Shmem、SReclaimable。如果 Shmem 很高,用 df -h -t tmpfs 找挂载;如果 SReclaimable 很高,用 slabtop 找目录项泄漏。
  4. 拒绝生产环境使用 drop_caches 定时任务:
    让内核自主管理 Page Cache 生命周期。针对高负载业务机器,配合合理设置 vm.swappiness = 10 和 vm.vfs_cache_pressure = 100。

把这几个底层机制理顺之后,面对监控大盘上飘红的内存指标,就能准确分辨出什么是良性的系统缓冲,什么是真正的内存泄漏。

评论一下?

OωO
取消