
最近做Linux服务器内存优化时我被一个名词卡住了memory domain。翻内核文档看到的是zone翻ARM手册看到的是Domain翻cgroup文档又看到一个domain。三个地方都叫domain实际管的却是完全不同的东西。这种命名错位在Linux内存管理里其实很常见尤其当你同时碰内核、嵌入式开发和容器调优很容易被绕得晕头转向。这篇文章就把“Linux内存domain”这个概念彻底摊开从硬件MMU、内核物理内存、资源隔离三个维度拆清楚再给出一套能直接落地的管理和排查方法。适合需要调内核参数、排查内存异常、做嵌入式或容器优化的工程师也能帮被各种“内存域”名词搞混的同学理清思路。1. 别被“内存域”绕晕Linux下domain的三种常见含义很多入门资料会把Linux物理内存管理里的zone翻译成“内存域”而在ARM体系里MMU也有一组“domain”寄存器再到cgroup v2里还有cgroup domain这种控制层级描述。这三个domain虽然拼写一样但所属层级完全不同。如果混在一起看很容易得出错误结论。1.1 ARM MMU的domain硬件访问控制域ARM32架构里的Domain是一套硬件地址访问控制机制。整个物理地址空间可以被划分成最多16个域每个域有对应的访问权限由系统控制协处理器里的DACRDomain Access Control Register管理。代码通过操作DACR可以控制某个域是允许无权限访问、只允许特权访问还是完全禁止访问。这套机制在内核启动时就必须初始化好之后切换进程地址空间时也要保证domain状态和页表项一致。我在调试一块老式ARM开发板时曾经遇到过访问某个内存区域直接触发异常dmesg里报的是domain fault而不是常见的translation fault。那一次就是因为在板级初始化代码里错误改了DACR导致内核态的访问落到了被禁止的域上。ARM的domain是硬件层面的“门禁”跟Linux内核里描述内存布局的zone完全是两码事。1.2 内核zone物理内存的逻辑分区Linux内核把物理内存按物理地址范围、硬件限制和用途分成若干个zone。zone才是大多数内核开发者所说的“内存域”。在struct pglist_data里一个NUMA节点下会维护一组node_zones常见的有ZONE_DMA、ZONE_DMA32、ZONE_NORMAL和ZONE_MOVABLE。每个zone负责管理一段物理地址区间内的空闲页并维护各自的伙伴系统链表。zone的划分逻辑其实很贴近硬件现实某些老设备只能做16MB以内的DMA一些PCIe设备只能寻址32位物理地址内核自己的线性直接映射区又需要连续的地址空间。所以内核才需要把物理内存拆成不同的zone分配时根据调用者的GFP掩码决定去哪个zone里取页。这个“内存域”是内核分配器的核心依据和ARM的domain权限控制没有直接关系。1.3 NUMA node与cgroup domain系统和资源控制的域再往上走NUMA体系里每个node是一个物理域它表示一组CPU和一段本地内存之间的亲和关系。numactl --hardware显示的就是节点分布。而cgroup v2也有domain的概念它描述的是cgroup树中的一个可包含子层级的节点。一个cgroup可以是domain、threaded或者domain threaded只有domain类型的cgroup才允许创建普通子cgroup比如memory控制器就必须挂在非threaded的domain层级里。所以你在Linux里提到“内存domain”首先要确认对方说的是哪一层是硬件MMU的domain是内核的zone是NUMA的node还是cgroup的资源域。不同层级管理方法完全不同搞错了轻则配置无效重则引发系统异常。2. 物理内存域的心脏zone与node如何协同管理既然内核zone是“内存domain”最核心的落点那我们从物理内存的层级结构入手看看页、zone和node是怎么串起来的。这个模型理解透了后面无论是看/proc/zoneinfo还是调oom参数都会变得清晰。2.1 从page到node_zones的层级逻辑Linux物理内存管理的基本单位是页page每个物理页对应一个struct page。内存节点node对应一个struct pglist_data里面用一个node_zones[]数组保存该节点下的所有zone。每个zone又包含自己的free_area数组配合伙伴系统按阶数order管理空闲页块。当内核要分配物理页时入口通常是alloc_pages或__get_free_pages最终会走到get_page_from_freelist。这个函数根据分配掩码GFP_MASK里的zone修饰符比如__GFP_DMA或__GFP_HIGHMEM决定优先从哪个zone取页。如果目标zone不够则通过zonelist里的备用zone顺序往下找。这个过程就是所谓“跨内存域分配”。我用一个生活类比解释zone就像小区里的几栋楼DMA楼离门口最近但房间很小NORMAL楼是主力居住楼MOVABLE楼专门留给会搬家的人。要分房给特殊需求的住户比如必须靠近门口的DMA设备就先看对应楼没有了再去隔壁楼借但借的时候要付出一定代价。2.2 DMA/DMA32/NORMAL/MOVABLE四种zone的分工在不同架构上zone的具体划分略有差异但x86和ARM64服务器上最常见的是这四个zone名称物理地址范围主要用途ZONE_DMA通常0~16MB兼容ISA设备、部分老DMA控制器ZONE_DMA320~4GB只能访问32位地址的设备DMAZONE_NORMAL直接映射区内核自身分配、普通进程页表等ZONE_MOVABLE不固定地址可迁移页利于内存热插拔和反碎片ZONE_DMA32在64位系统上尤其重要很多PCIe设备没有IOMMU驱动申请DMA缓冲区时只能用32位地址如果DMA32 zone耗尽设备初始化就会失败。而ZONE_MOVABLE是后来为了缓解碎片的方案它专门放可以迁移的用户页配合memory_hotplug可以整块迁移。默认情况下如果你的内核开了CONFIG_MOVABLE_NODE或使用movable_node启动参数物理内存会有一部分被划进MOVABLE domain。有些云主机上你会看到/proc/zoneinfo里ZONE_DMA和ZONE_DMA32的present页为0这很正常因为虚拟化环境可能不暴露低端内存。不要一看到DMA zone没有内存就紧张。2.3 查看和计算zone布局/proc/zoneinfo手工解析想确认机器上各个内存domain的实际布局最直接的方法就是读/proc/zoneinfo。这个文件比较啰嗦我一般先这样过滤grep -E Node|zone /proc/zoneinfo输出会列出每个节点下的zone名称以及该zone的spanned、present、managed等关键页数。这里简单解释下spanned该zone覆盖的物理页范围可能包含空洞。present实际存在的物理页。managed由伙伴系统管理的页数也就是真正可分配给内核和进程的页。managedpresent减去被内核预留的页比如内存管理页表、保留页等。你看到managed比present小一截是正常的。接下来配合free命令看例如free -h实际上free -h里的total是managed页的总和不是你物理内存的上限。这就是为什么服务器插了256GB内存free里常常显示只有250GB左右可用的原因。这些“少掉”的内存被内核管理结构吃掉了并不等于内存泄漏。3. 内核是如何给“内存域”做分配与回收决策的了解了zone的静态布局还需要知道内核在运行期如何动态分配和回收内存域里的页。这里有两个核心机制伙伴系统的跨zone分配以及watermark水位唤醒机制。理解它们有助于回答“为什么明明还有空闲内存内存分配却失败”这类经典问题。3.1 伙伴系统如何在zone间分配伙伴系统是每个zone内部的页块分配器。它把空闲页按order分成不同链表order0是单个4KB页order1是2个连续页以此类推。分配时如果找不到刚好大小的块就把大块逐级分裂。回收时如果相邻块都空闲就向上合并。这套机制保证了物理连续页的分配效率。当某个zone的空闲页不够时分配器不会立刻失败而是沿着zonelist的fallback顺序尝试其他zone。这里有个容易被忽略的点不同GFP标志会改变候选zone集合。比如GFP_HIGHUSER_MOVABLE会优先从MOVABLE zone分配因为它希望把用户页集中到可迁移区域便于反碎片。而GFP_KERNEL可能默认优先NORMAL zone。我曾经在一台数据库服务器上遇到过奇怪问题NORMAL zone内存几乎耗尽但MOVABLE zone还有很多空闲。原因是驱动或者内核线程大量使用不可迁移分配而用户态内存又被LRU回收得很慢导致MOVABLE页不能被转移。解决方案不是单纯加内存而是调整vm.min_free_kbytes和vm.zone_reclaim_mode让内核更激进地在zone之间平衡。3.2 watermark与kswapd内存域水位的动态平衡每个zone都有三个水位线pages_minlow通常叫min、pages_low、pages_high分别对应min、low、high。当zone空闲页低于low水位时内核唤醒kswapd内核线程异步回收页如果低于min水位说明很危险分配路径会直接进入直接回收甚至触发OOM killer。具体关系可以看/proc/zoneinfo里的这几行min 6 low 10 high 14 spanned 917504 present 917504 managed 903174这些数值由内核根据zone大小、min_free_kbytes等参数计算。全局参数vm.min_free_kbytes控制的是整个系统保留的最小空闲页量内核会按比例分摊到每个zone。对大多数服务器来说默认值在某些内存压力场景下偏低我建议根据经验调大一点。给出一个简单估算方法如果系统总内存是64GB可以尝试设置vm.min_free_kbytes524288也就是512MB左右。这个值会让每个zone保留下更多空闲页减少突发分配失败代价是可用内存少一点点。具体要在业务低峰期调整并观察一段时间不要照搬。3.3 分配策略参数zone_reclaim_mode和numa_balancingNUMA环境下还有一个经常被忽略的参数/proc/sys/vm/zone_reclaim_mode。它控制当某个NUMA node本地内存不足时内核是否愿意在本地zone里做更激进的回收而不是直接跳到远端node分配。默认值通常是0即允许跨node分配这样对内存带宽不敏感的任务更友好如果设置成1内核会倾向回收本地内存可能增加延迟。numa_balancing则控制自动页迁移。如果开启内核会通过扫描页表发现频繁访问远端内存的页然后迁移到进程所在node。这个功能对某些工作负载能显著降延迟但它也消耗CPU。出现内存domain分配不均衡时我会先看numastatnumastat如果看到local_node和other_node比例严重失衡再决定是否调整numa_balancing。个人经验是高并发Java应用开启numa_balancing通常有帮助而CPU亲和性已经很严格的实时任务则建议关闭避免迁移带来的抖动。4. 实战让每个内存domain真正可控理论说完了下面给出几个我实际在用的管理手段。核心思路是硬件MMU的domain靠内核启动代码管物理zone靠sysctl调NUMA亲和靠numactl管容器资源隔离靠cgroup v2管。逐层控制才能做到心中有数。4.1 用numactl绑定进程的内存域如果你希望某个进程的内存分配固定在特定NUMA node只设CPU亲和是不够的还要用numactl绑定内存。先查看硬件拓扑numactl --hardware输出会列出每个node的CPU范围、内存大小和空闲情况。想将一个进程及其内存都绑定到node 0可以这样启动numactl --cpunodebind0 --membind0 ./your_app--membind0会让进程的页分配优先落在node 0如果node 0不足默认情况下也可以按策略去其他node但会尽量避免。这个选项最适合对本地内存带宽敏感的HPC或数据库实例。如果进程已经运行不想重启可以看它的NUMA策略cat /proc/pid/numa_maps文件里面的N0...表示哪些页分布在哪个node。配合numastat -p pid能快速定位进程是否发生了大量跨节点分配。我排查过一个性能劣化问题现象是数据库从32核旧机迁移到双路新机上后延迟不降反升numa_maps显示进程大量页落在远端node。后来用numactl --interleaveall改为交错分配因为双路CPU的QPI带宽足够交错反而均衡了访问延迟。4.2 用cgroup v2的memory.domain隔离容器内存cgroup v2中的memory控制器是典型的domain型cgroup。要使用它需要先挂载cgroup2文件系统sudo mount -t cgroup2 none /sys/fs/cgroup然后查看根cgroup的cgroup.type通常是domain。创建一个用于限制内存的子组sudo mkdir /sys/fs/cgroup/example echo 1G | sudo tee /sys/fs/cgroup/example/memory.max echo 200M | sudo tee /sys/fs/cgroup/example/memory.swap.max之后把进程写入cgroup.procsecho pid | sudo tee /sys/fs/cgroup/example/cgroup.procs如果进程超过memory.max会发生回收并触发memory.events里的max计数严重时直接OOM kill。生产环境里我建议把memory.max设为业务峰值的1.2倍左右同时保留memory.swap.max为0或小值避免容器被swap拖垮。值得一提的坑在cgroup v2的cgroup.controllers文件里如果你看到memory控制器没有出现在列表说明需要先在父cgroup中启用。可以查看/sys/fs/cgroup/cgroup.subtree_control并加入memoryecho memory | sudo tee /sys/fs/cgroup/cgroup.subtree_control这个操作大多数发行版默认已经做了但如果你的环境是手动挂载的很容易漏掉。4.3 踩坑记录ARM上设置domain错误导致的内存访问异常说到踩坑我有一个记忆深刻的经历。某次在ARM32平台上调试外设驱动板子启动到一半直接挂掉串口输出停在Unhandled fault: domain fault。当时第一反应是页表配置有问题查了半天设备树都没找到原因。后来仔细看内核启动日志发现我们自己加的板级初始化代码里有一段操作CP15协处理器的逻辑里面错误地把一个domain设成了“禁止访问”结果内核态访问该domain下的寄存器区域时立刻触发了异常。这个案例说明ARM的domain管理非常底层一旦配错表现往往是随机crash而不是明确报错。排查时要留意dmesg中的fault类型domain fault和permission fault的处理路径完全不同。如果你遇到的是domain fault优先检查是否有未初始化或错误配置的DACR相关代码。遗憾的是很多SoC厂商的BSP代码里这类操作都封装在汇编里普通驱动开发者基本碰不到但了解了这个机制至少不会被一个domain fault吓到怀疑是业务内存泄漏。5. 内存domain管理的排障与调优经验最后聊一聊实际排障中积累的一些判断方法包括如何快速定位内存压力来自哪个层级以及一个容易被误解的“domain forbidden”错误。5.1 检查内存压力的正确姿势当系统出现卡顿或OOM时很多人第一反应是看top或free但这两个工具只能看到系统级汇总无法定位具体是哪个zone或node在扛压力。更靠谱的路径是cat /proc/pressure/memory这个文件里的some和full指标能反映内存回收造成的任务停顿比例。如果full avg60长期大于0.1说明内存压力已经比较严重。接着看vmstatvmstat -a其中si和so长时间不为0说明内存域正在大量换页。这时要查是谁在消耗内存ps aux --sort-%mem | head cat /proc/meminfo在/proc/meminfo里要留意几个字段Cached是页缓存AnonPages是匿名页Slab是内核对象占用的内存。经常有人看到Cached高就担心内存泄漏其实页缓存可以被内核随时回收在内存压力下会主动释放。真正需要警惕的是Slab中的不可回收部分持续增长可以用slabtop进一步观察。5.2 误把API错误当内存问题的“domain forbidden”乌龙很多人搜索“Linux 内存 domain 管理”时会看到类似{code:1004,error:domain forbidden}这样的报错第一反应觉得是系统内存域权限问题。这里要明确一个误区这种JSON格式的返回通常来自云平台或SDK的业务API比如对象存储、CDN或邮件服务里面的domain指的是业务域名或租户域不是Linux内核的内存domain。我遇到过一名运维同事看到应用日志里出现domain forbidden立刻去调整了服务器内存参数加了vm.min_free_kbytes当然毫无效果。后来查接口文档才发现是调用API时带了错误的域名或租户ID被网关拒绝。所以排查时先看错误来源如果是HTTP响应先别动内核参数。5.3 实用的内存domain调优清单根据我多年压测和调优经验给出一个适合大多数x86服务器的初始参数组合。注意不是所有环境都适用调整前请先备份并理解每个值的含义参数建议值适用场景vm.swappiness10~30一般服务避免过多swapvm.vfs_cache_pressure100~200文件缓存压力大时调高vm.min_free_kbytes内存的0.5%~1%减少突发分配失败vm.dirty_ratio10~30控制写缓存回写节奏/proc/sys/vm/zone_reclaim_mode0或1NUMA敏感场景按需设置以vm.swappiness为例默认60在内存domain紧张时会比较激进地把匿名页换到swap导致进程延迟大增。我一般先改成10观察一周。如果业务是数据库等重内存应用甚至直接设成0。但要小心设成0并不代表禁止swap而是降低内核使用swap的倾向在极端情况下仍然可能换出。另一个容易被忽略的是vm.vfs_cache_pressure。这个参数控制内核回收目录项和inode缓存slab的倾向默认100。如果你发现/proc/meminfo中Slab占用非常高而且业务主要是大量小文件读写可以适当提高到150让内核更早回收这些缓存。反过来如果希望提高文件元数据性能可以降低到50但需要承受更高内存占用。最后说一句个人体会Linux内存domain管理看起来名词混乱但只要抓住“物理页归属于nodenode内分zonezone内按order和迁移类型组织最后还要通过cgroup做资源上限”这条主线大部分问题都能定位到具体层级。真遇到诡异问题先读/proc/zoneinfo和dmesg别凭感觉改参数。系统稳定运行比压榨最后5%内存重要得多。