🤖
AI审核中

内存没爆,系统为什么先卡住:用Linux DAMON看懂冷热页与主动回收

Java 22分钟 113浏览 0评论

一、内存使用率不高,系统为什么还是会卡

排查服务器性能问题时,我们通常会先执行:

free -h
top
vmstat 1

如果是 Java 服务,还会继续检查堆内存、GC 次数和停顿时间。

但线上经常出现一种看起来很矛盾的情况:

  • 服务器没有发生 OOM;
  • CPU 使用率也不高;
  • JVM Full GC 并不频繁;
  • 内存还有少量可用空间;
  • 接口的 P99 延迟却周期性升高。

原因可能不在“内存是否用完”,而在于系统已经开始为寻找可回收页面付出代价。

当可用内存逐渐减少时,Linux 需要扫描页面、调整 LRU 链表、回写脏页或者回收冷页。回收不及时,业务线程还可能进入直接内存回收路径。此时即使 CPU 曲线不高,线程也可能因为内存和 IO 资源争用而停顿。

Linux 的 PSI 可以量化 CPU、内存和 IO 资源不足造成的任务停顿时间,但 PSI 主要回答的是“系统因为资源压力停顿了多久”,并不能直接指出“究竟是哪部分内存长期占着却很少访问”。(Linux内核文档)

于是,内存排障还缺少几个关键答案:

  1. 进程常驻的内存中,哪些区域正在被频繁访问?
  2. 哪些区域已经很久没有发生有效访问?
  3. 有多少内存只是占着物理页面,却并不属于当前工作集?
  4. 能否在真正发生高压回收之前,提前处理这些冷内存?

这正是 Linux DAMON 要解决的问题。

二、DAMON 是什么

DAMON 全称为 Data Access MONitoring and Access-aware System Operations,它是 Linux 内核中的数据访问监控与访问感知操作子系统。

它并不只是统计一个进程占用了多少内存,而是持续观察内存区域的访问模式,记录这些区域:

  • 有多大;
  • 被访问得有多频繁;
  • 当前访问模式维持了多久;
  • 属于虚拟地址空间还是物理地址空间;
  • 是否可能成为冷内存候选。

DAMON 的目标是让数据访问监控同时具备准确、轻量、可扩展、可调节和可自动化等特征,从而能够用于在线生产环境,而不只是离线性能分析。

DAMON 当前提供了三类主要监控操作集:

操作集 监控对象 典型场景
vaddr 指定进程的虚拟地址空间 分析 Java、数据库、缓存服务
fvaddr 固定虚拟地址范围 分析明确的映射区域
paddr 系统物理地址空间 系统级冷热内存管理

DAMON 把底层地址空间访问检查、核心采样逻辑和上层管理模块进行了分层。这样既可以监控单个进程,也可以从整个系统物理内存的角度进行分析。

三、DAMON 和传统内存工具有什么区别

不同工具回答的是不同问题。

工具或指标 主要回答的问题 局限
freeMemAvailable 系统还有多少可用内存 不知道内存是否活跃
RSS、PSS 进程常驻了多少物理内存 RSS 高不代表这些页面正在使用
JVM Heap、NMT Java 堆和本地内存由什么组成 不直接反映页面访问冷热度
PSI 资源压力造成了多少停顿 能发现压力,但不能定位冷区域
perf 哪段代码、哪类硬件事件最活跃 更偏向 CPU 和代码热点分析
DAMON 哪些内存区域多大、访问多频繁、模式维持多久 采样结果不是对象级或逐页绝对精确值

可以把这些工具理解成不同层次的摄像头:

  • RSS 告诉你仓库里放了多少货;
  • PSI 告诉你仓库入口堵了多久;
  • perf 告诉你哪些工作人员最忙;
  • DAMON 告诉你哪些货架一直在周转,哪些货架很久没人碰。

因此,DAMON 并不是用来替代 free、PSI、JFR 或 perf,而是补上“内存访问模式”这一层。

四、DAMON 为什么能够保持较低开销

1. 不逐页持续监控

最直接的内存访问监控方式,是检查每一个页面是否被访问。

