news 2026/7/24 16:36:10

Page Cache 与 Swap:容器内存使用量为什么总在临界点?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Page Cache 与 Swap:容器内存使用量为什么总在临界点?

Page Cache 与 Swap:容器内存使用量为什么总在临界点?

实验环境:Ubuntu 24.04 / 内核 6.8.0-106-generic / Cgroup v2 / 华为云 FlexusX 8C16G
本文所有命令输出均来自真实实验机,可直接复现。

一、引子:那个"虚高"的内存水位

上一篇文章我们复现了容器 OOM。但更多时候,你遇到的不是 OOM,而是困惑

“我的容器docker stats显示内存 80% 了,会不会马上 OOM?”
“为什么memory.current一直贴着上限,但容器稳如老狗,从来不被杀?”
“我明明只写了个日志文件,容器内存怎么涨了 400M?”

如果你只看memory.currentdocker stats的"内存使用率"来判断容器是否危险,你会长期误报。背后的两个"隐形推手"是Page Cache(页缓存)Swap。本文用真实数据拆解:为什么容器内存"看着满"却没事,以及怎样才算"真的快 OOM 了"。


二、问题复现:写个文件,内存就涨了 400M

我在宿主机编译了一个静态memhog(每 1MB 真实写满一页),配合dd做文件 I/O。进一个-m 512m的容器观察 cgroup 接口:

root@ecs-a8bb-0002:~# docker run -m 512m --name pc-test -v /root/memhog:/memhog alpine sh -c 'cat/sys/fs/cgroup/memory.currentddif=/dev/zeroof=/tmp/bigfilebs=1Mcount=4002>&1|tail-1grep-E"^(file|anon|inactive_file|active_file|file_mapped) "/sys/fs/cgroup/memory.statcat/sys/fs/cgroup/memory.current '

实测输出(已精简行号):

[1] 初始 memory.current: 1056768 # ≈ 1MB,空容器 [2] dd 写 400M 文件到 /tmp/bigfile 419430400 bytes (400.0MB) copied, 2.09s, 190.9MB/s [3] 写后 memory.stat(文件相关字段): anon 122880 file 419434496 # ≈ 400MB!全是文件页缓存 file_mapped 0 inactive_file 419434496 active_file 0 [4] 写后 memory.current: 433070080 # ≈ 413MB

发生了什么dd/tmp/bigfile写了 400MB,内核把这些数据放进Page Cache(页缓存),于是fileinactive_file各涨到 ~400MB,memory.current从 1MB 飙到 413MB——但容器没 OOM,甚至没有任何告警

这 400MB 不是"被吃掉的内存",而是内核用空闲内存做的文件读缓存。它随时可以被回收。


三、内存压力下,Page Cache 会自动让位

如果 page cache 真能被回收,那就该在内存紧张时看到它"缩水"。我们来验证:在同一个容器里,先写 400MB 文件,再在后台申请 300MB匿名内存(不可回收),观察 file 字段会不会被自动回收。

root@ecs-a8bb-0002:~# docker run -m 512m --name pc-test -v /root/memhog:/memhog alpine sh -c 'ddif=/dev/zeroof=/tmp/bigfilebs=1Mcount=4002>/dev/null /memhog300&# 后台申请 300MB 匿名内存sleep6grep-E"^(file|anon|inactive_file|active_file) "/sys/fs/cgroup/memory.statcat/sys/fs/cgroup/memory.current '

输出:

[6] 压力后 memory.stat: anon 315977728 # ≈ 301MB(新申请的匿名内存) file 212602880 # ≈ 203MB(被回收了约 197MB!) inactive_file 212598784 active_file 4096 [7] 压力后 memory.current: 536829952 # ≈ 512MB,正好顶到上限

关键结论:

  • 写文件后file ≈ 400MB;申请 300MB 匿名内存后,file自动降到 ~203MB,腾出的空间让给了匿名内存。
  • memory.current从 413MB 升到 ~512MB(上限),全程没有 OOM
  • 这证明:Page Cache 是可回收内存,内核在分配匿名页失败时,会先把不活跃的 file 页换出去,而不是杀进程。

四、docker stats 的口径:它其实"减过"缓存

前面我们看到memory.current是 413MB,但如果你用docker stats看,数字会小得多:

root@ecs-a8bb-0002:~# docker run -d -m 512m --name pc-stat -v /root/memhog:/memhog alpine sh -c \"dd if=/dev/zero of=/tmp/bigfile bs=1M count=400 2>/dev/null; sleep 3600"root@ecs-a8bb-0002:~# docker stats --no-stream pc-statCONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 57c341b6e2ec pc-stat0.00%12.2MiB / 512MiB2.38% 736B/126B 0B/379MB1root@ecs-a8bb-0002:~# # 同一时刻的 cgroup 真实数据:root@ecs-a8bb-0002:~# grep -E "^(anon|file|inactive_file) " /sys/fs/cgroup/.../memory.statanon45056file419434496inactive_file419434496root@ecs-a8bb-0002:~# cat /sys/fs/cgroup/.../memory.current432222208# ≈ 412MB(含缓存)

