1. 先把“内存domain”这个词说清楚我记得第一次在内核文档里看到“memory domain”这个词时第一反应是“这跟DNS域名有什么关系”后来才搞明白它跟你想的那个“domain”完全是两码事——Linux内存管理语境下的domain指的是物理内存被划分成的不同管理区域和访问域核心解决的是三件事内存从哪里来物理资源在哪、谁能访问硬件设备限制、怎么分配才公平隔离和策略。你去看内核源码malloc、free 这些用户态接口底下那一大坨逻辑本质上都在跟“domain”打交道。比如最常见的ZONE_DMA、ZONE_NORMAL、ZONE_MOVABLE这仨就是典型的内存domain再比如NUMA体系下每个CPU节点对应的内存节点node也是一种domain还有cgroup里给进程组划定的memory controller 域同样是domain。可以说理解了内存domain你就理解了大半个Linux内存管理。这篇文章适合三类人看一是被线上内存问题折磨的运维和SRE二是想搞懂内核内存子系统、准备面试或者单纯好奇的开发者三是在容器环境里做资源隔离和性能调优的人。我会把概念、原理、排查手段全部串起来讲尽量不堆术语但该深入的地方也不会含糊。2. 为什么非要把内存“分区划域”2.1 硬件给了Linux一个“不公平”的内存很多人写业务代码时有个错觉所有内存是等价的访问速度一样谁都能用。这在单核、单内存条的远古时代勉强成立但现代服务器早就不是这样了。拿一台NUMA架构的服务器举例两颗物理CPU各自直连一部分内存条跨CPU访问对面内存要通过片间互联总线延迟翻倍都不止。操作系统如果脑袋一根筋把所有内存当一锅粥随意分配那么一个跑在CPU0上的进程很可能拿到的是CPU1那边的内存每次读写都绕远路性能损耗非常难看。内核的解决方案就是按访问成本和物理位置把内存划分成不同的domain分配时尽量让进程靠近自己所在的CPU拿内存。你在numactl --hardware里看到的 node0、node1就是最典型的内存domain——每个node代表一块“离某个CPU很近”的物理内存。2.2 设备DMA的“挑食”逼出了ZONE_DMA还有一个硬件层面的硬约束。老一点的设备做DMA传输时只能在物理内存的低16MB或低4GB范围内寻址。如果内核把所有内存统一管理、随机分配那么一个网卡要发数据时发现自己拿到的缓冲区在高地址区根本没法DMA只能先拷贝到低地址区再传输白白多一次内存拷贝。于是内核把物理内存按地址范围切成了几个zoneZONE_DMAx86上通常是前16MBx86_64上DMA和DMA32配合使用、ZONE_NORMAL普通可寻址区、ZONE_MOVABLE可迁移区。这就是最经典的内存domain划分逻辑——不是内核闲得没事搞一堆概念是硬件逼着它这么干。2.3 隔离需求催生了cgroup内存域前两种domain解决的是“物理分布”问题而cgroup的memory domain解决的是“资源隔离”问题。一个物理内存条上的空间怎么在不同业务进程组之间切分A业务疯狂申请内存不能把B业务挤到OOM。这就需要一套按层级划分的内存控制域每个域有独立的usage统计、limit限制、回收策略。在云原生环境里每个Pod的内存限额底层就是靠这个机制实现的。所以你看domain这个概念贯穿了Linux内存管理的物理层、分配层和资源管理层它不是某个单一机制而是一整套“分而治之”的设计思想。3. 内存domain的层级结构与内核实现3.1 Node、Zone、Page从大到小的三级体系内核把物理内存组织成Node节点→ Zone区→ Page页的三级结构这就是最底层的内存domain地图。Node对应NUMA节点每个节点描述一块与某个CPU“亲近”的物理内存区。内核用struct pglist_data表示一个node它挂着自己的页分配器和内存统计信息。在UMA统一内存访问架构机器上通常只有一个node所有CPU访问内存的成本相同而在NUMA机器上node数量跟物理CPU插槽直接相关。Zone是node内部按用途切分的子域保留着物理地址的限制和迁移属性。x86_64架构上典型的zone包括Zone名称物理地址范围x86_64典型用途DMA0 ~ 16MB老设备ISA DMA寻址DMA320 ~ 4GB32位设备DMA寻址NORMAL4GB以上普通内存分配MOVABLE取决于物理布局可在线移除/迁移的内存Page是最小内存单元默认4KB大页HugePage可以到2MB或1GB。每个page归属于某个zone内核通过page-flags可以反查它属于哪个node、哪个zone。这一层级的归属关系直接影响分配和回收的路径。你可以用一条命令同时看到这套层级——cat /proc/zoneinfo会按node → zone → memory statistics逐级打印里面能看到每个zone的free pages数量、watermark水位、活跃非活跃页数等关键指标。3.2 伙伴系统如何在domain内分配内存页分配的核心是伙伴系统buddy system它按2的幂次把空闲页分组挂在不同的free_list上。分配时内核会先从当前CPU所属node的对应zone里找合适大小的块找不到就依次向上合并、跨zone借调甚至触发内存回收。这里有个关键逻辑分配优先级是从“最匹配domain”到“次匹配domain”逐级降级的。比如在NUMA机器上进程缺内存时先找所在node的本地区域node本地内存不足才允许跑到远端node去借——这就是“local allocation优先”策略。对应的内核提供numa_zonelist_order这种东西来控制跨node的新鲜程度。还有一个容易被忽视的机制是watermark水位线。每个zone设置了min、low、high三档水位空闲页在low水位以下时内核会触发kswapd异步回收降到min水位以下时直接进入同步的direct reclaim。这个水位机制就是为了防止单个zone内存耗尽导致DMA等关键domain无内存可用。3.3 内存策略mempolicy决定你能用哪个domain管理内存domain的另一个重要维度是策略内核通过mempolicy内存策略来控制进程的内存分配行为。常见的有这么几种MPOL_DEFAULT本地分配优先可以从远端借调。MPOL_BIND严格限定在指定的node上分配不允许跨domain。MPOL_PREFERRED优先在指定的某个node分配但实在没有可以落到别的node。MPOL_INTERLEAVE在多个node之间轮流交错分配适合需要均衡利用总带宽的HPC场景。设置策略用mbind或者set_mempolicy系统调用命令行工具就是numactl。比如把进程绑到node0上并且不允许用node1的内存numactl --membind0 --cpunodebind0 ./my_service这个命令会把进程的CPU亲和性和内存亲和性都约束在node0内。很多高性能服务比如Redis、数据库实例在NUMA机器上性能波动排查下来往往就是没做CPU和内存的domain绑定导致进程在不同node间“漂移”。4. 内存domain的实战观测与调整4.1 用zoneinfo和buddyinfo摸清内存家底排查内存问题我的第一站永远是/proc/zoneinfo和/proc/buddyinfo。前者看每个zone的用量和水位后者看每个zone里伙伴系统各阶空闲块的数量。cat /proc/zoneinfo输出里重点看这几项free该zone当前空闲页数。min / low / high三档水位注意单位是页面的千分之一不是实际页数。nr_free_pages伙伴系统挂在free_list上的总页数。managed该zone由伙伴系统管理的总页数。如果某个zone的free长期贴着min水位线说明这个domain压力极大分配器随时可能进入direct reclaim进程会卡在内存回收上表现就是CPU sys时间飙升、延迟抖动。再配合buddyinfo看是不是外部碎片严重——如果free页总数不缺但大块空闲页高阶order没人能分配出来那就是碎片问题需要靠compact或者开启THP来缓解。4.2 用numactl绑定NUMA domain在NUMA服务器上跑多实例服务我强烈建议把“绑核”和“绑内存”一起做。只绑CPU不绑内存你只是在给调度器做了一半约束。实操中的正确做法# 查看硬件拓扑 numactl --hardware # 启动时绑定CPU和内存到node0 numactl --cpunodebind0 --membind0 ./app # 如果服务已经跑起来了用numastat观察现状 numastat -cm我踩过的一个典型坑某个Java服务在32核机器上部署未经绑定phenomenon是每隔一段时间出现一次秒级卡顿。用numastat一看进程内存里 node0和node1几乎对半而且大量page在节点间反复迁移跨node访问量巨大总延迟被拉高。后来改成绑node0部署卡顿直接消失。NUMA的跨节点访问代价在内存密集型场景真的不是小数目。4.3 用cgroup v2切分内存域现代Linux发行版基本都切到了cgroup v2内存控制器被统一挂到/sys/fs/cgroup/xxx/memory.max这种扁平路径下。它的核心功能是给一组进程建一个独立的“内存domain”复用范围清晰可控。最常见的配置流程# 创建一个控制组 sudo mkdir /sys/fs/cgroup/webapp # 设置内存上限例如8GB echo 8589934592 /sys/fs/cgroup/webapp/memory.max # 把进程PID写进cgroup.procs echo 12345 /sys/fs/cgroup/webapp/cgroup.procs这时12345进程及其子进程的所有内存分配都会被约束在这个domain里超过上限就触发回收或OOM。比“在全局杀进程”安全得多。实际生产里我们通常给每个微服务建独立domain互不干扰。cgroup v2里还有一个很实用的参数memory.swap.max控制domain内的swap上限还有memory.high它是软水位超过后内核开始强力回收但不会立即杀进程专门给内存突发型业务用。4.4 用/proc/pagemap定位page所在domain如果想知道某个进程的虚拟页到底落在哪个node哪个zone可以读取/proc/pid/pagemap但格式比较底层需要解析bit位日常不太推荐。更省事的是直接用工具# 查看进程的物理内存分布 sudo pagemap 12345 # 或者用更直观的numastat numastat -p 12345numastat -p能显示进程在node0/node1上的内存分配量和缺页统计一眼看出内存有没有跨domain漂移。5. 常见的内存domain故障与排查实录5.1 “domain forbidden”和Linux内存domain没关系写这篇博客前我特意搜了下热门词发现很多人都把“domain forbidden”当成内存domain的报错搜进来然后一脸懵。这里必须澄清你遇到的 “code:1004 error:domain forbidden” 通常来自某个API网关或者HTTP服务端意思是请求的域名被服务端拒绝访问了这跟Linux内核的内存domain完全是两码事。如果你真的是在做内存排查结果日志里冒出这么个错误大概率是业务层调用的某个接口在做域名鉴权时挂了优先查网络代理、网关策略或者证书配置别在内核内存参数上纠结。反过来如果内核报类似 “out of memory” 或者 “allocation failure” 的错那才是内存domain资源出了问题。5.2 经典故障一DMA zone内存耗尽现象老一些的设备驱动加载失败dmesg里有 “allocation from DMA zone failed” 之类的话。排查思路cat /proc/zoneinfo看DMA zone的free是否已经接近0。cat /proc/buddyinfo看DMA zone还有没有可用的连续块。确认是否有驱动或者子系统在疯狂消耗低地址内存。解决办法通常是调整DMA内存预留参数内核参数vm.min_free_kbytes适当调大但要注意副作用、升级设备驱动支持新寻址方式或者更换硬件。碰上老网卡老RAID卡时这种问题很常见。5.3 经典故障二NUMA内存“旱涝不均”现象40核机器node0被进程占满node1空闲一大半但整体内存又没满奇怪的是部分进程非常卡。原因进程的CPU被调度到了node1但内存还留在node0跨节点访问。或者进程从node0启动绑了node0的CPU但内存被远端分配策略打断部分page漂到了node1。排查手段# 看全局node负载 numastat # 看具体进程的node分布 numastat -p pid # 看调用链上频繁缺页的node perf stat -e node-load-misses,node-store-misses -p pid修正手段要么重启进程并绑node要么在运行时把内存迁回本地# 将进程的内存迁移到指定node需要libnuma支持 numactl --membind0 --preferred0 -p pid注意-p参数只能影响后续分配不能强制迁移已有页。要迁移已经在物理内存里的页只能靠move_pages()系统调用或者干脆重启进程。5.4 经典故障三容器内存domain被“看不见”的页吃掉现象cgroup的内存统计显示用量不高但宿主机整体内存却持续走低容器内业务偶发卡顿。原因之一是page cache挂在容器domain里虽然memory.current包含page cache但很多团队只看memory.kmem或者memory.rss忽略了缓存部分。另一个原因是没人看memory.high——如果设了high但没有swap内核回收行为会很激进跟预期表现不符。正确检查方式是cat /sys/fs/cgroup/domain/memory.current cat /sys/fs/cgroup/domain/memory.stat重点看anon、file、kernel_stack、slab_reclaimable、pgscan这些字段。如果file页缓存占比异常高且回收频繁就考虑在容器内调整脏页参数比如调小/proc/sys/vm/dirty_ratio和dirty_background_ratio或者让业务主动用posix_fadvise丢弃不需要的缓存页。5.5 实战排查速查表症状可能原因优先检查内存余量充足但分配失败zone碎片化或watermark过低/proc/buddyinfo、vm.min_free_kbytes进程延迟抖动NUMA跨节点访问numastat -p、node-load-misses容器频繁卡顿memory.high设置不合理、回收激进memory.stat、pgscanDMA分配失败低地址内存耗尽/proc/zoneinfo的DMA区页面回收风暴dirty_ratio过高、写压力大vmstat、dirty_ratio/dirty_background_ratio6. 内核参数调优在domain边缘动刀6.1 vm.min_free_kbytes 的取舍设置低位内存预留可以避免在内存吃紧时分配器直接慌乱但设太高会导致可用内存变小整个系统频繁回收。我的经验值是从总内存的0.1%~0.3%开始试echo 65536 /proc/sys/vm/min_free_kbytes设置完用/proc/zoneinfo观察各zone水位是否稳定不要拍脑袋调大。这玩意儿在NUMA机器上还要考虑跨node均衡问题预留过低会让高优先级分配失败。6.2 控制NUMA自动均衡内核有个自动迁移热点页的功能numa_balancing。它会在进程访问跨node页时自动做balance但对延迟极其敏感的业务来说自动迁移可能带来不可预测的抖动。这个开关是进程级的也可以用系统全局变量调# 全局关闭自动NUMA均衡 echo 0 /proc/sys/kernel/numa_balancing # 或者启动进程时通过prctl关闭低延迟交易系统、游戏服务器这类场景很多我认识的同事都是直接关闭自动balance改成手动绑node。损失一点均衡性换确定性的性能表现。6.3 大页和domain的关系大页Hugepages尤其是1GB的大页在NUMA场景下要格外注意大页也会按node分开管理。/sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages这个路径就是每个node独立的大页池。你只在全局分配大页的话可能导致node0有充足大页、node1一个大页都没有。多实例部署时配合numactl --membind使用大页内存domain隔离效果会更好。7. 我踩过的几个“domain坑”给你提个醒说点不写进内核文档的实战体会。不要盲目追新参数。网上讨论的内存domain策略优化方案很多是针对特定版本内核和特定业务场景的比如某个参数在RHEL7和RHEL8上默认值完全不一样。线上操作前先看你当前内核版本的文档和/boot/config-*实际编译选项。monitormonitormonitor。调整内存domain相关参数后别只盯内存指标要同时观察应用的延迟、CS上下文切换、cache miss率。有一次我调大min_free_kbytes后内存分配是稳了但系统整体性能下降了8%因为可用页变少回收更频繁了。后面用perf聚焦compact_stall和allocstall才发现问题回退参数后恢复。domain的理解要落到“定位”上。线上排查时拿到一个“内存泄漏”的case先别急着查业务代码用numastat、zoneinfo、slabtop确定问题出在哪个内存域——是page cache域、slab域还是匿名页域。比如常见的内存泄漏其实是slab里的kmem_cache只增不减那就跟伙伴系统无关而是某个内核模块在漏。生产环境脚本化。每次排查内存domain问题流程雷同信息量又大我非常建议把这套检查命令固化成脚本统一输出。比如#!/bin/bash echo zoneinfo key fields grep -E Node|free|min|low|high|managed /proc/zoneinfo echo buddyinfo cat /proc/buddyinfo echo numa status numastat echo cgroup top memory find /sys/fs/cgroup -name memory.current -exec sh -c echo -n $1: cat $1 _ {} \;这套东西能在5分钟内把内存domain的健康状态摸个底比东敲一条西敲一条靠谱得多。最后再说一个容易被忽略的小技巧观察/proc/pressure/memory。这个是PSIPressure Stall Information接口能看到内存domain因为回收造成的任务停滞时间比例。只要some或者full的数值稳定上升说明某一块内存域已经是系统性瓶颈了再用上面那些工具去定位到底卡在哪个domain准没错。Linux内存domain这套东西说简单就是“分区”但真正做到不过度分配、不跨节点漂移、不互相干扰需要从硬件拓扑、内核分配策略、cgroup隔离三个层面积累经验。希望这篇能帮你少走点弯路特别是那些跟我一样第一次看到“domain”满头问号的朋友。