问题是,当服务器拥有几十 GB、几百 GB 甚至数 TB 内存时,逐页持续扫描的开销会快速增加,监控本身反而可能影响业务。

DAMON 采用的是区域化采样

它把相邻且访问特征相似的页面归为一个区域。每个采样周期只从区域中选择一个页面检查访问情况,再把结果记入整个区域的访问计数。

因此,DAMON 的开销更多取决于监控区域数量,而不是直接随着物理页面总数无限增长。(Linux内核文档)

2. 动态拆分与合并区域

业务的内存访问模式会持续变化。

某个连续内存区域最初可能都很活跃,运行一段时间后,其中一部分页面可能变冷。如果继续把整个区域视为一个整体,结果就会失真。

DAMON 会根据相邻区域的访问频率动态调整边界:

  • 访问模式接近的相邻区域可以合并;
  • 区域内部出现明显差异时可以拆分;
  • 区域总数被限制在用户配置的上下限内;
  • 在监控精度和运行开销之间保持可控平衡。

其核心流程如下:

flowchart LR
    A["确定监控地址范围"] --> B["划分初始内存区域"]
    B --> C["每个区域选择采样页面"]
    C --> D["检查页面是否被访问"]
    D --> E["累计区域访问次数"]
    E --> F["聚合访问结果"]
    F --> G{"相邻区域特征是否接近"}
    G -->|"接近"| H["合并区域"]
    G -->|"差异明显"| I["拆分区域"]
    H --> C
    I --> C

Linux 内核文档将这一机制称为 Region Based Sampling 和 Adaptive Regions Adjustment。(Linux内核文档)

五、理解 DAMON 输出的三个核心指标

DAMON 判断内存访问模式时,最重要的是三个维度。

1. Size:区域大小

表示该监控区域覆盖了多大的地址范围。

单独看到一个区域访问频率很低还不够。如果它只有几个 KB,对系统几乎没有影响;如果它有数百 MB,才可能具备明显的优化价值。

2. Access:访问频率

表示在聚合周期内,该区域的采样页面被访问了多少次,通常会被工具转换为访问比例或者访问频率。

访问频率高,说明它更可能属于业务热工作集。

访问频率低,并不意味着一定可以立即回收,因为它可能只是暂时处于业务低谷。

3. Age:访问模式持续时间

age 很容易被误解为“距离最后一次访问过去了多久”。

更准确地说,它表示当前区域大小和访问频率所形成的访问模式,已经相对稳定地维持了多久。当区域特征发生明显变化时,age 会被重置;特征没有明显变化时,age 会继续增加。(Linux内核文档)

因此,一个更可靠的冷内存候选通常同时具备:

  • 区域比较大;
  • 访问频率接近零;
  • age 比较高;
  • 在多个业务周期中都保持相似状态。

可以用下面的方式理解组合结果:

Size Access Age 可能含义
稳定的热工作集
重点冷内存候选
可能只是短暂空闲
虽然冷,但优化收益有限
波动 业务阶段正在切换

DAMON 是采样系统,所以不能因为某一次快照显示 access=0,就直接认定对应区域永远不会再使用。

六、从 DAMON 到 DAMOS:不只是观察,还能执行策略

DAMON 负责观察访问模式,而 DAMOS 负责把访问模式转换为系统操作。

DAMOS 全称为 Data Access Monitoring-based Operation Schemes。管理员可以声明一组高层策略:

当区域大小、访问频率和年龄满足某些条件时,对这些区域执行指定动作。

DAMOS 支持的代表性动作包括:

  • stat:只统计,不修改页面状态;
  • willneed:提示内核后续可能需要该区域;
  • cold:把区域标记为冷;
  • pageout:尝试回收对应区域;
  • hugepage:建议使用透明大页;
  • collapse:尝试折叠为大页;
  • lru_prio:提高页面在 LRU 中的优先级;
  • lru_deprio:降低页面在 LRU 中的优先级;
  • migrate_hot:优先迁移较热区域;
  • migrate_cold:优先迁移较冷区域。

