Linux下CXL与持久内存:cxl/ndctl/daxctl实战指南
1. 三层对象模型:这套工具到底在管什么
很多人第一次接触这块硬件,都是一脸懵:内存条插上去了,dmidecode里能看到一大块容量,可free -h的数字纹丝不动,lsblk里也找不到新盘。这不是硬件坏了,而是持久内存和 CXL 内存设备在 Linux 里走了一条跟普通 DDR 完全不同的注册路径——它不会自动变成内存页,也不会自动变成块设备,必须由用户态工具显式"开局"。这三个工具就是开局的钥匙:cxl负责跟物理层面的 CXL 设备、端口和解码器打交道,ndctl负责 libnvdimm 子系统里的 region 与 namespace 逻辑划分,daxctl负责把已经划分好的区域重新塑形成字符设备或者热插拔内存。三个工具命令风格一致,源码同源,但职责边界清晰,搞不清谁管谁,就会在"我明明创建了怎么还是看不见"这种问题上耗掉一整天。
这篇内容面向的是需要在裸金属服务器、虚拟化宿主机或者测试环境里真正把这套设备跑起来的人。零基础也能跟,因为我会把对象模型和设备树先讲透;有经验的可以直接跳到第三节的实操流程和第五节的排查表。所有命令和路径都来自实际调试过程,参数取值我会写清楚是怎么算出来的,而不是丢一个数字让你抄。
1.1 从总线到命名空间的父子链路
理解这套体系,最关键的是记住一条单向的父子链:总线(bus)→ 内存设备(dimm / memdev)→ 区域(region)→ 命名空间(namespace)→ 上层设备(/dev/pmemX 或 /dev/daxX.Y)。每一层都是一对多的包含关系,父层不释放,子层建不出来;反过来删的时候必须自下而上,否则内核会直接回你一句设备忙。
以 libnvdimm 为例,内核加载之后会在/sys/bus/nd/devices/下面铺开一整棵设备树。你会看到nvdimm_bus0、nvdimm0、nvdimm1、region0、namespace0.0这样的命名。nvdimm_bus0是根,每个物理设备对应一个nvdimmX,若干个物理设备按照交织规则组成一个regionX,region 内部再切出namespaceX.Y。命名规则里那个点号前面的数字是 region 编号,后面是 region 内部的命名空间序号,namespace0.0和namespace0.1一定属于同一个region0,容量之和不会超过 region 的可用空间。
CXL 设备走的是另一套目录/sys/bus/cxl/devices/,你能看到root0、port1、endpoint2、decoder0.0、mem0这些对象。关键在于:CXL 的 memdev 在被cxl create-region组织成 region 之后,会由cxl_pmem这层桥接驱动翻译成 nvdimm 世界里的 region,于是后面 ndctl 和 daxctl 才能像管理传统持久内存那样管理它。这也是为什么很多教程标题把 CXL 和 NVDIMM 写在一起——对用户态工具来说,它们最终落到了同一个框架里。
注意:内核版本差异在这块特别明显。较新的内核里,CXL 的易失性区域有可能直接挂出一个 dax 设备,不再经过 nvdimm 桥接。所以你在
daxctl list里看到的对象名称前缀可能是dax0.0,也可能带着 region 的路径信息,不要因为名字跟教程不一样就以为环境有问题。
1.2 cxl、ndctl、daxctl 的职责分界
我在带新人的时候习惯用一个比喻:cxl 是规划局,ndctl 是房产中介,daxctl 是装修队。规划局决定这栋楼(region)怎么盖、由哪几块地(memdev)拼起来、几路交织;中介负责把楼划分成一套套房子(namespace)并办好产权证(标签区);装修队决定这套房子最终是当仓库(块设备 + 文件系统)还是当车间(字符设备直通),甚至直接把墙砸了并入客厅(system-ram 热插拔进内存池)。
具体到命令上,判断该用谁,看你要动的对象在哪个目录下就行:
| 目标操作 | 对应工具 | 典型命令 |
|---|---|---|
| 查看 CXL 端口、端点、解码器拓扑 | cxl | cxl list |
| 把若干 memdev 组织成一个 region | cxl | cxl create-region -d decoder0.0 -m mem0 -w 1 -g 4096 |
| 在 region 里切出 fsdax / devdax 命名空间 | ndctl | ndctl create-namespace -r region0 -m fsdax |
| 把 devdax 转成可热插拔的系统内存 | daxctl | daxctl reconfigure-device --mode=system-ram --online dax0.0 |
| 查健康状态、触发介质扫描 | ndctl | ndctl list -H、ndctl start-scrub |
一个常见误区是以为cxl create-region之后就能直接mkfs。实际上 region 只是"圈地",真正对上层暴露设备节点的是 namespace。CXL 的 region 建立后,如果类型是 pmem,内核会自动在其上生成一个 nvdimm region,但里面还是空的,available_size等于全部容量,需要再用 ndctl 切一刀。这一步漏掉,就会出现"region 明明建好了,/dev/pmem0死活不出现"的情况。
1.3 四种使用形态的本质区别
同一块硬件,落地形态不同,性能特征和适用场景差别巨大。这也是我在选型时最愿意花时间跟人解释的部分,因为选错了后面基本要推倒重来。
| 形态 | 设备节点 | 是否走页缓存 | 断电保持 | 典型用途 |
|---|---|---|---|---|
| raw 命名空间 | 无块设备 | 不适用 | 是 | 应用自己mmap裸设备,完全自管 |
| fsdax 命名空间 | /dev/pmem0 | 可绕过(DAX 挂载) | 是 | 需要文件语义又要低延迟的数据库、日志 |
| devdax 命名空间 | /dev/dax0.0 | 完全绕过 | 是 | 虚拟机内存后端、用户态直接映射 |
| system-ram | 无独立节点,并入内存池 | 走正常内存路径 | 否(易失) | 扩容内存、内存分层、临时大页池 |
raw 模式现在很少有人主动用了,因为它没有任何保护,写坏了自己负责;sector 模式(BTT)更是历史包袱,除非你要兼容十年前的驱动,否则直接跳过。真正要做决策的是 fsdax 和 devdax 二选一,以及要不要干脆转成 system-ram。
fsdax 的优势是能用文件系统的一切能力:权限、快照、配额、reflink,而且 DAX 挂载之后读写直接落到介质,页缓存被绕过。代价是文件系统的元数据操作仍然有开销,并且对齐没做好会出现"部分访问退化到 memcpy"的情况。devdax 则是把整块区域当成一个巨大的字符设备交出去,没有任何文件系统层,适合 QEMU 这类需要mmap一块固定物理地址的场景。system-ram 严格来说是"放弃持久性换容量",把这块介质当成普通 DRAM 用,方便做内存分层实验。
2. 准备阶段:内核能力确认与设备可见性
动手之前必须先确认内核认不认这块硬件。我踩过最冤的一次坑,是在一台预生产机器上折腾了两个小时ndctl list返回空,最后发现内核编译时压根没开 CXL 相关选项,硬件在lspci里躺着但驱动没绑上。所以这一节虽然是准备工作,但值得单独拿出来讲,因为排查顺序错了后面全是无用功。
2.1 内核配置与模块依赖
先看配置。持久内存相关的核心选项是CONFIG_NVDIMM、CONFIG_BLK_DEV_PMEM、CONFIG_FS_DAX、CONFIG_DEV_DAX;CXL 相关的是CONFIG_CXL_BUS、CONFIG_CXL_PCI、CONFIG_CXL_ACPI、CONFIG_CXL_PORT、CONFIG_CXL_REGION,以及最关键的桥接项CONFIG_CXL_PMEM。少了最后这个,CXL 设备能识别、region 能建,但永远不会出现在 nvdimm 世界里,ndctl 也就什么都看不到。
# 一次性检查依赖是否到位 grep -E "CONFIG_(NVDIMM|BLK_DEV_PMEM|FS_DAX|DEV_DAX|CXL_)" /boot/config-$(uname -r)如果发现关键项是=m,那就确认模块是否加载:
lsmod | grep -E "nvdimm|dax|cxl" modprobe cxl_pci modprobe cxl_acpi modprobe cxl_pmem modprobe nd_pmem modprobe dax_pmemcxl_acpi负责从 ACPI 表里解析出 CXL 拓扑,cxl_pci负责绑定 PCI 设备,cxl_pmem是桥接层。加载顺序有讲究:如果cxl_acpi先起,它会去枚举端口;如果硬件侧拓扑信息不完整,可能会在dmesg里刷一堆错误但设备照样能用。所以模块加载完之后,一定要回头看一眼dmesg:
dmesg | grep -iE "cxl|nvdimm|dax" | tail -40我习惯关注三类信息:一是有没有 "decoder" 相关的枚举结果,二是 memdev 的容量是否被正确读出,三是有没有出现 "failed to" 这种字样。哪怕设备最后能用,这些错误往往暗示着某些特性(比如交织、标签区)不可用。
2.2 用三个 list 命令摸清家底
装好工具包之后,第一件事不是创建,而是把所有对象列一遍,脑子里先建立起这台机器上的拓扑图。三个工具的 list 子命令输出风格高度一致,-u参数把容量转成人类可读单位,建议养成习惯直接带上。
cxl list -u # 看端口、端点、解码器、memdev 全拓扑 ndctl list -b nvdimm_bus0 -u # 看 nvdimm 世界的 dimm/region/namespace daxctl list -u # 看已经存在的 dax 设备cxl list的输出是一个带层级缩进的 JSON 结构,里面memdevs数组是最值得关注的,每个条目包含size、ram_size、pmem_size、volatile_only这些字段。如果size显示为零或者缺字段,说明固件侧信息没上报完整,这时候去建 region 大概率会失败。某些版本里 memdev 和 media-ready 设备的过滤选项大小写有区别,拿不准就直接看cxl list --help,两秒钟的事,比试错快。
ndctl list我更常用的是分项查看。默认输出会一次性把 dimm、region、namespace 都列出来,层级深的时候眼睛容易花。可以加参数只列某一类,比如只看 region 的时候我会关注available_size,那个数字决定了还能切出多少空间;只看 namespace 的时候关注mode和size,能立刻判断当前是什么形态。
提示:如果
ndctl list返回的是空数组[],但cxl list明明有 memdev,八成是 region 还没建,或者建了但类型是易失性的、没走 pmem 桥接。这时候顺序是先查cxl list里有没有 region 对象,再查 region 的type。
2.3 标签区初始化与容量核算
传统持久内存有条关键约束:命名空间的划分信息存在介质上的标签区(label area)里,如果标签区没初始化,ndctl 会拒绝创建命名空间。CXL 设备情况略有不同——走 pmem 桥接的场景下同样需要标签支持,而某些易失性配置则完全不需要。判断方法很简单,直接cat一下 sysfs:
cat /sys/bus/nd/devices/region0/available_size cat /sys/bus/nd/devices/nvdimm0/size如果 region 的available_size明显小于 dimm 的物理容量,而且差值大到不像元数据开销,那就要怀疑标签区没初始化或者配置不对。对传统 persistent memory 设备,初始化标签区用的命令是:
ndctl init-labels nvdimm0 # 会清空现有分区信息,务必确认空盘 ndctl check-labels nvdimm0 # 校验,输出里能看到 signature 和 free space这里必须强调风险:init-labels是破坏性操作,只要这个 dimm 上还有你需要的命名空间,跑下去就全没了。我在实验室里见过有人把它当成"刷新一下"随手敲,结果把跑了两周的测试数据抹了。养成习惯,动手前先ndctl list存档一份输出,必要时还能照着重建。
容量核算这块,行业里有个粗略的经验值:标签区占用大约是每 dimm 128KB 量级,加上 region 元数据和对齐损耗,最终可用容量通常比标称容量少几个百分点。多路交织的时候损耗比例会略高,因为要按 granularity 对齐。所以当你在需求评审时被问"这块 512G 的卡能给我多少可用空间",回答 480G 到 500G 之间比较稳妥,别拍脑袋说满容量。
3. 三种落地形态的完整实操
前面讲的都是认知层面,这一节开始动手。我把常见需求归成三类:想把设备当成内存池的一部分、想挂文件系统、想直通给虚拟机或用户态程序。三条路径从同一个起点出发,但中间命令差别很大,建议先在一个干净环境里把三条都跑一遍,建立肌肉记忆。
3.1 形态一:整块切成易失性扩展内存
这是最近两年最热的一种用法,尤其是把 CXL 内存扩展卡挂上去当"廉价大内存"的场景。核心思路是让这块介质被内核识别成可热插拔的内存,然后上线到某个 NUMA 节点,free -h里立刻就能看到数字变大。
第一步,确认 CXL 设备侧的 region 是否已经建好。如果是整卡直通、不需要交织,命令很简洁:
cxl list -u # 确认 mem0 存在且容量正确 cxl create-region -d decoder0.0 -m mem0 -w 1 -g 4096参数含义:-d decoder0.0指定用哪个根解码器,-m mem0指定参与的内存设备,-w 1表示一路交织(不交织),-g 4096是交织粒度,单位字节。单设备场景-g其实影响不大,但习惯性写上,免得后面加设备时忘了改。
region 建好之后,daxctl list应该能看到对应的 dax 设备。接着把它转成系统内存:
daxctl reconfigure-device --mode=system-ram --online dax0.0执行完这一步,内核会把这块区域按 section 大小(通常是 128MB)切成内存块,逐个上线。上线目标 NUMA 节点由固件或者设备在 sysfs 里的属性决定,可以用numactl -H验证分布是否符合预期。我在一台双路机器上做过对比,同一张卡挂在不同的 PCIe 根下面,最终归属的 NUMA 节点确实不一样,跨节点访问的延迟差异能到 40% 以上,所以高并发场景下这个细节不能忽略。
注意:
--online参数会让内存立即上线,某些版本还支持--movable把这块内存放进可迁移区,方便后续做内存规整。如果你打算用它跑大页,反而不建议加 movable,因为可迁移区里拿不到稳定的连续物理页。
回滚也很简单,先下线再切回去:
daxctl offline-memory dax0.0 daxctl reconfigure-device --mode=devdax dax0.03.2 形态二:fsdax 命名空间跑文件系统
需要持久化又要文件语义的场景,走这条。前提是 region 已经存在并且有足够的available_size。
ndctl create-namespace -r region0 -m fsdax -s 200G -n namespace0.0-m fsdax指定模式,-s指定大小(不写就吃满),-n指定名字方便后续引用。创建成功后/dev/pmem0出现,但它还不是文件系统,只是裸设备。
格式化的时候有个关键细节,很多人忽略:文件系统的条带参数要和交织配置对齐。假设 region 是 4 路交织、粒度 4096 字节,那么逻辑上每次写入会横跨 4 个设备、每个设备 4KB,等效条带宽度是 16KB。XFS 的写法是:
mkfs.xfs -f -d su=4096,sw=4 /dev/pmem0 mount -o dax=always /dev/pmem0 /mnt/pmemsu是单个条带单元大小,sw是条带数量,两者相乘才是完整条带宽度。ext4 的表达方式不同,用-E stride=1,stripe_width=4(单位是块,4KB 块下 stride=1 就代表 4KB)。对齐没做对不会报错,但性能会打折,因为每次下发的 IO 都可能被拆成不对齐的碎片。
挂载之后验证 DAX 是否真的生效,不要只看命令返回值:
mount | grep pmem # 输出里应该带 dax 字样 xfs_info /mnt/pmem | grep -i daxXFS 的dax=always在新版本里有被dax=inode取代的趋势,后者只对设置了 DAX 属性的文件生效,更精细。生产环境我一般用dax=inode配合xfs_io -c "chattr +x"逐个标记,避免全盘 DAX 带来的元数据开销。
3.3 形态三:devdax 字符设备直通
devdax 是三种形态里最"硬核"的一种,直接把介质映射成字符设备,中间没有任何文件系统或缓存层。创建方式跟 fsdax 几乎一样,只是模式不同:
ndctl create-namespace -r region0 -m devdax -s 64G -n namespace0.1生成的是/dev/dax0.1这样一个节点。注意编号规则:dax0.1里的0通常对应 region 编号,1是 region 内的序号,不要跟namespace0.1的编号混淆,虽然大多数情况下它们是对应的。
典型用法之一是给虚拟机当内存后端。QEMU 侧的写法大致是这样:
-object memory-backend-file,id=mem1,mem-path=/dev/dax0.1,size=64G,align=2M,share=on \ -device nvdimm,memdev=mem1,id=nv1align=2M这个参数不能省,因为大页后端要求对齐,不写会退化成 4K 对齐,性能损失非常明显。我在一次压测里就是因为漏了这个参数,虚拟机的内存带宽只有预期的一半,查了半天才发现。
另一种用法是用户态程序直接mmap这个字符设备,配合MAP_SHARED做零拷贝的数据结构。这种用法常见于高频交易和实时分析场景,优势是完全没有内核路径开销,缺点是程序要自己处理持久化语义(什么时候真正落盘、掉电一致性怎么保证),门槛不低。
注意:devdax 设备不能被两个进程以不同的映射方式同时使用。如果一个进程已经
mmap了它,你试图在上面执行ndctl destroy-namespace,内核会返回设备忙。处理办法是先确认占用进程,必要时重启相关服务,别急着加-f。
3.4 参数怎么定:interleave、granularity、align
这三个参数是新手最容易蒙圈的地方,我按实际决策顺序讲一遍。
交织路数(ways)决定数据分散到几个物理设备上。一路就是不交织,简单但带宽受限于单设备;多路能线性提升带宽,代价是任意一个设备故障就整体降级,而且所有参与设备的容量必须一致(按最小的算)。实际选型时我一般看两点:如果只有一张卡,那没得选;如果有多张,且业务是带宽敏感的(比如列存扫描、大规模矩阵运算),4 路以内收益明显,8 路以上边际收益递减而故障域变大。
交织粒度(granularity)是每个设备上一次连续写入的长度。太小(比如 256 字节)会让每次访问都被拆得很碎,地址翻译开销上升;太大(比如 1MB)则在小 IO 场景下浪费带宽,因为一次读只要 4KB 但实际要从一个设备拉 1MB。业界常见的取值是 4KB 和 2MB,前者通用,后者配合大页和顺序访问效果好。我自己的经验是:不确定就选 4096,除非你明确知道业务是大块顺序访问。
对齐(align)主要出现在 dax 设备给虚拟机或者做内存热插拔的时候。原则很简单,上层要什么对齐就给什么对齐:QEMU 用大页就设 2M,用户态程序用 4K 页就设 4K。sysfs 里能看到当前设备的align属性,改之前先看一眼现状。
cat /sys/bus/dax/devices/dax0.1/align cat /sys/bus/dax/devices/dax0.1/size顺便说一句,capacity 核算时要把对齐损耗算进去。比如你要给虚拟机 64G,但对齐要求 2M,那实际分配应该向上取整到 2M 的整数倍,否则创建时会直接报错。
4. 回收与重建:顺序错了会卡住
创建流程顺风顺水的时候谁都不会去看拆除流程,等到要改配置了才发现删不掉。这一节讲的顺序问题,我在三个不同的环境里都遇到过,值得单独拎出来。
4.1 正确的拆除顺序
原则只有一条:自下而上,从最上层的使用方开始拆。完整顺序是:先卸载文件系统或者停掉占用进程,然后ndctl destroy-namespace删命名空间,再处理 region,最后才是 dimm 或者 memdev。
umount /mnt/pmem ndctl destroy-namespace namespace0.0 -f cxl destroy-region -r region0 -f-f是强制标志,但它的作用是"跳过交互确认",不是"忽略依赖关系"。如果 namespace 还被挂载着,加不加-f都删不掉,内核会在 sysfs 层面拒绝。所以正确姿势是先umount,再删。
有一种情况特别容易踩:namespace 删了,但 region 的available_size没有立刻恢复。这不是 bug,是异步回收,通常几秒钟内会刷新。如果等了半分钟还没变,去看dmesg,往往能看到内核在等待某个引用计数归零,这时候lsof或者fuser扫一下设备节点,基本都能找到元凶。
4.2 被占用时的处理办法
占用来源大概分三类。第一类是文件系统挂载,用findmnt一查就清楚。第二类是进程直接打开的字符设备,lsof /dev/dax0.1能看到持有者。第三类最隐蔽,是虚拟机进程持有内存后端文件,这种情况下lsof也能看到,但你可能不敢直接 kill,那就得走虚拟机的正常关机流程。
还有个特殊情况是内核自己占着,比如某个 dax 设备刚被reconfigure-device改成 system-ram 并上线了内存,这时候它已经不归 daxctl 管了,得先offline-memory再切回 devdax,才能继续后续的删除动作。这条路径我在第一次做内存分层实验时完全没想到,卡了将近半小时。
提示:删除任何对象之前,先跑一遍
ndctl list -u和daxctl list -u存档,把当前的拓扑和容量记下来。重建的时候照着这份记录走,比凭记忆靠谱得多,尤其是涉及多路交织的时候。
5. 常见故障排查实录
这一节全部来自实际环境里的问题,我按现象分类,每个都给出排查路径。有些问题看着吓人,其实一行命令就能定位。
5.1 容量对不上
最常见的现象是:卡标称 512G,ndctl list里 dimm 容量对得上,但 region 的available_size只有 400 多 G,或者干脆是 0。
先分清是哪种"对不上"。如果是available_size小于物理容量但非零,通常是标签区占用加对齐损耗,属于正常。如果直接是 0,那说明 region 里已经有 namespace 吃满了,ndctl list -n看一眼就明白。还有一种情况是 dimm 容量本身就偏小,这往往是固件上报问题,或者交织配置导致只能按最小设备算容量。
排查顺序建议这样:先cat /sys/bus/nd/devices/regionX/available_size拿准确数字,再ndctl list -n看已分配,最后对比 dimm 的size和physical_size(有些设备这两个值不一样,差值就是元数据开销)。
5.2 region 创建失败
cxl create-region报错的种类不少,我遇到过的按频率排序:
| 报错现象 | 大概率原因 | 处理思路 |
|---|---|---|
no decoder found | 根解码器未使能或拓扑没枚举完 | 查cxl list里 decoder 状态,必要时重新加载 cxl_acpi |
failed to reserve decode range | 目标地址区间已被别的 region 占用 | 先销毁冲突 region,或换一个 decoder |
memdev size mismatch | 参与交织的 memdev 容量不一致 | 只选容量相同的设备,或减少交织路数 |
unsupported granularity | 粒度不是设备支持的取值 | 尝试 256 / 4096 / 16384,看设备能力位图 |
这里我想强调一下第二行那个错误。地址区间冲突在多卡环境下非常容易出现,因为每个根解码器下面有一段可分配窗口,前一个 region 没删干净,后一个就分不到。处理办法不是硬来,而是把现有 region 全列出来,看清楚各自的地址范围,规划好再建。
5.3 dax 模式切换失败
daxctl reconfigure-device失败,十有八九是两个原因。一个是设备正被占用,前面讲过。另一个是目标模式跟当前 region 的能力不匹配,比如 region 是易失性类型,你想切回 devdax 保留持久性,那内核会拒绝,因为物理上这块介质就不是按持久化设计的。
还有一种情况是切换成功了,但内存没上线,free -h没变化。这时候别急着怀疑工具,去看dmesg里有没有内存热插拔相关的日志,常见的是目标 NUMA 节点不存在,或者 section 对齐不满足要求。我遇到过一次是因为设备容量不是 128MB 的整数倍,最后一点零头无法上线,属于正常现象。
5.4 排查速查表
把上面这些浓缩成一张表,出问题时按顺序过一遍,绝大多数情况五分钟内能定位。
| 现象 | 第一步 | 第二步 | 第三步 |
|---|---|---|---|
| 三个工具都看不到设备 | lspci确认硬件在位 | lsmod查 cxl_pci 是否加载 | dmesg看绑定失败原因 |
| cxl 看得到、ndctl 看不到 | cxl list查 region 是否创建 | 查 region 的 type 是否为 pmem | 确认 cxl_pmem 模块已加载 |
| namespace 建不出来 | 查available_size | 查标签区是否初始化 | 查模式是否与 region 兼容 |
/dev/daxX.Y不出现 | daxctl list是否可见 | 查内核是否开了 DEV_DAX | 查设备是否被标记为 idle |
| 挂载后性能不达预期 | 查条带参数是否对齐 | 查 DAX 是否真正生效 | 查 NUMA 归属是否跨节点 |
| 删除时报设备忙 | findmnt查挂载 | lsof查占用进程 | 查是否有内存已上线未下线 |
6. 长期运维:监控、健康与错误处理
设备跑起来只是开始,真正考验人的是几个月后的稳定性问题。持久内存和 CXL 内存有个特点:介质错误不会立刻让系统崩溃,而是悄悄累积,等到某个阈值才爆发。所以监控和定期扫描是必须的,不能等出事再查。
6.1 ndctl monitor 常驻
ndctl monitor是一个事件监听守护进程,会把内核上报的健康事件、介质错误、寿命告警转成可读日志,也支持输出成 JSON 交给日志系统。启动方式很轻量:
ndctl monitor --log=/var/log/ndctl-monitor.log --daemon它监听的事件类型包括 dimm 健康状态变化、介质错误、命名空间生命周期变化等。我在测试环境里配了一个简单的告警规则:只要出现介质错误事件就立刻查,因为持久内存的介质错误不像 DRAM 有 ECC 兜底,一旦某个地址反复报错,很可能意味着这块区域要标记为坏块。
健康信息也可以按需拉取:
ndctl list -H -d输出里会包含health字段,里面有temperature、life_used、media_errors这些数值。life_used表示寿命消耗百分比,不同厂商标定方式不一样,但只要它开始持续上涨,就说明写入量已经到了需要关注的级别。
6.2 scrub 与错误注入
scrub 是介质扫描,把整块区域读一遍,发现并纠正可纠正错误,同时把不可纠正的地址标记出来。定期跑一次能提前发现问题区域。
ndctl start-scrub ndctl wait-scrub第二条命令会阻塞直到扫描完成,容量大的设备可能要跑几分钟到几十分钟,建议放在维护窗口里做。扫描进度可以在 sysfs 里看到,路径大致是/sys/bus/nd/devices/nvdimmX/scrub,读出来是当前扫描到的地址。
错误注入是另一个方向的工具,用于验证你的监控和容错逻辑是否真的有效。ndctl inject-error可以在指定地址上人为造一个错误,看上层应用和文件系统怎么反应。这个功能在测试环境里非常有用,我在做一个高可用方案验证时,就是靠它把各种故障路径都跑了一遍。但绝对不要在生产环境用,一个不小心的地址参数就可能把正常数据毁掉。
# 仅限测试环境,先确认地址范围 ndctl inject-error namespace0.0 --block 0 --count 1 ndctl clear-errors namespace0.0跑完之后记得清理,clear-errors会把注入的错误标记清掉,避免影响后续的扫描结果。
我个人在实际运维中的体会是,这套设备的运维成本主要不在日常监控,而在"出事时的判断"。普通内存坏了,系统会报 machine check,日志明明白白;持久内存出了介质问题,表现可能是某个文件读出来校验失败,也可能是应用莫名其妙返回错误码,链路更长,定位更绕。所以我的建议是,只要上了这类硬件,就把ndctl monitor和定期 scrub 一起配上,别省这点开销。真到了要排查的那天,你会发现日志里那几行记录比什么都值钱。