Linux文件读写性能的核心依赖之一是页缓存(page cache),它的工作机制决定了它不会因为你没有读写文件而主动释放内存。页缓存是内核为文件内容保留的内存副本,而脏页需要后台线程异步写回磁盘,只有当整机内存出现回收压力或脏页比例超过阈值时,内核才会触发回收行为。因此,看到页缓存占用大量内存并不意味着内存泄漏,而是一个需要结合回收机制与内核参数来判断的系统状态。
页缓存不是内存泄漏,而是内核设计用来减少磁盘I/O的一种缓存策略。读取一个文件时,内核先把磁盘块的内容读入页缓存,后续再读同一个文件,就直接从内存返回数据;写入文件时,数据先落在页缓存中,标记为脏页,由内核的pdflush线程(或writeback机制)统一写回磁盘。
根据内核文档《Memory Management》中的描述,页缓存以基数树(radix tree)结构索引文件页,每个页的生命周期由reclaim逻辑管理。回收页缓存的前提有两个:第一,系统空余内存低于某一水位线,kswapd内核线程开始换页回收;第二,脏页数量超过vm.dirty_ratio或vm.dirty_background_ratio设置的阈值。你观察到的页缓存长时间不释放,往往是这两个条件都未被触发。
一个常见的误解是系统内存越接近上限,内核就越积极清空页缓存。事实上,如果没有新的内存请求导致分配失败,kswapd不会主动扫描缓存的inactive链。也就是说,只要没有内存压力,内核宁可把本该用于业务的内存空间留给页缓存,也不去主动做无用功。
Linux内核只在两种场景下手动处理页缓存:内存分配压力或显式触发drop_caches操作。前者由分配器向伙伴系统请求内存,发现空闲块不足时调用shrink_slab和shrink_page_list,逐出一部分干净页;后者是你主动执行sync; echo 1 > /proc/sys/vm/drop_caches,把干净页释放回伙伴系统。
在排查内存占用时,先使用free命令查看各字段的分布。free输出的cached列包含tmpfs和shmem文件系统的页,以及普通的文件页缓存;buffers则是块设备上的缓冲数据。将cached与available做对比,如果available明显小于cached,说明当前页缓存的实际可回收比例并不高。
free -m total used free shared buff/cache available Mem: 15872 12063 301 1024 3508 2704
从上面的输出可见,cached有3508MB,但available只有2704MB,中间的差值包含了不可回收的共享内存页和内核栈等。此时页缓存并未失控,而是可回收比例已被其他不可回收页压缩。
脏页含量直接决定了页缓存回收后是否需要写磁盘,而这与你的写入模式强相关。内核参数vm.dirty_background_ratio默认值为10,含义是当脏页占全部可用内存的10%时,后台内核线程开始回写脏页,但不阻塞业务。vm.dirty_ratio默认值为20,当脏页占比达到这个值时,触发同步写回,写文件的进程会被阻塞直到脏页落盘。
如果你的服务频繁追加写入大文件,比如日志系统或消息队列,脏页比例会稳定在background_ratio附近,此时观察到页缓存占用高且writeback持续有I/O活动,是预期行为。反过来,如果读多写少,页缓存大部分是干净页,这些页面几乎不会引发回收成本,页缓存保留对系统有利。
关键的是,不要看到页缓存高就立刻执行echo 3 > drop_caches。干净页被逐出后,虽然free内存增加,但下次访问文件又得全部重新读盘,性能反而变差。更合理的做法是分析内存压力与磁盘I/O负载的关联,再决定是否调整dirty参数。
| 内核参数 | 默认值 | 作用 | 触发方式 |
|---|---|---|---|
| vm.dirty_ratio | 20% | 脏页占内存上限,到达后同步阻塞写 | 进程写路径 |
| vm.dirty_background_ratio | 10% | 启动后台回写线程 | 内核线程 |
| vm.dirty_writeback_centisecs | 500 | 定期唤醒回写线程的间隔(百分之一秒) | 定时器 |
| vm.dirty_expire_centisecs | 3000 | 脏页超过该时限才强制写回 | 定时器 |
数据库服务如MySQL或PostgreSQL会调用posix_fadvise或madvise技术管理自身缓存,但它们的预读行为会让页缓存出现短期猛增。InnoDB每次从磁盘读页时会触发一次长度为64KB左右的预读,如果表扫描操作频繁,页缓存会在短时间内吸纳大量文件页,这属于数据库的常规路径。
这类场景下正确区分页缓存和数据库缓冲池很重要。InnoDB缓冲池是进程内存,不属于页缓存;页缓存是内核层的文件副本。当表格数据远小于缓冲池时,页缓存中只是出现了数据页的镜像,这种镜像不会导致额外内存泄漏,因为InnoDB通常使用O_DIRECT绕过页缓存直接读写数据文件。只有在未使用O_DIRECT的模式下,页缓存才会承载真正的数据库I/O流量。
在排查页缓存占用前,先确认你的数据库是否启用了O_DIRECT。使用如下命令检查MySQL的innodb_flush_method参数:
mysql -e "show variables like 'innodb_flush_method';" +---------------------+----------------+ | Variable_name | Value | +---------------------+----------------+ | innodb_flush_method | O_DIRECT | +---------------------+----------------+
如果结果不是O_DIRECT,那么数据库读写会同时产生页缓存和缓冲池双份副本,这是内存占用翻倍的重要原因。
判断页缓存是否需要处理的依据是内存压力指标,而不是缓存绝对值。查看/proc/pressure/memory文件中的PSI数据,其中some和full字段显示任务因内存分配而受阻的时间占比。如果full值长期为0,说明没有任务因为内存而等待,页缓存保留对性能有益,不需要干预。
cat /proc/pressure/memory some avg10=0.00 avg60=0.00 avg300=0.00 total=0 full avg10=0.00 avg60=0.00 avg300=0.00 total=0
当PSI的平均值持续高于5%时,才需要考虑以下几种调整策略。第一种,降低脏页阈值,让后台写回更早介入,避免脏页堆积到导致同步阻塞。第二种,为热数据文件开启O_DIRECT绕过页缓存,让文件直接读取磁盘。第三种,调整swappiness优先回收页缓存而非匿名页。
对于写多读少的服务,将vm.dirty_background_ratio调低至5,dirty_ratio调低至10,能让后台回写线程更早启动,减少writeback积累。调整命令如下:
sysctl -w vm.dirty_background_ratio=5 sysctl -w vm.dirty_ratio=10
这些改动对海量顺序写日志的应用帮助明显。对于读多写少的Web服务,保留默认值即可,因为干净页的回收成本远低于再次读盘的开销,人为释放页缓存只会伤害缓存命中率。
另一个被忽视的维度和swap设置有关。vm.swappiness值控制在回收时优先选择匿名页还是文件页的权重,默认值60会让内核在内存不足时同时回收文件页和匿名页。如果页缓存占比高且可用内存低,将swappiness调低至10以下,强制减少匿名页的交换,从而保留更多文件缓存。
页缓存占用没有统一的标准数值,而是和业务读写比例强相关。根据网上公开的运营经验以及kernel社区的讨论,页缓存占可用内存的20%到30%属于常见的默认区间,只读缓存类服务占50%以上也是常态。这里提供一个更客观的判断方法:监控page reclaims指标和major faults的数量,如果major faults持续上升但页缓存保持高位,说明冷文件访问在频繁把新页拉入缓存,但缓存命中率并没有提升,此时考虑缓存冷热分区。
在监控层面,建议采集以下四个指标来跟踪页缓存状态:cached、dirty、writeback和pgscan。cached值反映缓存总量,dirty反映待写回的数据量,writeback反映正在写回的量,pgscan则表明页缓存回收是否频繁发生。当dirty接近dirty_ratio阈值且pgscan同步升高时,系统已经到了内存瓶颈的边缘,需要调整业务策略或扩容内存。
最后提醒一个误区:页缓存是Linux内核优化I/O的核心机制,现代内核的回收算法已经足够成熟,不会因缓存不释放而引发系统崩溃。真正需要关注的,是缓存回收时引发的延迟抖动和写阻塞是否影响在线业务,而不是free命令下面那行遥不可及的free数字。
评论
加载中...