具体动作是否可用,取决于所选择的 DAMON 操作集和当前内核能力。(Linux内核文档)

整个决策过程可以概括为:

flowchart LR
    A["DAMON访问监控"] --> B["获得Size Access Age"]
    B --> C["访问模式规则"]
    C --> D["过滤地址或内存组"]
    D --> E["检查水位条件"]
    E --> F["检查时间与容量配额"]
    F --> G{"选择操作"}
    G -->|"仅观察"| H["统计Stat"]
    G -->|"冷内存"| I["标冷或Pageout"]
    G -->|"热内存"| J["提高LRU优先级"]
    G -->|"大页候选"| K["HugePage或Collapse"]

这里有三个特别重要的安全机制。

1. Quota:限制动作开销

如果一次找到几 GB 冷内存,直接全部执行 pageout,可能产生大量 CPU、扫描和 IO 开销。

DAMOS 可以限制:

  • 每个时间窗口最多执行多长时间;
  • 每个时间窗口最多处理多少字节;
  • 配额用完后暂停动作;
  • 下一时间窗口再重新计算。

这样可以避免优化动作本身制造新的性能问题。(Linux内核文档)

2. Watermark:只在合适的压力区间运行

内存非常充足时,没有必要频繁回收。

内存已经严重抖动时,再进行激进扫描也可能适得其反。

水位机制允许 DAMON 只在特定内存压力区间启动,在压力过低或过高时暂停。

3. Filter:限制操作范围

DAMOS 可以使用地址范围、监控目标、匿名页、活跃页和 memcg 等条件进行过滤。

其中 memcg 能把策略限制在特定内存控制组,为容器和 Kubernetes 场景提供了重要基础。部分过滤能力依赖于具体操作集,不能假设所有内核与模式都支持完全相同的过滤条件。(Linux内核文档)

七、快速体验 DAMON

1. 检查内核能力

DAMON 需要内核在编译时启用相关配置,用户态工具通常通过 sysfs 与 DAMON 交互。官方入门文档要求内核启用相应的 CONFIG_DAMON_* 配置,并确保 sysfs 已挂载。(Linux内核文档)

可以先执行:

uname -r

grep -E 'CONFIG_DAMON|CONFIG_DAMON_SYSFS' \
  /boot/config-$(uname -r)

mount | grep ' on /sys '

不同发行版内核启用的 DAMON 功能可能不同。即使存在基础 DAMON 配置,也不代表物理地址监控、回收模块和 LRU 排序模块全部可用。

2. 获取 DAMO 用户态工具

DAMO 是 DAMON 的官方用户态操作工具,可以用于启动监控、读取快照、记录访问模式和生成热力图。(GitHub)

git clone https://github.com/damonitor/damo.git
cd damo

./damo version
sudo ./damo report sysinfo

report sysinfo 可以帮助检查当前内核、DAMON 接口和相关工具支持情况。

3. 监控一个正在运行的 Java 服务

先定位进程:

PID=$(pgrep -n -f 'java.*your-app.jar')

echo "$PID"

启动 DAMON:

sudo ./damo start --target_pid "$PID"

持续查看访问快照:

sudo ./damo report access --repeat

结束后停止监控:

sudo ./damo stop

输出中通常可以看到类似信息:

addr 140.213 TiB
size 64.000 MiB
access 0 %
age 72.300 s

它表达的是:

  • addr:区域起始地址;
  • size:区域大小;
  • access:聚合周期中的访问强度;
  • age:当前访问模式维持时间。

官方入门示例同样使用 damo startdamo report accessdamo stop 完成一次进程级快照分析。(Linux内核文档)

4. 记录一段时间并生成热力图

快照适合看当前状态,记录模式更适合观察业务周期。

sudo ./damo record "$PID"

记录结束后生成热力图:

sudo ./damo report heatmap \
  --output access-pattern.png \
  --draw_range hottest

热力图可以帮助判断:

  • 热区是否始终集中在固定地址范围;
  • 大量内存是否只在启动阶段被访问;
  • 定时任务是否周期性唤醒一批冷区域;
  • 工作集是否随着流量增长持续扩张;
  • 冷热区域是否频繁切换。

