当前位置: 首页 > news >正文

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 三件套,教你看穿上述现象背后的内核执行路径。

http://www.cnnetsun.cn/news/3622956.html

相关文章:

  • 大模型转型指南:从入门到商业交付的实战路径
  • 全球牙科树脂粘接剂行业发展态势分析及投资战略研究报告2026年版
  • TI ADS8353/7853双通道SAR ADC评估套件深度解析与实战指南
  • 细粒度图像识别技术:从原理到波音747型号识别实践
  • 智能体化ABM:从Mesa实战到统计模型检验的完整指南
  • 银行卡号识别技术:混合方案实现99.2%准确率
  • AutoGPT与AI Agent技术:自主决策框架实战指南
  • MoE架构解析:如何提升大模型计算效率与容量
  • 提示词写不对,表格永远错一半:3类典型失效场景,20年数据工程师亲授调试心法
  • 如何在忙碌工作中轻松提升词汇量?ToastFish让你在摸鱼时间悄悄变强!
  • Rust 的所有权模型在安全审计中的实际价值:从内存安全到逻辑安全的自然延伸
  • 英雄联盟智能助手Seraphine:3步实现高效自动化游戏数据管理
  • 突破百度网盘限速壁垒:直链解析工具的完整实战指南
  • GHelper:重新定义华硕笔记本硬件控制的终极开源方案
  • YOLOv11木材缺陷智能检测系统优化与实践
  • BetterGI原神自动化助手:从入门到精通的完整指南
  • RHCE第三次作业
  • SAR ADC评估套件实战指南:从硬件配置到动态性能分析
  • 谷歌多模态向量模型:跨模态AI搜索技术解析与实践
  • RAG系统文档分块优化指南:提升问答效果的关键
  • TPT 2时间序列大模型:工业AI落地的关键技术突破
  • Agent智能体技术:核心架构与行业应用解析
  • 前端 Serverless 架构的实践复盘:Cloudflare Workers 与 Vercel Edge 对比
  • GAN技术解析:从原理到创造性内容生成实践
  • 神经网络深度化的理论与实践:从万能逼近定理到大语言模型
  • 华硕笔记本风扇噪音终结者:G-Helper 静音控制完全手册
  • 终极指南:用Scarab轻松管理空洞骑士Mod的3大核心功能
  • 终极解放:用G-Helper彻底摆脱华硕笔记本的臃肿控制软件
  • 高低双线 同花顺期货通指标
  • 低成本AI生成技术:动态算力分发与Token优化