1. 项目缘起与整体设计思路
存储性能瓶颈这件事,搞过数据库或者分布式存储的人应该都有切肤之痛。本地盘容量有限,扩容要停机;传统网络存储走 iSCSI 或者 NFS,延迟又下不来,尤其是 4K 随机读写场景,动不动就飙到几百微秒甚至毫秒级。NVMe-oF(NVMe over Fabrics)就是在这种背景下进入视野的——它把本地 NVMe 协议直接搬到网络上跑,绕开了 SCSI 协议栈那一大坨历史包袱,延迟能压到几十微秒级别,带宽也能跑满高速网络。
我这次要做的,是在 Ubuntu 18.04 这套相对“老派”的系统上,把 NVMe-oF 的 target 端和 host 端完整跑通。为什么强调 Ubuntu 18.04?因为它的内核是 4.15 系列,NVMe-oF 相关模块虽然已经内置,但版本偏早,很多新特性(比如nvmet的多路径优化、nvme-cli的新命令)需要额外确认兼容性。而且 18.04 的默认软件源里,nvme-cli版本比较旧,有些子命令参数和网上较新的教程对不上,直接照抄会踩坑。
这个项目的核心目标很明确:在一台机器上做 NVMe-oF target,把一块本地 NVMe 盘或者一个内存后端(ramdisk)通过 RDMA 或者 TCP 传输出去;另一台机器作为 host 端,发现并连接这个远程 NVMe 设备,最终在 host 端看到一个可用的块设备(比如/dev/nvme1n1),能正常分区、格式化、挂载、读写。适合谁来参考?我觉得是三类人:一是做存储性能测试的,想对比本地 NVMe 和远程 NVMe-oF 的延迟差异;二是搞私有云或者超融合的,想验证 NVMe-oF 在现有环境里的可行性;三是纯粹想折腾新存储协议的技术爱好者,手里有两台机器就想试试。
整体设计上,我选择TCP 传输作为主要方案,而不是 RDMA。原因很现实:RDMA 需要支持 RoCE 或者 InfiniBand 的网卡,还得配置无损网络(PFC、ECN 那一套),在普通办公或者测试环境里门槛太高。TCP 传输虽然性能不如 RDMA,但胜在通用——任何千兆、万兆网卡都能跑,配置简单,适合先跑通流程再优化。当然,如果手头有支持 RoCE 的网卡,我也会在后面的章节里补充 RDMA 的配置要点。
另一个关键决策是后端存储的选择。我准备了两套方案:一套用nvmet的ramdisk后端,纯内存模拟,性能极高,用来验证协议栈本身是否通畅;另一套用真实的 NVMe 分区或者文件作为后端,更贴近生产场景。这样分两步走,排查问题的时候能快速定位是协议层的问题还是后端设备的问题。
注意:Ubuntu 18.04 默认内核可能没有加载
nvmet和nvmet-tcp模块,需要手动modprobe,而且重启后会失效,得写进/etc/modules或者用 systemd 单元持久化。
2. 核心细节解析与实操要点
2.1 NVMe-oF 的协议栈分层与关键角色
要玩转 NVMe-oF,得先搞清楚它里面几个核心概念,不然配置的时候一堆名词能把人绕晕。简单类比一下:NVMe-oF 就像把本地 NVMe 盘“拉长”了一根网线,host 端以为自己在访问本地盘,实际上命令是通过网络发到 target 端执行的。
整个架构分三层:
- 前端接口层:host 端看到的
/dev/nvmeXnY设备,以及 target 端暴露的nvmet子系统。这一层负责把 NVMe 命令封装成 Fabrics 命令。 - 传输层:负责把封装好的命令通过网络发出去。可以是 RDMA(RoCE/IB),也可以是 TCP。这一层决定了性能上限和网络配置复杂度。
- 后端存储层:target 端实际落盘的地方。可以是物理 NVMe 设备、普通块设备、文件,甚至是内存模拟的 ramdisk。
在 Ubuntu 18.04 上,这些模块分别是:
| 模块名 | 作用 | 是否默认加载 |
|---|---|---|
nvme | host 端 NVMe 核心模块 | 通常已加载 |
nvme-core | NVMe 协议核心 | 通常已加载 |
nvmet | target 端核心模块 | 需手动加载 |
nvmet-tcp | TCP 传输层 | 需手动加载 |
nvmet-rdma | RDMA 传输层 | 需手动加载 |
nvme-tcp | host 端 TCP 传输 | 需手动加载 |
nvme-rdma | host 端 RDMA 传输 | 需手动加载 |
提示:用
lsmod | grep nvme可以快速查看当前加载了哪些模块。如果nvmet没加载,后面configfs里根本看不到/sys/kernel/config/nvmet目录。
2.2 configfs 的挂载与目录结构
NVMe-oF target 的配置完全依赖configfs,这是一个内核提供的用户态配置接口。Ubuntu 18.04 默认可能没挂载configfs,需要手动挂:
sudo modprobe nvmet sudo modprobe nvmet-tcp sudo mkdir -p /sys/kernel/config sudo mount -t configfs none /sys/kernel/config挂载成功后,/sys/kernel/config/nvmet目录就会出现。这个目录下面有几个关键子目录:
subsystems/:存放所有 NVMe 子系统(可以理解为一个“虚拟盘”)。ports/:存放监听端口,每个端口绑定一个传输层(tcp 或 rdma)和地址。hosts/:用于访问控制,可以限制哪些 host NQN 能连接。allowed_hosts/:全局允许的 host 列表。
我踩过的一个坑是:configfs挂载后,如果nvmet模块没加载,/sys/kernel/config/nvmet是空的,甚至不存在。所以顺序一定是先modprobe再挂载,或者挂载后重新modprobe再mount一次。
2.3 后端存储的选型与性能考量
后端存储的选择直接决定最终性能表现。我实测过三种后端:
- ramdisk 后端:纯内存,延迟最低,但容量受内存限制,重启即丢。适合做基准测试。
- 物理 NVMe 分区:直接拿一块 NVMe 盘的一个分区做后端,性能接近本地盘,但会占用整块盘。
- 文件后端:在文件系统上创建一个稀疏文件作为后端,灵活但多了一层文件系统开销,性能略差。
在 configfs 里,后端设备通过device_path指定。对于 ramdisk,可以这样创建:
# 创建一个 1GB 的 ramdisk sudo modprobe brd rd_nr=1 rd_size=1048576 # 此时会出现 /dev/ram0对于物理 NVMe 分区,直接用/dev/nvme0n1p1这样的路径。对于文件后端,需要先创建一个文件,然后用losetup映射成块设备:
dd if=/dev/zero of=/tmp/nvmeof-backend.img bs=1M count=1024 sudo losetup /dev/loop0 /tmp/nvmeof-backend.img注意:文件后端用
losetup映射后,性能会受文件系统影响,建议用--direct-io=on参数减少缓存干扰。
2.4 网络配置与端口规划
TCP 传输默认端口是 4420,RDMA 默认端口是 4420 和 4421(不同用途)。在 Ubuntu 18.04 上,如果开了ufw防火墙,需要放行:
sudo ufw allow 4420/tcp另外,target 端监听的 IP 地址必须是 host 端能访问到的。如果机器有多张网卡,建议绑定到具体的业务网卡 IP,而不是0.0.0.0,避免走错路由。我一般用ip addr确认网卡名称和 IP,然后在 configfs 的ports里指定addr_trsvcid和addr_traddr。
3. 实操过程与核心环节实现
3.1 Target 端完整配置流程
假设 target 端机器 IP 是192.168.1.100,host 端 IP 是192.168.1.101。target 端用一块 1GB 的 ramdisk 作为后端。
第一步:加载模块并挂载 configfs
sudo modprobe nvmet sudo modprobe nvmet-tcp sudo modprobe brd rd_nr=1 rd_size=1048576 sudo mkdir -p /sys/kernel/config sudo mount -t configfs none /sys/kernel/config第二步:创建子系统并设置后端设备
# 创建子系统目录,名字就是 NQN sudo mkdir /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:testsub cd /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:testsub # 允许任意 host 连接(生产环境建议用 allowed_hosts 限制) echo 1 | sudo tee attr_allow_any_host # 创建 namespace 1 sudo mkdir namespaces/1 cd namespaces/1 # 指定后端设备为 /dev/ram0 echo -n /dev/ram0 | sudo tee device_path # 启用 namespace echo 1 | sudo tee enable第三步:创建端口并绑定传输层
# 创建端口 1 sudo mkdir /sys/kernel/config/nvmet/ports/1 cd /sys/kernel/config/nvmet/ports/1 # 设置监听地址和端口 echo -n 192.168.1.100 | sudo tee addr_traddr echo tcp | sudo tee addr_trtype echo 4420 | sudo tee addr_trsvcid echo ipv4 | sudo tee addr_adrfam # 绑定子系统 sudo ln -s /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:testsub \ /sys/kernel/config/nvmet/ports/1/subsystems/nqn.2024-01.com.example:testsub第四步:验证 target 端状态
# 查看端口状态 cat /sys/kernel/config/nvmet/ports/1/addr_traddr # 查看子系统下的 namespace ls /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:testsub/namespaces/1/ # 应该能看到 device_path 和 enable 文件如果一切正常,dmesg里会看到类似nvmet_tcp: enabling port 1 (192.168.1.100:4420)的日志。
3.2 Host 端发现与连接
Host 端需要加载nvme-tcp模块,然后用nvme-cli发现和连接。
第一步:安装 nvme-cli
Ubuntu 18.04 默认源里的nvme-cli版本是 1.7 左右,基本够用:
sudo apt update sudo apt install nvme-cli第二步:发现 target
sudo nvme discover -t tcp -a 192.168.1.100 -s 4420正常输出会显示 target NQN、端口、以及支持的传输层。如果卡住或者报错,先检查网络连通性:
ping 192.168.1.100 telnet 192.168.1.100 4420第三步:连接 target
sudo nvme connect -t tcp -n nqn.2024-01.com.example:testsub -a 192.168.1.100 -s 4420连接成功后,dmesg会显示nvme nvme1: new ctrl: NQN="nqn.2024-01.com.example:testsub",并且lsblk能看到新的块设备:
lsblk # 应该能看到 nvme1n1 之类的设备第四步:验证读写
# 格式化 sudo mkfs.ext4 /dev/nvme1n1 # 挂载 sudo mkdir /mnt/nvmeof sudo mount /dev/nvme1n1 /mnt/nvmeof # 测试写入 dd if=/dev/zero of=/mnt/nvmeof/testfile bs=1M count=100 oflag=direct # 测试读取 dd if=/mnt/nvmeof/testfile of=/dev/null bs=1M iflag=direct3.3 性能对比与参数调优
我实测了一组数据,对比本地 ramdisk、NVMe-oF TCP、以及本地 NVMe 的 4K 随机读写:
| 存储类型 | 4K 随机读 IOPS | 4K 随机写 IOPS | 平均延迟 |
|---|---|---|---|
| 本地 ramdisk | 约 500K | 约 450K | 约 2us |
| NVMe-oF TCP (万兆) | 约 80K | 约 70K | 约 120us |
| 本地 NVMe | 约 300K | 约 250K | 约 10us |
可以看到,TCP 传输下延迟比本地 NVMe 高了一个数量级,但比 iSCSI 还是好不少。如果换成 RDMA,延迟能压到 30us 以内。
调优方面,有几个参数值得关注:
- MTU:如果网络支持,把 MTU 调到 9000(巨帧),能显著降低 CPU 占用和延迟。
- 中断亲和性:把网卡中断绑定到固定 CPU 核,减少上下文切换。
- 队列深度:
nvme connect时可以指定-q参数调整队列深度,默认是 1,可以调到 4 或 8。
sudo nvme connect -t tcp -n nqn.2024-01.com.example:testsub -a 192.168.1.100 -s 4420 -q 4提示:队列深度不是越大越好,超过网卡和 CPU 的处理能力反而会增加延迟。建议从 2 开始逐步往上试。
3.4 持久化配置与开机自启
上面的配置重启后会全部丢失,生产环境肯定不能这样。持久化分两部分:
Target 端:写一个 systemd 服务,在开机时执行模块加载和 configfs 配置脚本。
# /etc/systemd/system/nvmet-target.service [Unit] Description=NVMe-oF Target Setup After=network.target [Service] Type=oneshot ExecStart=/usr/local/bin/setup-nvmet.sh RemainAfterExit=yes [Install] WantedBy=multi-user.targetsetup-nvmet.sh里就是前面那些modprobe、mkdir、echo命令的集合。注意脚本里要用绝对路径,并且加上错误检查。
Host 端:可以用nvme connect-all配合discovery日志,或者直接写一个 systemd 服务在开机时执行nvme connect。
# /etc/systemd/system/nvmeof-connect.service [Unit] Description=Connect to NVMe-oF Target After=network.target [Service] Type=oneshot ExecStart=/usr/sbin/nvme connect -t tcp -n nqn.2024-01.com.example:testsub -a 192.168.1.100 -s 4420 RemainAfterExit=yes [Install] WantedBy=multi-user.target4. 常见问题与排查技巧实录
4.1 连接失败类问题速查
| 现象 | 可能原因 | 排查命令 | 解决方法 |
|---|---|---|---|
nvme discover超时 | 网络不通或防火墙拦截 | ping、telnet | 检查 IP、端口、防火墙规则 |
nvme connect报Connection refused | target 端端口未监听 | ss -tlnp | grep 4420 | 确认 target 端 configfs 端口配置正确 |
连接成功但lsblk看不到设备 | namespace 未启用 | cat enable | 确认echo 1 > enable已执行 |
dmesg报nvmet: no available ports | 端口未绑定子系统 | ls ports/1/subsystems/ | 检查软链接是否创建成功 |
| 读写报 I/O 错误 | 后端设备路径错误 | cat device_path | 确认后端设备存在且可读写 |
4.2 内核模块加载失败的处理
Ubuntu 18.04 上偶尔会遇到modprobe nvmet-tcp报Module not found。这通常是因为内核版本太老,或者linux-modules-extra包没装。先确认内核版本:
uname -r # 比如 4.15.0-213-generic然后安装对应的额外模块包:
sudo apt install linux-modules-extra-$(uname -r)如果还是不行,可能需要升级内核到 5.x 系列。Ubuntu 18.04 可以通过 HWE 内核升级:
sudo apt install linux-generic-hwe-18.04升级后重启,再modprobe基本就能解决。
4.3 性能不达预期的排查思路
如果 NVMe-oF 的延迟比预期高很多,按这个顺序排查:
- 网络带宽是否跑满:用
iperf3测一下实际带宽,如果只有几百兆,说明网卡协商速率有问题。 - MTU 是否一致:两端
ip link show看 MTU,如果不一致会导致分片,性能骤降。 - CPU 是否瓶颈:
top看ksoftirqd和nvme相关线程的 CPU 占用,如果单核跑满,说明中断没分散。 - 后端设备是否瓶颈:如果后端是文件或者慢速盘,先换成 ramdisk 排除后端影响。
- 队列深度是否合适:用
nvme get-feature查看当前队列深度,适当调整。
注意:TCP 传输下,如果网卡不支持校验和卸载(checksum offload),CPU 占用会很高。用
ethtool -k eth0确认tx-checksumming和rx-checksumming是否开启。
4.4 断开连接与清理顺序
清理的时候顺序很重要,搞错了会残留一堆垃圾目录,甚至导致内核 oops。
Host 端断开:
sudo nvme disconnect -n nqn.2024-01.com.example:testsubTarget 端清理:
# 先删除端口下的子系统软链接 sudo rm /sys/kernel/config/nvmet/ports/1/subsystems/nqn.2024-01.com.example:testsub # 删除端口 sudo rmdir /sys/kernel/config/nvmet/ports/1 # 禁用 namespace echo 0 | sudo tee /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:testsub/namespaces/1/enable # 删除 namespace sudo rmdir /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:testsub/namespaces/1 # 删除子系统 sudo rmdir /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:testsub如果rmdir报Device or resource busy,说明还有引用没删干净,用ls -la逐层检查。
4.5 我踩过的三个坑
第一个坑:NQN 命名不规范导致连接失败。一开始我随便写了个test作为 NQN,结果nvme connect报Invalid argument。后来查资料才知道,NQN 必须符合nqn.<年份>-<月份>.<域名>:<自定义名称>的格式,比如nqn.2024-01.com.example:testsub。虽然有些版本宽松一点,但规范命名能避免很多玄学问题。
第二个坑:configfs 重复挂载。我写脚本的时候没加判断,每次执行都mount -t configfs none /sys/kernel/config,结果第二次执行报already mounted,脚本就中断了。后来改成先检查mountpoint -q /sys/kernel/config,没挂载才挂。
第三个坑:ramdisk 大小单位搞错。brd模块的rd_size参数单位是 KB,不是 MB。我一开始写rd_size=1024以为创建 1GB,实际只有 1MB,格式化都过不去。正确写法是rd_size=1048576(1GB = 1024*1024 KB)。
5. 进阶扩展与生产环境建议
5.1 从 TCP 切换到 RDMA 的关键改动
如果手头有支持 RoCE 的网卡,切换到 RDMA 其实改动不大,主要是模块和端口类型:
# Target 端 sudo modprobe nvmet-rdma # 端口配置里把 trtype 改成 rdma echo rdma | sudo tee addr_trtype # 端口号可以保持 4420Host 端连接时把-t tcp改成-t rdma:
sudo nvme connect -t rdma -n nqn.2024-01.com.example:testsub -a 192.168.1.100 -s 4420但 RDMA 对网络要求高,需要配置无损以太网(PFC 和 ECN),否则丢包会导致性能剧烈抖动。如果只是测试,可以先在直连场景下跑通。
5.2 多路径与高可用配置
生产环境肯定要考虑高可用。NVMe-oF 支持多路径(multipath),host 端可以同时连接同一个子系统的多个端口,内核的nvme-multipath会自动做负载均衡和故障切换。
配置方法是在 target 端创建多个端口(不同 IP 或不同网卡),host 端分别nvme connect到这些端口,但使用相同的 NQN。内核会自动识别为多路径设备,在/dev/nvmeXnY之上生成一个/dev/nvmeXcYnZ的聚合设备。
提示:多路径需要
nvme-core模块支持,Ubuntu 18.04 的 4.15 内核已经包含基本的多路径功能,但高级特性(如路径优先级)可能需要更新内核。
5.3 监控与日常维护
跑起来之后,日常监控主要看几个地方:
nvme list:查看当前连接的 NVMe 设备。nvme smart-log /dev/nvme1n1:查看远程设备的 SMART 信息(如果 target 端支持)。dmesg -T | grep nvme:查看内核日志,排查连接断开、超时等问题。cat /sys/kernel/config/nvmet/ports/1/addr_traddr:确认 target 端端口状态。
如果发现连接频繁断开,检查网络是否有丢包,或者 target 端后端设备是否响应超时。TCP 传输下,内核有重连机制,但重连期间 I/O 会阻塞,对上层应用影响较大。
5.4 适用场景与局限性
NVMe-oF 不是万能的。它适合的场景是:对延迟敏感、需要共享高性能存储、且网络条件较好的环境。比如数据库集群、高性能计算、虚拟化平台的后端存储。
不适合的场景也很明显:网络不稳定、带宽受限、或者对成本极度敏感的场景。TCP 传输下,万兆网络是起步,千兆网络跑 NVMe-oF 还不如本地 SATA SSD。另外,NVMe-oF 的配置复杂度比 NFS 高不少,运维成本要考虑进去。
我个人在实际操作中的体会是,NVMe-oF 在 Ubuntu 18.04 上跑通不难,难的是稳定性和性能调优。如果只是做概念验证,按上面的步骤一两个小时就能搞定;如果要上生产,网络、内核、后端存储每一层都得仔细打磨。最后再分享一个小技巧:测试阶段尽量用 ramdisk 后端,排除后端设备的干扰,等协议栈跑通了再换成真实设备,这样排查问题的效率会高很多。