对比非常刺眼:

指标数值含义
memory.current(原始)432222208 ≈412MB含 page cache 的全部记账
docker statsMEM USAGE12.2MiBmemory.current − inactive_file(≈ 412 − 400)

docker stats的"内存使用"其实是working set(工作集)≈memory.current − inactive_file。Docker(cadvisor)认为"不活跃的页缓存随时能回收,不该算作已用"。所以docker statsmemory.current更接近真实压力,但也因此低估了 page cache 占用的物理页——它只减了inactive_file,没减active_fileslab_reclaimable


五、内核原理深挖

5.1 什么是 Page Cache

当进程读/写文件,内核不会每次都直接怼磁盘,而是把文件内容缓存在内存的页缓存里。好处:重复读命中缓存,速度提升几个数量级。代价:它占用了"看起来像已用"的内存。在 cgroup v2 里,这些页计入memory.statfile字段,并细分为:

  • inactive_file:长时间未被访问、最容易被回收的缓存。
  • active_file:近期被访问、仍在"热"列表的缓存(回收优先级低于 inactive)。
  • file_mapped:被 mmap 映射到进程地址空间的缓存(回收时需先解映射,成本更高)。

5.2 cgroup v2 memory.stat 字段解读(实测常用)

anon 进程堆/栈/匿名mmap,不可回收,"真·占用" file 所有文件页缓存(= active_file + inactive_file + 其他) inactive_file 冷缓存,OOM 时首选回收对象 active_file 热缓存,回收前先降级为 inactive file_mapped 被 mmap 的缓存 slab_reclaimable 内核 slab 中可回收部分(dentry/inode 缓存等)

v1/v2 差异:v1 的memory.stat字段命名不同(如total_cachetotal_rsstotal_mapped_file),且 v1 还有memory.stat中的total_inactive_file等带total_前缀;v2 去掉了total_前缀,并把cache改名为filerss改名为anon。另外 v2 新增了workingset相关统计(workingset_refault_anon/file等),用于衡量"回收后再次发生缺页"的压力信号。

5.3 回收机制:LRU 双链表

内核用active/inactive两条 LRU 链表管理页。新缓存进inactive,被访问则升级到active;内存紧张时,先扫描inactive_file回收。这就是为什么上面实验里inactive_file从 400MB 被砍到 203MB,而匿名内存不受影响。

ASCII 示意:

内存紧张,分配匿名页失败 │ ▼ shrink_node() 回收路径 │ ├─ 先回收 inactive_file(冷文件缓存) ← dd 产生的 400MB 在这里被砍 ├─ 再回收 active_file(热文件缓存,先降级) ├─ 再回收 slab_reclaimable(dentry/inode) └─ 实在不够 → 匿名页换出到 swap(见下文) │ ▼ 仍超限 → OOM Killer

5.4 正确评估"真实水位"的口径

你想知道的该看的指标危险信号
究竟有多少内存不能回收memory.statanonanon 接近memory.max
工作集(含热缓存)anon + active_file接近memory.max
是否真被 OOM 杀过memory.eventsoom_killoom_kill > 0
缓存占比(通常无害)file / memory.current占比高反而说明"内存利用率高",不是危险

结论:判断容器是否危险,核心看anon是否逼近memory.max,而不是memory.currentdocker stats的百分比。


六、Swap 实验:内存超限时的"缓兵之计"

Page Cache 能回收,但匿名内存(堆)不可回收。当匿名内存超过memory.max,要么 OOM,要么——换到 Swap

6.1 宿主机先开 Swap

实验机默认无 swap,且 cgroup v2 在本内核没有 per-cgroup 的memory.swappiness(后面详解)。先建 swapfile 并拉开全局swappiness

root@ecs-a8bb-0002:~# free -h | grep SwapSwap: 0B 0B 0B# 原本没开root@ecs-a8bb-0002:~# ls /sys/fs/cgroup/memory.swappinessls: cannot access'/sys/fs/cgroup/memory.swappiness':No suchfileor directory# v2 无 per-cgroup swappinessroot@ecs-a8bb-0002:~# sysctl vm.swappinessvm.swappiness=0# 默认 0,不会主动 swaproot@ecs-a8bb-0002:~# fallocate -l 2G /swapfile && chmod 600 /swapfile \&&mkswap/swapfile&&swapon/swapfile root@ecs-a8bb-0002:~# sysctl vm.swappiness=60root@ecs-a8bb-0002:~# free -h | grep SwapSwap:2.0Gi 0B2.0Gi# 已就绪