完整记录通常依赖 perftrace-cmd;如果只读取部分 DAMON 快照,则不一定需要安装它们。记录文件还可能包含内存占用、虚拟内存映射和进程统计等辅助数据。(GitHub)

八、在 Java 服务中怎么使用 DAMON

DAMON 看到的是内存页面,而不是 Java 对象。

它无法直接告诉你:

  • 哪个 Java 类占用了这块区域;
  • 哪一个缓存键长期未使用;
  • 哪个对象应该被垃圾回收;
  • 某块内存到底来自堆、线程栈还是本地库。

因此,Java 内存问题应该采用分层排查。

问题 建议工具
Java 堆中是什么对象 Heap Dump、MAT
对象分配为什么这么多 JFR、Allocation Profiling
JVM 本地内存由什么组成 Native Memory Tracking
进程映射了哪些地址区域 /proc/PID/smaps
哪些页面真正频繁访问 DAMON
系统是否已出现内存停顿 PSI、vmstat
回收是否造成缺页和 IO vmstatsarperf

可以先建立基础观测:

cat /proc/pressure/memory

vmstat 1

cat /proc/"$PID"/smaps_rollup

jcmd "$PID" VM.native_memory summary

使用 jcmd VM.native_memory 的前提,是 JVM 启动时已经开启 Native Memory Tracking,例如:

-XX:NativeMemoryTracking=summary

推荐的排查顺序是:

  1. 用 RSS、PSS 和 NMT 确认内存规模与组成;
  2. 用 JFR、GC 日志确认对象分配和回收行为;
  3. 用 PSI、vmstat 判断系统是否正在承受内存压力;
  4. 用 DAMON 查看常驻页面是否属于真实工作集;
  5. 将 DAMON 冷热变化与接口延迟、定时任务和流量曲线对齐;
  6. 优先修改缓存、堆大小、对象生命周期和容器限制;
  7. 最后才考虑自动 pageout 或 LRU 调整。

例如,一个 JVM 的 RSS 长期保持在 8 GB,并不能直接说明它泄漏了 8 GB。

但如果 DAMON 显示其中有 3 GB 区域在数十分钟内几乎没有访问,同时应用业务量稳定,那么就值得进一步判断:

  • 是否存在过大的历史缓存;
  • 是否设置了明显超过工作集的堆;
  • 是否有只在启动阶段使用的映射文件;
  • 是否存在本地库内存长期常驻;
  • 是否可以降低实例内存规格;
  • 是否应该调整容器 Request 和 Limit。

九、DAMON_RECLAIM:提前回收冷页面

Linux 还提供了基于 DAMON 的主动回收模块 DAMON_RECLAIM

它会寻找在指定时间内没有被访问的区域,并尝试回收这些冷页面。它的定位是在轻度内存压力下进行主动、轻量的回收,而不是取代传统的 LRU 页面回收机制。(Linux内核文档)

传统回收往往是压力出现以后才开始工作:

flowchart LR
    A["可用内存持续下降"] --> B["kswapd开始扫描"]
    B --> C["扫描和回收成本增加"]
    C --> D["业务线程申请内存"]
    D --> E["可能进入Direct Reclaim"]
    E --> F["接口延迟出现尖峰"]

DAMON_RECLAIM 的思路是:

  1. 在压力尚未失控时识别长期冷区域;
  2. 在配额约束下逐步回收;
  3. 尽量减少突发压力下的大规模扫描;
  4. 降低业务线程进入直接回收的概率。

当前内核文档中的默认冷区域阈值为 120 秒;默认回收时间配额为每个时间窗口 10 毫秒,容量配额为 128 MiB,配额窗口为 1 秒。不同内核版本和发行版可能调整默认值,实际使用时应以本机 sysfs 参数为准。(Linux内核文档)

可以先只读取参数,不要直接修改:

grep . \
  /sys/module/damon_reclaim/parameters/enabled \
  /sys/module/damon_reclaim/parameters/min_age \
  /sys/module/damon_reclaim/parameters/quota_ms \
  /sys/module/damon_reclaim/parameters/quota_sz \
  /sys/module/damon_reclaim/parameters/quota_reset_interval_ms \
  2>/dev/null

