Linux下CXL与持久内存:cxl/ndctl/daxctl实战指南

发布时间:2026/11/5 2:39:54
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 端口、端点、解码器拓扑cxlcxl list
把若干 memdev 组织成一个 regioncxlcxl create-region -d decoder0.0 -m mem0 -w 1 -g 4096
在 region 里切出 fsdax / devdax 命名空间ndctlndctl create-namespace -r region0 -m fsdax
把 devdax 转成可热插拔的系统内存daxctldaxctl reconfigure-device --mode=system-ram --online dax0.0
查健康状态、触发介质扫描ndctlndctl 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_pmem

cxl_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.0

3.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/pmem

su是单个条带单元大小,sw是条带数量,两者相乘才是完整条带宽度。ext4 的表达方式不同,用-E stride=1,stripe_width=4(单位是块,4KB 块下 stride=1 就代表 4KB)。对齐没做对不会报错,但性能会打折,因为每次下发的 IO 都可能被拆成不对齐的碎片。

挂载之后验证 DAX 是否真的生效,不要只看命令返回值:

mount | grep pmem # 输出里应该带 dax 字样 xfs_info /mnt/pmem | grep -i dax

XFS 的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=nv1

align=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 一起配上,别省这点开销。真到了要排查的那天,你会发现日志里那几行记录比什么都值钱。