6.2 禁用 Swap:同样 800M,直接 OOM

-m 512m --memory-swap 512m等价于memory.swap.max = 0(不允许任何 swap):

root@ecs-a8bb-0002:~# docker run -m 512m --memory-swap 512m --name sw-no -v /root/memhog:/memhog alpine /memhog 800allocated16MB... allocated496MB# 被 OOM Killer 杀死root@ecs-a8bb-0002:~# docker inspect sw-no --format "OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}"OOMKilled=trueExitCode=137

cgroup 里memory.swap.max = 0,匿名内存无处可去 → OOM。

6.3 允许 Swap:800M 也能活

-m 512m --memory-swap 1gmemory.swap.max = 512M(内存 + swap 共 1GB):

root@ecs-a8bb-0002:~# docker run -d -m 512m --memory-swap 1g --name sw-yes -v /root/memhog:/memhog alpine /memhog 800root@ecs-a8bb-0002:~# sleep 6root@ecs-a8bb-0002:~# CID=$(docker inspect sw-yes --format {{.Id}})root@ecs-a8bb-0002:~# CG=/sys/fs/cgroup/system.slice/docker-${CID}.scoperoot@ecs-a8bb-0002:~# echo "memory.max = $(cat $CG/memory.max)"memory.max=536870912# 512MB(RAM 上限)root@ecs-a8bb-0002:~# echo "memory.current = $(cat $CG/memory.current)"memory.current=536784896# ≈ 512MB(RAM 部分已顶满)root@ecs-a8bb-0002:~# echo "memory.swap.max = $(cat $CG/memory.swap.max)"memory.swap.max=536870912# 512MB(可换出到 swap 的上限)root@ecs-a8bb-0002:~# echo "memory.swap.current = $(cat $CG/memory.swap.current)"memory.swap.current=307576832# ≈ 293MB(已换到 swap!)root@ecs-a8bb-0002:~# docker inspect sw-yes --format "OOMKilled={{.State.OOMKilled}}"OOMKilled=false# 没被杀,活下来了root@ecs-a8bb-0002:~# grep -E "^(anon|file) " $CG/memory.statanon534650880# ≈ 510MB 在 RAM

划重点:800MB 的匿名内存 = 510MB 留在 RAM(memory.current顶到 512MB) +293MB 进 swapmemory.swap.current)。因为 swap 吸收了溢出部分,容器没有被 OOM。代价是那 293MB 的访问会变慢(磁盘 IO)。

6.4 v1/v2 的 swappiness 差异(实测验证)

  • Cgroup v1:每个 cgroup 有独立的memory.swappiness(0~100),可单独控制"该 cgroup 多愿意 swap"。
  • Cgroup v2(本内核 6.8.0-106)没有 per-cgroup 的memory.swappiness文件(已用ls验证不存在)。是否 swap 完全由全局/proc/sys/vm/swappiness控制。这意味着同一台机器上的所有容器共享同一套 swap 倾向,你没法让"数据库容器别 swap、批处理容器随意 swap"。
  • 另外注意:本实验必须sysctl vm.swappiness=60才能让 swap 真正发生;若全局swappiness=0,即使memory.swap.max设得再大,内核也几乎不换出。

6.5 一个隐蔽的坑:docker 默认的 swap 翻倍

本文所有-m 512m实验(包括上一篇 OOM),如果不显式指定--memory-swap,Docker 在 v2 下会默认把memory.swap.max设为等于memory.max。上一篇 oom-test 的内核日志里就有这行佐证:

memory: usage 524288kB, limit 524288kB, failcnt 49 swap: usage 0kB, limit 524288kB, failcnt 0 # swap 上限默认 = 512MB!

也就是说:docker run -m 512m默认允许容器总共用到 1GB(512M RAM + 512M swap),只要宿主机有 swap。这在"内存用满才开始慢"的场景下极容易掩盖真实内存压力——你的容器其实已经把 512M 全用了,只是偷偷换到了磁盘。生产上若要严格限制,务必显式--memory-swap=512m(关掉 swap)或设置一个明确的总量。


七、排查思路:怎样正确评估容器真实水位

  1. 别只看百分比。先cat容器 cgroup 的memory.stat,看anon占比。
  2. 危险信号排序
    • memory.eventsoom_kill > 0→ 已经 OOM 过,最紧急;
    • anon持续逼近memory.max→ 马上会 OOM;
    • file/inactive_file很大 → 多半正常,是缓存利用得好。
  3. swap 是延迟而非解决memory.swap.current持续增长,说明匿名内存已超过 RAM,性能已在下滑。
  4. workingset 口径:真实工作集 ≈anon + active_filedocker stats的 MEM USAGE 已减掉 inactive_file,可作为快览,但精细排障看memory.stat