如果文件不存在,通常说明当前内核没有编译或加载对应模块。

十、DAMON_LRU_SORT:让冷热页排序更可信

Linux 的页面回收依赖 LRU 体系判断哪些页面应该优先保留、哪些页面可以优先回收。

但在超大内存系统中,如果持续逐页检查访问情况,成本可能很高。因此,LRU 链表通常不是在所有时刻都被完整、主动地重新排序。

DAMON_LRU_SORT 使用 DAMON 识别热区域和冷区域:

  • 对热区域提高 LRU 优先级;
  • 对冷区域降低 LRU 优先级;
  • 通过 CPU 时间配额限制排序成本;
  • 通过内存水位决定何时启动或停止。

它不是直接把所有冷页面立即换出,而是在真正发生内存压力前,帮助 LRU 链表更准确地反映页面冷热关系。(Linux内核文档)

相较于直接 pageout,这种方式通常更加温和:

flowchart LR
    A["DAMON识别访问模式"] --> B{"页面冷热状态"}
    B -->|"热"| C["提高LRU优先级"]
    B -->|"冷"| D["降低LRU优先级"]
    C --> E["压力来临时优先保留"]
    D --> F["压力来临时优先回收"]

十一、在 Kubernetes 中的应用思路

Kubernetes 中的内存问题经常表现为:

  • Pod 的 memory.usage 长期接近 Limit;
  • Java 堆没有明显泄漏;
  • 节点 PSI 持续升高;
  • 容器发生频繁回收甚至 OOM Kill;
  • 相同配置的不同实例延迟差异明显。

此时不能只看“Pod 使用了多少内存”,还要判断“这些内存是不是当前工作集”。

一个比较完整的分析链路是:

flowchart LR
    A["Pod内存持续升高"] --> B["检查JVM堆和NMT"]
    B --> C["检查Cgroup Memory PSI"]
    C --> D["DAMON分析页面冷热"]
    D --> E{"大量冷内存是否存在"}
    E -->|"否"| F["工作集确实过大"]
    E -->|"是"| G["排查缓存和内存配置"]
    F --> H["扩容或拆分负载"]
    G --> I["缩小堆或调整缓存"]

DAMOS 的 memcg 过滤能力可以把部分操作限定在特定内存控制组中,为容器级内存策略提供基础。由于不同内核版本、cgroup 模式和 DAMO 版本的配置方式可能变化,生产中应先确认本机能力,不能直接复制其他环境的参数。(Linux内核文档)

还要特别注意:

如果容器的真实热工作集本身就超过 Limit,那么主动回收不会解决问题,反而可能形成以下循环:

回收热页 → 再次访问 → 发生缺页 → 重新载入 → 再次回收。

这种现象本质上是内存抖动,正确方案应该是增加内存、拆分工作负载或者减少真实工作集,而不是继续提高回收强度。

十二、生产环境应该怎么灰度

第一阶段:只观察,不执行回收

首先使用 DAMON 快照、记录功能或者 DAMOS 的 stat 动作。

至少覆盖以下时间段:

  • 业务高峰;
  • 业务低峰;
  • 定时任务运行期间;
  • 大批量数据处理期间;
  • 发布后的预热阶段;
  • Full GC 前后。

不要只观察几秒钟,就根据单次快照判断冷热。

第二阶段:建立冷内存候选规则

例如,可以先把以下条件作为分析候选,而不是立即执行规则:

  • 区域大小超过 16 MiB;
  • 访问频率长期接近零;
  • 当前模式维持超过 5 分钟;
  • 多个观测窗口中持续满足条件;
  • 不属于延迟敏感的关键映射;
  • 业务正处于正常流量阶段。

这些阈值没有统一答案,需要根据业务访问周期进行调整。

一天运行一次的任务,即使 23 小时都显示为冷,也不代表对应数据可以毫无代价地回收。

第三阶段:单实例、小配额测试

