
1. 从一次内存抖动排查说起DAMON到底解决了什么问题前段时间帮一个朋友看他们线上服务的问题现象很典型一台跑了容器化业务的机器内存看着没满但业务侧偶发延迟毛刺P99时不时飙一下。用常规的free、vmstat看指标都挺正常si/so也不高但就是有抖动。后来用perf抓了一下发现大量时间耗在kswapd和直接回收路径上也就是说内存其实一直在被回收只是没触发到明显的swap换入换出而已。这个场景其实非常常见。传统的内存监控手段不管是/proc/meminfo还是vmstat给的都是全局聚合的视角——整机还剩多少内存、回收了多少页。但你真正想知道的是到底是哪个进程、哪段地址范围在被频繁访问导致这块内存一直被判定为热而无法回收这个问题在聚合视角下是回答不了的。DAMONData Access MONitor就是冲着这个痛点来的。它是Linux内核里一个轻量级的数据访问监控框架核心能力是让你指定一段虚拟地址范围可以是某个进程的也可以是物理地址空间然后它用极低的开销去采样这段范围的访问情况最后告诉你这段内存里哪些区域是热的、哪些是冷的。有了这个信息你就能做很多事情精准回收冷内存、做分层内存管理、给业务做内存画像、甚至驱动一些自动化的内存优化策略。这篇文章我打算把DAMON从原理到实操完整拆一遍。适合谁看如果你在做内核性能优化、内存管理、容器资源调度或者单纯对内核子系统感兴趣想动手跑一跑那这篇应该能帮到你。我会尽量把为什么这么设计讲清楚而不是只丢一堆API和参数。毕竟DAMON这东西光看文档你会觉得挺简单真上手调的时候坑不少。2. DAMON的整体设计与核心思路拆解2.1 为什么不用传统的页表扫描方案要理解DAMON的设计得先知道它不做什么。内核里其实早就有访问监控的机制比如通过页表的Accessed位PTE young bit来判断一个页最近有没有被访问。你完全可以写个模块定期遍历页表把Accessed位读出来再清掉这样就能统计访问频率。但这个方案有几个致命问题。第一遍历页表本身开销巨大。一个进程的页表可能几十万上百万个PTE你每次扫描都要走一遍这在生产环境根本扛不住。第二Accessed位是二值的只能告诉你这段时间内访问过没有没法量化访问的密集程度。第三精度和开销难以平衡你想精度高就得扫得勤扫得勤开销就爆炸。DAMON换了个思路基于采样的区域级监控。它不逐页看而是把目标地址范围切成若干区域region然后随机挑一些位置去采样通过统计采样命中情况来推断每个区域的访问热度。这个思路的关键在于——用统计近似代替精确扫描牺牲一点点精度换来数量级上的开销下降。提示DAMON的采样是随机位置采样不是随机区域采样。它会在整个目标范围内随机撒点然后看这个点落在哪个区域从而给对应区域累加计数。这个设计细节后面讲原理时会再展开。2.2 三个核心抽象Target、Region、SchemeDAMON的整个模型可以浓缩成三个概念理解了这三个基本就理解了一半。Target监控目标就是你要监控的那段地址空间。它可以是某个进程的虚拟地址空间pid指定也可以是物理地址空间paddr指定。一个DAMON实例可以挂多个target。Region区域Target被切分成若干个region每个region是一段连续的地址范围带有一个访问频率的统计值。DAMON会动态调整region的划分——访问模式变化剧烈的地方切细一点平稳的地方合并粗一点。这个动态调整机制叫自适应区域调整adaptive regions adjustment是DAMON能兼顾精度和开销的核心。Scheme方案这是DAMON的动作部分。你定义一组规则比如如果某个region的访问频率低于阈值就把它标记为冷内存然后DAMON在每轮监控结束后对满足条件的region执行你指定的动作。动作可以是stat只统计、pageout换出、cold标记为冷、madvise等。这三者的关系是Target提供监控对象Region承载监控结果Scheme决定后续动作。整个DAMON就是围绕这三者循环运转的。2.3 采样机制与开销控制DAMON的采样逻辑我尽量用大白话讲清楚。假设你的target是1GB的地址范围DAMON把它切成N个region。每一轮采样它会在1GB范围内随机选一个地址找到这个地址属于哪个region检查这个地址对应的页最近有没有被访问通过页表的Accessed位如果访问了给这个region的计数加一。重复这个过程sample_count次比如5000次一轮就结束了。然后DAMON根据每个region的命中比例估算出它的访问频率。这里有个很妙的点采样次数是固定的跟target大小无关。你监控1GB和监控100GB每轮的采样开销是一样的。这就是DAMON能扩展到很大地址范围的原因。当然target越大每个region分到的采样点越少统计噪声就越大所以region的粒度需要相应调整。开销控制上DAMON还做了几件事。一是采样间隔可调sample_interval默认5ms你可以拉长来降开销。二是聚合间隔可调aggr_interval默认100ms控制多久汇总一次结果。三是最小region数min_nr_regions和最大region数max_nr_regions的约束防止region数量失控导致管理开销上升。2.4 自适应区域调整DAMON的精髓所在这是我觉得DAMON设计里最值得琢磨的部分。固定切分region的问题是你不知道该切多细。切太细region数量爆炸管理开销大切太粗热点区域和冷区域混在一起统计结果没意义。DAMON的做法是动态调整。每轮聚合后它会看相邻region的访问频率差异。如果两个相邻region的频率差很大说明这里有个边界需要切开如果频率差不多说明它们本质是一类可以合并。通过这种分裂-合并的迭代region的划分会逐渐收敛到跟实际访问模式匹配的状态。具体算法上DAMON用的是类似加权直方图的思路。它维护每个region的访问计数然后根据计数的分布来决定分裂和合并。分裂时会在频率变化最大的地方切一刀合并时会把频率相近的相邻region并起来。整个过程是自动的你只需要给个region数量的上下界就行。注意自适应调整需要几轮才能收敛所以DAMON刚启动时统计结果可能不太准。如果你要做精确分析建议让它跑一段时间比如几十个聚合周期再看数据。3. 核心细节解析与实操要点3.1 内核配置编译选项怎么选要用DAMON首先内核得编进去。相关的配置项有这么几个CONFIG_DAMONy CONFIG_DAMON_VADDRy CONFIG_DAMON_PADDRy CONFIG_DAMON_SYSFSy CONFIG_DAMON_DBGFSy逐个说下。CONFIG_DAMON是核心框架必须开。CONFIG_DAMON_VADDR提供虚拟地址监控能力CONFIG_DAMON_PADDR提供物理地址监控能力这两个按需开——如果你只监控进程地址空间PADDR可以不开。CONFIG_DAMON_SYSFS和CONFIG_DAMON_DBGFS是两套用户态接口SYSFS是较新的、推荐的接口DBGFS是老的调试接口功能更全但不够稳定。我的建议是两个接口都开。SYSFS适合做生产环境的稳定控制DBGFS适合做实验和调试因为它的功能更细能看到更多中间状态。实测下来DBGFS的schemes目录能直接看到每个region的统计调参的时候非常方便。编译的时候有个坑DAMON依赖CONFIG_MMU在没有MMU的平台上编不了。另外如果你用的是较老的内核5.15之前DAMON的接口和现在差别挺大建议至少用6.1以上的版本接口稳定功能也全。3.2 用户态接口SYSFS与DBGFS的区别两套接口的定位不太一样我列个表对比一下维度SYSFS接口DBGFS接口挂载点/sys/kernel/mm/damon/admin//sys/kernel/debug/damon/稳定性稳定ABI有保证调试用可能变功能粒度较粗够用细能看到中间状态适用场景生产环境实验、调参多实例支持支持支持SYSFS接口的目录结构是分层的kdamonds/下面可以有多个kdamond实例每个实例下面有contexts/每个context下面有targets/和schemes/。这个层级关系要理清楚不然配置的时候容易搞混。DBGFS就直白多了直接是pid/这样的目录进去就是schemes、monitoring_attrs这些文件。调参的时候我一般用DBGFS因为改完立刻能看到效果不用像SYSFS那样一层层cd进去。提示DBGFS需要内核开CONFIG_DEBUG_FS并且debugfs要挂载。有些发行版默认不挂debugfs记得先mount -t debugfs none /sys/kernel/debug。3.3 关键参数逐个拆解DAMON的参数不少但真正影响行为的就那么几个。我把最关键的挑出来讲。sampling interval采样间隔两次采样之间的时间。默认5ms。这个值越小采样越密统计越准但开销越大。实测在普通x86机器上5ms间隔、5000次采样的开销大概在1% CPU以内可以接受。如果你监控的地址范围特别大可以适当拉长到10ms甚至20ms。aggregation interval聚合间隔多久把采样结果汇总一次并触发scheme执行。默认100ms。这个值决定了你响应访问模式变化的速度。设太小scheme频繁触发开销上升设太大响应迟钝。一般保持默认就行除非你有特殊的实时性要求。sampling count采样次数每轮采样的次数。默认根据region数量自动算一般是region数的几倍。这个值直接影响统计精度。有个经验公式采样次数至少要是region数的10倍以上统计结果才比较可信。比如你有100个region采样次数最好在1000以上。region数量上下界min_nr_regions默认10max_nr_regions默认1000。这两个值约束了自适应调整的范围。如果你监控的地址范围访问模式很复杂可以把max调大但注意region越多每轮的管理开销越大。scheme的access pattern这是scheme的核心定义了什么样的region算命中。它有三个维度sz大小范围、nr_accesses访问次数范围、age年龄范围。比如你可以定义大小在4KB到1MB之间、访问次数小于10、年龄大于5个聚合周期的region为冷内存。这三个条件是与的关系都满足才触发。3.4 监控目标的指定方式指定target有几种方式各有适用场景。按pid监控最常用。echo pid target/pid就行。DAMON会监控这个进程的整个虚拟地址空间。注意进程退出后target会失效需要重新配置。按物理地址监控echo start end target/paddr。适合监控特定物理内存区域比如某个设备的内存、或者你想做整机物理内存画像的时候。按虚拟地址范围监控这个稍微绕一点需要先指定pid再通过init_regions指定具体的地址范围。DBGFS下是往pid/init_regions写start end weight的格式。weight是初始权重影响自适应调整的初始划分。我个人的经验是做进程级内存分析用pid就够了DAMON会自动覆盖整个地址空间。只有当你明确知道要监控某段特定地址比如某个mmap出来的大块内存时才需要手动指定范围。4. 实操过程与核心环节实现4.1 环境准备与内核编译先确认你的内核版本。uname -r看一下建议6.1以上。然后检查DAMON是否已经编进去了ls /sys/kernel/mm/damon/admin/ 2/dev/null echo SYSFS OK ls /sys/kernel/debug/damon/ 2/dev/null echo DBGFS OK如果目录不存在说明内核没开DAMON需要重新编译。编译流程不复杂但有几个点要注意。下载内核源码后make menuconfig在Memory Management options下面找到DAMON相关的项。或者直接改.configCONFIG_DAMONy CONFIG_DAMON_VADDRy CONFIG_DAMON_PADDRy CONFIG_DAMON_SYSFSy CONFIG_DAMON_DBGFSy CONFIG_DEBUG_FSy然后make -j$(nproc)编译make modules_install make install安装重启选新内核。注意如果你是在虚拟机里做实验编译内核前先确认磁盘空间够。完整编译一次内核大概需要20-30GB空间别编到一半空间不够了。4.2 用DBGFS跑第一个监控实例DBGFS上手最快我们先用它跑一个最简单的例子。假设要监控pid为1234的进程。# 挂载debugfs如果没挂的话 mount -t debugfs none /sys/kernel/debug # 进入DAMON目录 cd /sys/kernel/debug/damon # 创建监控目标 mkdir 1234 # 设置监控属性 echo 5000 1234/monitoring_attrs/sample_count echo 5000 1234/monitoring_attrs/sample_interval echo 100000 1234/monitoring_attrs/aggr_interval echo 10 1234/monitoring_attrs/min_nr_regions echo 1000 1234/monitoring_attrs/max_nr_regions # 启动监控 echo on monitor_on启动后1234/目录下会出现schemes、regions等文件。regions文件里就是当前的region划分和统计结果格式是start end nr_accesses age。你可以cat出来看看。这里有个细节sample_interval和aggr_interval的单位是微秒不是毫秒。我一开始按毫秒填结果采样间隔变成了5秒统计完全不准。这个坑踩过一次就记住了。4.3 配置一个冷内存识别scheme光监控不够我们配个scheme让它自动识别冷内存。在1234/schemes/下创建一个schememkdir 1234/schemes/s cd 1234/schemes/s # 设置访问模式访问次数小于10年龄大于3 echo 0 0 10 3 access_pattern/sz/nr_accesses/age # 设置动作标记为冷内存 echo cold action # 设置应用范围所有region echo 0 target_nid等等这里的access_pattern格式我得说清楚。DBGFS下access pattern是三个文件sz、nr_accesses、age每个文件里写min max。所以正确的写法是echo 0 0 access_pattern/sz echo 0 10 access_pattern/nr_accesses echo 3 0 access_pattern/ageage的max写0表示无上限。这样配置的意思是大小不限、访问次数在0到10之间、年龄大于3的region执行cold动作。cold动作本身不做什么实际的事它只是把region标记出来你可以通过regions文件看到哪些被标记了。如果你想真的回收内存可以用pageout动作但那需要配合其他机制风险也更高建议先在测试环境验证。4.4 用SYSFS做生产级配置DBGFS适合实验生产环境建议用SYSFS。SYSFS的配置流程稍微繁琐一点因为层级多。cd /sys/kernel/mm/damon/admin # 创建kdamond实例 echo 1 kdamonds/nr_kdamonds # 配置第一个kdamond cd kdamonds/0 echo 1 contexts/nr_contexts # 配置context cd contexts/0 echo 1 targets/nr_targets echo 1 schemes/nr_schemes # 配置target cd targets/0 echo 1234 pid # 配置scheme cd ../../schemes/0 echo 0 access_pattern/sz/min echo 0 access_pattern/sz/max echo 0 access_pattern/nr_accesses/min echo 10 access_pattern/nr_accesses/max echo 3 access_pattern/age/min echo 0 access_pattern/age/max echo cold action # 启动 cd /sys/kernel/mm/damon/admin/kdamonds/0 echo on stateSYSFS的路径比较深建议写个脚本封装一下。我自己的做法是写个bash函数把常用的配置参数化这样切换监控目标的时候不用每次敲一长串路径。4.5 读取和分析监控结果监控跑起来后怎么读结果DBGFS下直接cat pid/regions输出格式是每行一个region0 4096 15 2 4096 8192 3 5 8192 16384 0 8 ...四列分别是起始地址、结束地址、访问次数、年龄。访问次数越高说明越热年龄越大说明越久没被访问年龄的定义是连续多少个聚合周期访问次数没超过阈值。SYSFS下读取稍微麻烦点需要先触发一次update_schemes_stats然后从schemes/0/stats里读。不过SYSFS的统计粒度比较粗只有总的命中次数看不到每个region的细节。所以做详细分析还是得用DBGFS。分析的时候我一般关注两个指标热区占比和冷区年龄分布。热区占比高说明内存压力大冷区年龄分布能告诉你哪些内存是真的可以回收的。如果大量region的年龄都很高但访问次数不为零说明这些内存是偶尔被碰一下的类型回收要谨慎。5. 常见问题与排查技巧实录5.1 监控启动失败排查表DAMON启动失败的原因挺多我整理了个速查表现象可能原因排查方法monitor_on写入失败内核没开DAMON检查/boot/config-$(uname -r)里的CONFIG_DAMON创建pid目录失败进程不存在或无权限ps -p pid确认检查是否rootregions为空采样还没跑够等几个聚合周期再看统计结果全是0采样间隔单位搞错确认是微秒不是毫秒开销异常高region数量过多调小max_nr_regionsscheme不触发access pattern配置错检查三个维度的min/max是否合理5.2 统计结果不准怎么办统计不准是最常见的问题原因通常有几个。采样次数不够。前面说过采样次数至少要是region数的10倍。如果你有500个region但只采样1000次那每个region平均才2个采样点统计噪声巨大。解决办法是调大sample_count或者调小max_nr_regions。聚合间隔太短。聚合间隔太短的话每轮采样的样本还没积累够就汇总了结果波动大。建议aggr_interval至少是sample_interval的20倍以上。自适应调整没收敛。DAMON刚启动时region划分是初始值需要几轮调整才能匹配实际访问模式。如果你在启动后立刻读结果看到的可能是还没收敛的中间状态。建议至少等10个聚合周期。提示判断是否收敛可以连续读几次regions看region的边界是否稳定。如果边界一直在变说明还在调整中。5.3 开销控制的实战经验DAMON号称低开销但配置不当照样能把CPU吃满。我实测过几组配置分享下经验。在4核8G的虚拟机上监控一个约2GB地址空间的进程配置CPU开销统计质量sample_count1000, interval5ms0.5%一般sample_count5000, interval5ms~1%好sample_count5000, interval1ms~5%很好sample_count20000, interval5ms~4%很好结论是sample_count和sample_interval是开销的主要来源。生产环境建议用5000次采样、5ms间隔这个组合开销和精度比较平衡。如果只是做粗略画像1000次采样也够用。还有个隐藏开销来源是region数量。region越多每轮聚合时的排序、分裂、合并操作越耗时。我见过有人把max_nr_regions设成10000结果聚合阶段CPU直接飙到20%。一般1000以内是安全的。5.4 几个容易踩的坑坑一pid复用。Linux的pid是会复用的。你监控pid 1234进程退出后pid 1234可能被新进程占用DAMON会继续监控这个新进程但你以为还在监控老进程。解决办法是监控的时候记录进程的启动时间或者用cgroup路径而不是pid来指定target较新内核支持。坑二地址空间变化。进程运行过程中会mmap/munmap地址空间是动态变化的。DAMON的region划分是基于监控开始时的快照如果地址空间变化剧烈统计结果会失真。对于这种场景建议缩短聚合间隔让DAMON更快地重新调整。坑三DBGFS的写入顺序。DBGFS下配置scheme时必须先写access_pattern再写action顺序反了会报错。这个在文档里没明说我是试出来的。坑四多实例的资源竞争。同时跑多个kdamond实例会争抢CPU和内存带宽。如果非要跑多个建议错开它们的采样时间或者降低各自的采样频率。5.5 和其他工具的配合使用DAMON不是孤立的它跟其他工具配合能发挥更大价值。跟perf配合DAMON告诉你哪段内存热perf告诉你为什么热。先用DAMON定位热点区域再用perf record针对性地采样效率高很多。跟cgroup配合DAMON可以按cgroup监控需要内核支持这样你能做容器级的内存画像。对于多租户环境这个能力很实用。跟numa配合DAMON的region统计可以指导NUMA迁移决策。热region往近端节点迁冷region往远端节点迁能显著降低内存访问延迟。不过这个需要你自己写迁移逻辑DAMON只提供数据。6. 进阶玩法与扩展思路6.1 基于DAMON的自动化内存回收DAMON最实用的进阶玩法是驱动自动化内存回收。思路是配一个pageoutscheme让DAMON自动把冷内存换出。这样比内核默认的LRU回收更精准因为它基于实际的访问频率而不是粗略的LRU链表。配置上把action从cold改成pageout然后调整access pattern的阈值。阈值怎么定我的经验是先用cold模式跑一段时间观察冷region的访问次数分布然后取一个略高于分布峰值的值作为阈值。比如大部分冷region的访问次数在5以下那阈值就设10留点余量。注意pageout动作会真的触发内存换出在SSD上还好在机械盘上可能引起IO抖动。生产环境用之前一定要在测试环境验证并且配合监控告警。6.2 做进程级内存画像如果你想知道一个进程的内存访问模式长什么样DAMON是个很好的工具。具体做法是监控目标进程跑一段时间然后把regions文件导出用脚本分析。分析维度可以包括热区占总地址空间的比例、热区的地址分布是集中在堆还是栈还是mmap区、访问频率的直方图分布。这些信息对优化程序的内存布局很有价值。比如你发现热区集中在几个大的mmap块里那可能说明这些块的数据结构有优化空间。我写过一个简单的分析脚本把regions输出转成CSV然后用Python画直方图。代码不复杂核心就是解析那四列数据然后按访问次数分桶。6.3 多目标监控的配置技巧DAMON支持一个实例监控多个target。配置上就是在targets/下创建多个子目录每个指定不同的pid或地址范围。但要注意多个target是共享同一套monitoring_attrs和schemes的也就是说采样参数和动作规则是统一的。如果你需要对不同target用不同的策略那就得开多个kdamond实例。多实例的配置就是kdamonds/nr_kdamonds设成大于1然后分别配置。多实例之间是独立的互不影响。多实例的代价是资源开销叠加。每个实例都有自己的采样线程和聚合逻辑跑多了CPU和内存都会涨。我的建议是实例数不要超过CPU核数的一半不然调度开销会很明显。6.4 未来可能的扩展方向DAMON还在持续演进几个我觉得值得关注的方向。一是更细粒度的监控。现在的region粒度受限于采样机制最小也就到页级别。未来如果能做到更细对某些场景会很有用。二是更丰富的动作类型。现在的action主要是stat、cold、pageout、madvise这几种。如果能支持自定义动作比如触发用户态回调玩法就多了。三是和调度器的联动。DAMON的访问数据如果能反馈给调度器指导任务迁移和内存分配对NUMA系统的性能优化会很有帮助。这个方向已经有了一些研究但还没进主线。四是硬件辅助。有些CPU提供了访问监控的硬件支持比如Intel的PEBS、AMD的IBS如果能和DAMON结合精度和开销都能进一步优化。7. 我个人的一些实操体会DAMON这东西我前后折腾了小半年从最开始看文档一头雾水到后来能熟练配置各种scheme中间踩的坑不少。最大的体会是别指望开箱即用。它的默认参数适合做演示真要用到生产环境采样参数、region上下界、scheme阈值都得根据你的实际负载调。另一个体会是DBGFS和SYSFS要配合用。DBGFS调参方便SYSFS部署稳定。我的做法是先在DBGFS上把参数调好验证效果然后再翻译成SYSFS配置部署到生产。这样既快又稳。还有个小心得监控结果要结合业务指标看。DAMON告诉你内存访问模式但这个模式是好是坏得结合业务延迟、吞吐这些指标判断。单纯看DAMON的数据容易钻牛角尖比如你发现某个region特别热但它可能就是业务的核心数据结构热是应该的不该去优化它。最后说个实际效果。前面提到的那个内存抖动问题用DAMON定位后发现是一个缓存模块的冷数据没被及时回收导致内存压力一直下不去。配了个pageoutscheme把冷数据换出后P99延迟从原来的200ms降到了80ms左右。这个收益还是挺可观的也让我对DAMON的实用价值有了更直观的认识。如果你也在做内存相关的优化DAMON值得花时间研究一下。它的学习曲线不算陡但细节挺多建议边看文档边动手跑光看是看不出门道的。