八、解决方案与最佳实践

  1. 监控anon而非memory.current:Prometheus + cadvisor 取container_memory_working_set_bytes会比container_memory_usage_bytes更接近危险线;再补一条container_memory_swap监控。
  2. 明确 swap 策略:内存敏感型服务(数据库)建议--memory-swap=512m关 swap,避免抖动;批处理/可重试任务可开 swap 当安全网。
  3. 别让默认翻倍坑你:生产镜像/编排显式写死--memory-swap
  4. page cache 不是敌人:高file缓存说明 I/O 命中率高,不要因为memory.current高就去"优化"掉缓存。
  5. 调大memory.high做软限流:v2 的memory.high超过会被节流(主动回收+限流),比memory.max直接杀更平滑。

九、小结与思考题

小结memory.current高 ≠ 内存紧张——其中大部分可能是可随时回收的 Page Cache。判断容器是否危险,核心是看anon是否逼近memory.maxdocker stats已减去 inactive_file,可作快览但不能替代memory.stat。Swap 能延缓匿名内存超限导致的 OOM,但带来 IO 抖动,且 cgroup v2 本内核不支持 per-cgroup swappiness,只能靠全局vm.swappiness控制。Docker 默认会悄悄给 swap 翻倍额度,生产务必显式声明。

思考题

  1. 一个容器只做"读大文件后立刻丢弃"的工作,memory.current会一直涨吗?为什么?(提示:inactive_file 回收)
  2. 如果vm.swappiness=0memory.swap.max=512M,容器匿名内存超限会被 OOM 还是换出?为什么?
  3. docker stats显示 5%,但容器随后被 OOM——可能吗?什么场景下会这样?
  4. 为什么 cgroup v2 取消了 per-cgroup 的memory.swappiness?这给多租户调度带来了什么限制?

本篇与上一篇《OOM Killer》共同构成"容器内存"模块。下一系列《内核调试工具》将从 perf / ftrace / eBPF 三件套,教你看穿上述现象背后的内核执行路径。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/24 16:32:22

大模型转型指南:从入门到商业交付的实战路径

1. 为什么你需要这份大模型转型指南 上周帮团队面试了37位想转大模型的工程师,发现90%的人还在用错误的方式学习。一位有8年经验的Java后端工程师,花了三个月刷Transformer论文,却连最基础的API调用都搞不定;另一位刚毕业的硕士生…

作者头像 李华
网站建设 2026/7/24 16:32:03

全球牙科树脂粘接剂行业发展态势分析及投资战略研究报告2026年版

全球牙科树脂粘接剂行业发展态势分析及投资战略研究报告2026年版牙科树脂粘接剂是一类用于在牙体硬组织(如牙釉质和牙本质)与树脂基修复材料之间建立高强度粘接界面的功能性口腔材料,通常由功能性单体、树脂基体、溶剂及光引发体系组成&#…

作者头像 李华
网站建设 2026/7/24 16:31:35

TI ADS8353/7853双通道SAR ADC评估套件深度解析与实战指南

1. 项目概述:深入解析双通道SAR ADC评估套件 在精密数据采集系统的设计初期,工程师们常常面临一个核心挑战:如何快速、准确地评估一颗高性能模数转换器(ADC)在目标应用中的真实表现?数据手册上的参数固然重…

作者头像 李华
网站建设 2026/7/24 16:31:30

细粒度图像识别技术:从原理到波音747型号识别实践

最近在技术社区看到一个有趣的现象:一个看似简单的波音747模型竞猜活动,竟然无人能准确匹配对应的模型版本。这背后反映的不仅仅是航空知识的专业门槛,更揭示了模型识别领域的技术痛点——当面对高度相似的变体时,传统识别方法为何…

作者头像 李华
网站建设 2026/7/24 16:31:20

智能体化ABM:从Mesa实战到统计模型检验的完整指南

在复杂系统建模与仿真领域,基于代理的模型(Agent-based Models, ABM)正经历一场深刻的范式变革。传统ABM通常将代理行为预设为固定规则,而新兴的"智能体化"(Agentic)ABM则致力于赋予代理更高层次…

作者头像 李华
网站建设 2026/7/24 16:28:47

银行卡号识别技术:混合方案实现99.2%准确率

1. 项目背景与核心价值银行卡号识别是金融科技领域的基础性技术,每天有数以亿计的银行卡交易需要处理。传统OCR技术在这个特定场景下存在明显短板:卡面反光、磨损、倾斜拍摄等现实因素导致识别准确率难以突破90%大关。我们团队通过融合传统图像处理与深度…

作者头像 李华