选择一台非核心实例进行灰度,并限制:

  • 每个窗口处理的最大内存;
  • 每个窗口允许消耗的 CPU 时间;
  • 策略生效的内存压力区间;
  • 可操作的进程或内存控制组;
  • 冷区域的最小年龄。

同时观察:

cat /proc/pressure/memory

vmstat 1

grep -E 'pgfault|pgmajfault|pgscan|pgsteal|pswpin|pswpout' \
  /proc/vmstat

业务侧还要同步观察:

  • P50、P95、P99 延迟;
  • 请求吞吐量;
  • 错误率;
  • CPU 使用率;
  • 磁盘读写量;
  • Major Page Fault;
  • Swap In 和 Swap Out;
  • 实例重启与 OOM 次数。

第四阶段:比较净收益

不能只看“RSS 降了多少”。

真正需要比较的是:

释放的内存价值,是否大于重新访问这些页面造成的缺页、IO 和延迟成本。

一个策略即使释放了 2 GB 内存,但导致 P99 延迟从 100 毫秒升到 500 毫秒,也不能称为有效优化。

DAMOS 还提供 sz_triedsz_appliedqt_exceeds 等统计信息,可用于判断有多少区域进入候选、多少区域实际执行了动作,以及配额被触发了多少次。

十三、使用 DAMON 最容易踩的坑

1. 把 RSS 当成真实工作集

RSS 只代表页面当前驻留在物理内存中,不代表这些页面正在被业务频繁使用。

2. 把一次 access=0 当成永久冷内存

DAMON 是采样监控。短时间没有采样到访问,不代表区域永远不会被访问。

3. 把 age 简单理解为最后访问时间

age 表示当前访问模式的持续时间,需要结合访问频率一起判断。

4. 一上来就启用 pageout

正确顺序应该是:

快照观察 → 长时间记录 → 识别候选 → 统计模式 → 小配额灰度 → 自动执行。

5. 采样间隔设置得过小

采样越频繁,理论精度越高,但监控开销也会增加。

官方设计文档建议根据业务访问强度选择聚合周期,并让采样周期与聚合周期保持合理比例。默认的聚合周期在部分大型系统上可能过短,参数应根据实际结果调整。

6. 只看内存下降,不看 Major Fault

冷页被回收后,如果很快再次访问,就可能产生大量 Major Page Fault 和磁盘读取。

这通常意味着冷内存阈值过于激进,或者识别出的并不是真正冷页。

7. 忽视数据库和缓存的预热成本

数据库 Buffer Pool、搜索索引和本地缓存即使一段时间没有访问,也可能在流量恢复后迅速变热。

回收这些页面虽然能暂时降低 RSS,却可能让下一次流量高峰承担重新加载成本。

8. 同时启用多个页面访问跟踪机制

DAMON 的部分操作集通过 PTE Accessed Bit 检查访问情况。官方文档指出,它可能与 Idle Page Tracking 等同样依赖访问位的机制产生干扰,因此同时使用前应进行专门评估。

十四、结语

过去我们做内存优化,关注的通常是一个数字:

这个进程占了多少内存?

而 DAMON 带来的变化,是把问题继续向前推进一步:

这些内存现在到底有没有被使用?

一旦能够看到内存访问频率和冷热变化,很多问题都会变得更加清晰:

  • RSS 高,到底是泄漏还是合理缓存;
  • JVM 堆大,到底是工作集需要还是配置过度;
  • 系统抖动,到底是 CPU 不足还是内存回收;
  • 容器接近 Limit,到底应该扩容还是缩小无效常驻内存;
  • 冷页面应该直接回收,还是只降低 LRU 优先级。

DAMON 并不是一个“一键释放内存”的工具。

它真正的价值,是把内存管理从静态容量统计,推进到动态访问模式分析。生产环境也不应该从 pageout 开始,而应该从观测开始:先知道哪些内存真正热、哪些内存长期冷,再决定是否调整缓存、堆大小、容器规格和回收策略。

当系统开始从“用了多少内存”转向“这些内存有没有价值”,内存优化才真正进入精细化阶段。

0 条评论
如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  0 条评论