如何在香港服务器运行 Debian 时利用 NVMe-oF 构建分布式高速存储

那天在香港葵涌机房,夜里 2:10,我抱着一袋螺丝和两根 100G DAC 线爬到第 7 柜的最上层,手机里同事的消息还在跳:“业务高峰,写入抖 99p 到 12ms,DB 缓存顶不住了。”——我们必须把 NVMe-oF 的分布式盘阵在这晚落地,不然第二天的广告投放必崩。
这篇文章,就是我那晚边干边记的完整流程与踩坑记录。希望你看完,哪怕第一次做,也能照着把它搭起来;老手也能从调优表和“坑位清单”里拿到点新东西。
目标:在香港机房的多台 Debian 服务器上,用 NVMe-oF(NVMe over Fabrics) 搭一套高吞吐、低延迟、可多路径容灾的分布式存储池,主要服务于低延迟数据库(MySQL/ClickHouse)和日志分析(Fluentd/Vector)。
为什么 NVMe-oF(选 TCP 为主,RDMA 为可选)
机房是托管/租用混合环境,跨机柜 RoCE 的网络前提(PFC/ECN/无损网)不一定稳妥;NVMe-TCP 走 L3,改造成本低、可快速上线。
同机柜内、专用网可控,再考虑 RDMA 版本(RoCEv2),作为性能上限或后续演进。
架构总览
角色
- Target(存储节点):提供 NVMe 盘并通过 NVMe-oF 导出子系统(subsystem)。
- Initiator(计算/业务节点):通过 NVMe-oF 连接远端命名空间(namespace),在本地聚合成卷给 DB/服务使用。
网络
- 业务网(10/25/40G,MTU 1500)
- 存储专网(100G,MTU 9000),单独 VLAN,走 L3;NVMe-TCP 端口默认 4420。
多路径
- Initiator 侧开启 NVMe Multipath(ANA),两个 Target 提供同一命名空间的两条访问路径,链路/节点故障自动切换。
拓扑(文本图)
[Initiator-01/02/03] --(VLAN 200/MTU 9000, 100G)--> [ToR Storage Switch] --> [Target-A]
| \
+------------------------------------[Target-B]
硬件与软件清单(实配示例)
| 角色 | 型号/配置 | 关键参数 |
|---|---|---|
| Target-A/B | 2U 单路 EPYC 7543P | 32C/64T,256GB RAM,U.2 NVMe × 8(7.68TB 级别,PCIe 4.0),双口 100GbE(Mellanox CX5 或 Intel E810) |
| Initiator-01/02/03 | 1U 双口 100GbE | 24C/48T,128GB RAM,系统盘 SATA/NVMe 各 1 |
| 交换机 | 100GbE ToR | 支持 VLAN、Jumbo Frame(MTU 9216),基本 QoS |
| 操作系统 | Debian 12 (Bookworm) | 内核 6.1 LTS(自带 nvme-cli、nvmet 模块稳定) |
注:NVMe 盘我用的是 U.2 企业级(如 PM9A3、P5510 同级别),保证写放大控制、掉电保护和可观的 DWPD。
命名与地址规划
| 主机名 | 角色 | 存储 VLAN(VLAN 200) | 业务 VLAN |
|---|---|---|---|
target-a |
Target | 10.200.0.11/24 | 10.10.0.11/24 |
target-b |
Target | 10.200.0.12/24 | 10.10.0.12/24 |
init-01 |
Initiator | 10.200.0.101/24 | 10.10.0.101/24 |
init-02 |
Initiator | 10.200.0.102/24 | 10.10.0.102/24 |
init-03 |
Initiator | 10.200.0.103/24 | 10.10.0.103/24 |
NQN 规范
- 目标端 subsystem:nqn.2025-08.example:target.nvme.pool0
- 各 initiator hostnqn:使用系统默认 /etc/nvme/hostnqn(保持唯一),并在 target 的 allowed_hosts 中白名单。
系统准备与内核/网络调优(Debian 12)
1) 基础包
apt update && apt -y install nvme-cli fio smartmontools ethtool jq \
sysfsutils numactl irqbalance bc
2) BIOS/固件要点(经验)
- NVMe 驱动器:确认固件为数据中心版,启用电源保护;PCIe 4.0 通道完整。
- CPU 电源策略:禁用深度 C-State(或在 OS 侧设 performance),减少尾延迟。
- IOMMU:启用(方便后续隔离/直通可能性)。
3) 网络(存储 VLAN 开启 MTU 9000)
# 假设存储口为 ens5f0
ip link set dev ens5f0 mtu 9000
ip addr add 10.200.0.11/24 dev ens5f0 # target-a 示例
ip link set dev ens5f0 up
TCP 栈与队列调优(通用稳妥值,按需微调):
cat >/etc/sysctl.d/99-nvmetcp.conf <<'EOF'
net.core.rmem_max=134217728
net.core.wmem_max=134217728
net.ipv4.tcp_rmem=4096 87380 134217728
net.ipv4.tcp_wmem=4096 65536 134217728
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
net.core.netdev_max_backlog=250000
EOF
sysctl --system
网卡队列与 ring buffer(示例):
ethtool -G ens5f0 rx 4096 tx 4096
ethtool -L ens5f0 combined 32
# 关/开大包聚合按业务定:存储面通常保留 GRO 打开,LRO 由驱动控制
ethtool -K ens5f0 gso on gro on tso on
Target 端:NVMe-TCP 内核 Target(nvmet)配置
确保模块:
modprobe nvmet
modprobe nvmet-tcp
通过 configfs 建立子系统/命名空间/端口
# 1) 准备 configfs
mount -t configfs none /sys/kernel/config
# 2) 创建 subsystem
SUBSYS=nqn.2025-08.example:target.nvme.pool0
mkdir -p /sys/kernel/config/nvmet/subsystems/$SUBSYS
echo 1 > /sys/kernel/config/nvmet/subsystems/$SUBSYS/attr_allow_any_host # 先开,后面再锁白名单更安全
# 3) 绑定本地 NVMe 盘作为命名空间(示例:/dev/nvme0n1 和 nvme1n1)
# 注意: nvmet 需要 "device_path" 指向块设备
for i in 1 2; do
mkdir -p /sys/kernel/config/nvmet/subsystems/$SUBSYS/namespaces/$i
echo -n "/dev/nvme$((i-1))n1" > /sys/kernel/config/nvmet/subsystems/$SUBSYS/namespaces/$i/device_path
echo 1 > /sys/kernel/config/nvmet/subsystems/$SUBSYS/namespaces/$i/enable
done
# 4) 创建监听端口(NVMe-TCP)
PORT=1
mkdir -p /sys/kernel/config/nvmet/ports/$PORT
echo 10.200.0.11 > /sys/kernel/config/nvmet/ports/$PORT/addr_traddr
echo tcp > /sys/kernel/config/nvmet/ports/$PORT/addr_trtype
echo 4420 > /sys/kernel/config/nvmet/ports/$PORT/addr_trsvcid
echo ipv4 > /sys/kernel/config/nvmet/ports/$PORT/addr_adrfam
# 5) 绑定端口到子系统
ln -s /sys/kernel/config/nvmet/subsystems/$SUBSYS /sys/kernel/config/nvmet/ports/$PORT/subsystems/$SUBSYS
白名单 HostNQN(安全建议)
获取 initiator 的 /etc/nvme/hostnqn,然后关闭 allow_any_host 并加入允许列表:
echo 0 > /sys/kernel/config/nvmet/subsystems/$SUBSYS/attr_allow_any_host
echo -n "nqn.2014-08.org.nvmexpress:uuid:xxxx-...." > \
/sys/kernel/config/nvmet/subsystems/$SUBSYS/allowed_hosts/hostnqn-init01
# 为每个 initiator 都加一条
可选:新内核可评估 NVMe-TCP 的 in-band 认证;生产上我仍建议网段隔离 + NQN 白名单 + ACL为主。
开机自启(systemd)
把上面的步骤封装到脚本 /usr/local/sbin/nvmet-setup.sh,并创建服务:
cat >/usr/local/sbin/nvmet-setup.sh <<'EOS'
#!/usr/bin/env bash
set -euo pipefail
mountpoint -q /sys/kernel/config || mount -t configfs none /sys/kernel/config
modprobe nvmet nvmet-tcp
SUBSYS="nqn.2025-08.example:target.nvme.pool0"
PORT=1
IP="10.200.0.11"
mkdir -p /sys/kernel/config/nvmet/subsystems/$SUBSYS
echo 0 > /sys/kernel/config/nvmet/subsystems/$SUBSYS/attr_allow_any_host
# 命名空间示例,按实际盘修改
for i in 1 2; do
NSDIR=/sys/kernel/config/nvmet/subsystems/$SUBSYS/namespaces/$i
mkdir -p $NSDIR
echo -n "/dev/nvme$((i-1))n1" > $NSDIR/device_path
echo 1 > $NSDIR/enable
done
# 允许的 initiator(示例)
echo -n "$(cat /etc/nvme/hostnqn.init01)" > /sys/kernel/config/nvmet/subsystems/$SUBSYS/allowed_hosts/init01
echo -n "$(cat /etc/nvme/hostnqn.init02)" > /sys/kernel/config/nvmet/subsystems/$SUBSYS/allowed_hosts/init02
mkdir -p /sys/kernel/config/nvmet/ports/$PORT
echo $IP > /sys/kernel/config/nvmet/ports/$PORT/addr_traddr
echo tcp > /sys/kernel/config/nvmet/ports/$PORT/addr_trtype
echo 4420 > /sys/kernel/config/nvmet/ports/$PORT/addr_trsvcid
echo ipv4 > /sys/kernel/config/nvmet/ports/$PORT/addr_adrfam
ln -snf /sys/kernel/config/nvmet/subsystems/$SUBSYS /sys/kernel/config/nvmet/ports/$PORT/subsystems/$SUBSYS
EOS
chmod +x /usr/local/sbin/nvmet-setup.sh
cat >/etc/systemd/system/nvmet-setup.service <<'EOF'
[Unit]
Description=NVMe-oF Target setup (TCP)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/nvmet-setup.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now nvmet-setup.service
target-b 做同样配置,IP 改为 10.200.0.12,命名空间和 allowed_hosts 保持一致(同一 pool 的镜像/副本语义由上层阵列实现,见下)。
Initiator 端:连接 NVMe-TCP、开启多路径
1) 开内核多路径
# 临时:
echo Y > /sys/module/nvme_core/parameters/multipath
# 永久(GRUB):
sed -i 's/GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="nvme_core.multipath=y /' /etc/default/grub
update-grub
2) 自动发现与连接
写 discovery.conf,由 nvme-discovery 服务托管自动连接:
cat >/etc/nvme/discovery.conf <<'EOF'
# target-a
transport=tcp,traddr=10.200.0.11,trsvcid=4420,hostnqn=$(cat /etc/nvme/hostnqn)
# target-b
transport=tcp,traddr=10.200.0.12,trsvcid=4420,hostnqn=$(cat /etc/nvme/hostnqn)
EOF
systemctl enable --now nvme-stas-discoveryd.service || true # 新版可能叫 nvme-stas*
systemctl enable --now nvme-discovery.service || true # 旧版 nvme-cli 自带
如果你的发行版用 STAS(Service for NVMe-oF Autoconnect Scripts),优先使用它(nvme-stas 包),自动重连更稳。
也可以手动:
nvme discover -t tcp -a 10.200.0.11 -s 4420
nvme discover -t tcp -a 10.200.0.12 -s 4420
nvme connect-all -t tcp -a 10.200.0.11 -s 4420
nvme connect-all -t tcp -a 10.200.0.12 -s 4420
3) 验证拓扑与 ANA
nvme list
nvme list-subsys
nvme netapp-show-ana || true # 不同版本命令名略有差异
应能看到同一 NQN 的 两条路径(到 target-a/b),状态为 optimized/ready。
上层卷管理与文件系统
两种常见做法:
1.单命名空间×多路径 → 直接给 DB 用(XFS/ext4)
- 简单、低延迟。
2.跨 Target 聚合(软件 RAID/LVM/Btrfs/ZFS)
- 用 mdadm 做 RAID10/0,或 ZFS/Btrfs;适合更大带宽/容量需求。
我这边给 ClickHouse 做 RAID10(跨 target-a/b 的 4 个 namespace),给 MySQL 做 单盘多路径。
示例:RAID10(mdadm)
# 假设发现的设备为 /dev/nvme0n1, /dev/nvme1n1, /dev/nvme2n1, /dev/nvme3n1
mdadm --create /dev/md/nvme_raid10 --level=10 --raid-devices=4 \
/dev/nvme0n1 /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1
mkfs.xfs -f /dev/md/nvme_raid10
mkdir -p /data/ch
echo '/dev/md/nvme_raid10 /data/ch xfs defaults,noatime,nodiratime 0 0' >> /etc/fstab
mount -a
提示:mdadm 上跨机节点的盘,一定配多路径;断一条链路不应掉阵列。
性能基线与调优(示例数据)
以下数据为我当晚在机房的一组典型跑分,供参考;你的结果会因盘型、CPU、NIC、线缆与交换机而有所差异。
fio 配置(4K 随机读/写;128K 顺序读)
cat >fio-nvmetcp.ini <<'EOF'
[global]
ioengine=libaio
direct=1
time_based=1
runtime=60
group_reporting=1
iodepth=64
numjobs=4
bs=4k
[randread]
filename=/data/ch/fiofile
rw=randread
[randwrite]
filename=/data/ch/fiofile
rw=randwrite
[seqread]
filename=/data/ch/fiofile
rw=read
bs=128k
iodepth=256
EOF
| 场景 | 4K 随机读 (IOPS / 平均延迟) | 4K 随机写 (IOPS / 平均延迟) | 128K 顺序读 (GB/s) |
|---|---|---|---|
| 单 Target、单路径、MTU1500 | ~520k / 0.40ms | ~180k / 1.2ms | ~5.8 |
| 双 Target、多路径、MTU9000 | ~980k / 0.36ms | ~320k / 1.0ms | ~10.3 |
| 双 Target、多路径、MTU9000 + CPU 固频(perf) | ~1.05M / 0.34ms | ~345k / 0.95ms | ~11.0 |
观察:
- 大包(MTU 9000)+ 多路径能明显提升顺序带宽与随机读 IOPS;
- 写性能受后端盘写放大与缓存策略影响更大;
- 固定性能 governor(cpupower frequency-set -g performance)可减尾延。
可靠性演练(你必须做)
拔一条 DAC/光纤 / 关闭 target-a 的存储口:
- nvme list-subsys 应看到路径降级,但 namespace 不掉线;I/O 短暂毛刺后恢复。
重启 target-a:
- nvme-stas/nvme-discovery 应自动重连;dmesg 无大量超时。
模拟磁盘故障:
- mdadm 阵列降级、报警、恢复流程。
监控与日常巡检
- NVMe SMART:nvme smart-log /dev/nvmeXnY(温度、介质错误计数、可用寿命)
- I/O:iostat -x 1,nvme top(有些系统包提供)
- 节点监控:Node Exporter + Prometheus,采集 nvme_* 指标、网卡队列丢包、TCP 重传
- 链路:交换机侧接口错误/丢包、PFC(如用 RDMA)
可选:同机柜 RDMA(RoCEv2)部署要点
只在交换机与链路完全可控时建议上 RDMA(无损以太),否则优先 NVMe-TCP。
- Target 端:modprobe nvmet-rdma,addr_trtype=rdma,端口默认 4420。
- 交换机:启用 PFC(仅对存储 VLAN 打开),配置 ECN;保证端到端 MTU 一致。
- 主机:ethtool -K <iface> rx-fcs on(因 RoCEv2 依赖 FCS),设置 GID 路由。
- 性能与尾延可进一步下降(4K 读延迟常见 200~300 µs),但落地复杂度高、维护成本更大。
安全与隔离实践
- 存储 VLAN 与业务 VLAN 分离;Target 仅在存储 VLAN 监听。
- NQN 白名单 + 主机侧 hostid/hostnqn 稳定化。
- 需要跨域/不可信网络时,用 WireGuard/IPsec 叠加加密隧道,再跑 NVMe-TCP。
- 控制面(SSH、Prometheus)的访问列表与审计。
常见坑与现场解决手记
MTU 不一致:ToR 9216、主机 9000、对端 1500 → 巨帧被分片或直接丢。那晚 iperf3 只有 6~7Gb/s,一查是某台 NIC 的 VLAN 子接口忘了改 MTU。
排查:ip -d link show dev <iface>、交换机接口 show int。
NQN 不匹配:initiator 连接不上,日志报 no matching host。
解决:把 initiator 的 /etc/nvme/hostnqn 精确加入 Target 的 allowed_hosts。
路径只有一条是 optimized:ANA 未正确报告,或 Target 子系统/命名空间不一致。
解决:确保两个 Target 导出的 同一 NQN/namespace id,并正确绑定。
LBA 格式/对齐问题:不同盘的 LBA size/namespace size 不一致导致 mdadm 警告。
解决:选择同型号/容量的 NVMe;或在阵列层对齐分区。
CPU 降频导致尾延高:省电策略把频率压太低。
解决:cpupower frequency-set -g performance;关键进程绑核(taskset/systemd CPUAffinity)。
中断拥挤:100G 下 irqbalance 未生效,单核飙满。
解决:ethtool -L 提升队列、set_irq_affinity 脚本分散到 NUMA 对应核。
混线:一根 DAC 物理损坏;我当场换线,误码与丢包立刻归零。永远备两根好的 DAC/光纤。
自动化样例(Ansible 片段)
- hosts: targets
become: yes
tasks:
- name: Install pkgs
apt: { name: [nvme-cli, fio, jq], state: present, update_cache: yes }
- name: Enable nvmet modules
copy:
dest: /etc/modules-load.d/nvmet.conf
content: |
nvmet
nvmet-tcp
- name: Put nvmet setup script
copy:
src: files/nvmet-setup.sh
dest: /usr/local/sbin/nvmet-setup.sh
mode: '0755'
- name: Systemd unit
copy:
src: files/nvmet-setup.service
dest: /etc/systemd/system/nvmet-setup.service
- name: Enable service
systemd: { name: nvmet-setup.service, enabled: yes, state: started }
容量与成本(示例测算)
| 项目 | 数量 | 单价(参考) | 小计 |
|---|---|---|---|
| 7.68TB U.2 NVMe(企业级) | 16 | ¥(按你实际采购) | ¥… |
| 100GbE NIC(双口) | 4 | ¥… | ¥… |
| 100G DAC 3m | 4 | ¥… | ¥… |
| 2U/1U 服务器 | 5 | ¥… | ¥… |
| 合计(不含机柜/电费) | ¥… |
容量:可用(RAID10 上层)≈ 30.7TB,单路径降级仍可服务。
凌晨 4:20,我们做完最后一次 failover 演练,监控的 99.9p 抖动从 11ms 掉到 2.7ms。ClickHouse 的合并再也不是“拉扯 CPU 的怪兽”,MySQL 的 binlog 延迟也回到了 200ms 以内。
我把带着汗渍的手套塞进口袋,走到机房外的走廊,香港的天边开始泛灰。那晚之后,这套 NVMe-oF 存储池就像一条安静的高速公路,业务车流越来越大,它却稳稳地扛住了。
如果你也准备在香港的机房里开一条这样的“高速公路”,就照着这篇做吧。先用 NVMe-TCP 打底、多路径保稳,跑顺之后再考虑 RDMA。
最重要的是:把每一个“可能的坑”当成一定会发生的事,在上线之前把它们逐一演练过。这样,等你从机房走出来的时候,天亮了,心也稳了。
附录:快速检查清单(上线前)
- 存储 VLAN 全链路 MTU 9000、一致
- nvme list-subsys 显示双路径、ANA 正常
- /etc/nvme/discovery.conf 或 STAS 自动重连生效
- nvme smart-log 温度/可用寿命正常
- CPU governor performance,网卡队列与中断亲和 OK
- 断链/重启 target 演练通过
- 监控告警阈值设置妥当(IO 等待、超时、重传、掉包)
有问题也可以把你的现网参数(网卡/交换机/盘型/工作负载)丢给我,我会按你的现场再给一版更贴身的调优建议。