5分で動かす:NVMe-oF TargetとInitiatorの基本構成
筆者はProxmox VEで12台のVMとコンテナを管理するhomelab環境を運用しています。本番環境に投入する前にあらゆる構成を検証するための実験場です。最近、専用ストレージノードを追加し、KVMホストへできる限り低レイテンシでNVMeディスクをネットワーク越しに共有する必要が生じました。NVMe/TCPはまさにこの課題を解決してくれます。kernel 5.0以上とLinuxマシン2台があれば十分で、特殊なNICや専用スイッチは不要です。
構成は2つのコンポーネントに分かれます:Target(NVMeディスクを持ち、ネットワークに共有するマシン)とInitiator(KVMホスト、ローカルディスクとして接続して使用)。
ステップ1:Targetの設定(Storage Server)
# 必要なカーネルモジュールをロード
sudo modprobe nvmet
sudo modprobe nvmet-tcp
# モジュールがロードされたことを確認
lsmod | grep nvmet
# NVMe-oFサブシステムを作成
sudo mkdir -p /sys/kernel/config/nvmet/subsystems/nvme-storage-01
# 任意のホストからの接続を許可(lab環境用 — 本番環境ではNQNホワイトリストを使用)
echo 1 | sudo tee /sys/kernel/config/nvmet/subsystems/nvme-storage-01/attr_allow_any_host
# ネームスペースを作成してNVMeデバイスを指定
sudo mkdir -p /sys/kernel/config/nvmet/subsystems/nvme-storage-01/namespaces/1
echo /dev/nvme0n1 | sudo tee /sys/kernel/config/nvmet/subsystems/nvme-storage-01/namespaces/1/device_path
echo 1 | sudo tee /sys/kernel/config/nvmet/subsystems/nvme-storage-01/namespaces/1/enable
# TCPポートを作成(4420はNVMe-oFの標準ポート)
sudo mkdir -p /sys/kernel/config/nvmet/ports/1
echo "192.168.10.100" | sudo tee /sys/kernel/config/nvmet/ports/1/addr_traddr
echo "tcp" | sudo tee /sys/kernel/config/nvmet/ports/1/addr_trtype
echo "4420" | sudo tee /sys/kernel/config/nvmet/ports/1/addr_trsvcid
echo "ipv4" | sudo tee /sys/kernel/config/nvmet/ports/1/addr_adrfam
# サブシステムをポートにリンク
sudo ln -s /sys/kernel/config/nvmet/subsystems/nvme-storage-01 \
/sys/kernel/config/nvmet/ports/1/subsystems/nvme-storage-01
ステップ2:Initiatorからの接続(KVM Host)
# クライアント側モジュールをロード
sudo modprobe nvme-tcp
# ターゲット上のサブシステムを検出
sudo nvme discover -t tcp -a 192.168.10.100 -s 4420
# サブシステムに接続
sudo nvme connect -t tcp -a 192.168.10.100 -s 4420 \
-n nvme-storage-01
# 新しいデバイスを確認
sudo nvme list
lsblk | grep nvme
以上です。5分以内に、NVMeリモートディスクがKVMホスト上でローカルデバイスと同様に認識されます。次のセクションでは、iSCSIではなくNVMe-oFを選ぶ理由と、libvirtへの組み込み方法を説明します。
NVMe-oFとiSCSIの違い
iSCSIは20年以上の歴史を持ち、今も機能しますが、回転ディスク時代に設計されたSCSIコマンドをラップするプロトコルです。NVMe/TCPはNVMeコントローラーと直接通信し、最大65535のI/Oキューという高いキュー深度をそのまま活かせます。間にSCSI変換レイヤーは一切存在しません。
lab内の2ノード間で同一の10GbEネットワーク上で実測した結果:
- iSCSI:約210 μs レイテンシ、約380K IOPS(4Kランダムリード)
- NVMe/TCP:約105 μs レイテンシ、約720K IOPS(4Kランダムリード)
IOPSはほぼ2倍、レイテンシは半減します。VM内のデータベースやI/O集中型ワークロードは、この差をすぐに体感できます。PostgreSQLやMySQLでは、NVMe-oFに移行後にクエリ時間の短縮が目に見えて現れました。
NVMe-oFの3つのトランスポートモード
- NVMe/TCP:通常のTCP/IP上で動作し、専用スイッチや専用NICは不要。筆者が現在使用している方式で、高価なハードウェアを持たないhomelab環境や本番環境に適しています。
- NVMe/RDMA(RoCE):RDMA対応NICと適切に設定されたスイッチが必要ですが、極めて低いレイテンシ(約20 μs)を実現。対応ハードウェアがある場合に選択します。
- NVMe/FC:既存のFibre Channelインフラを持つデータセンター向け。
NVMe-oFディスクをKVM/libvirtに統合する
Initiatorの接続に成功すると、デバイスが/dev/nvme1n1(または同様の名称)として認識されます。VMへの組み込み方法は2通りあります:
方法1:VMへの直接パススルー(推奨)
<!-- VM定義に追加する方法:virsh edit VM名 -->
<disk type='block' device='disk'>
<driver name='qemu' type='raw' cache='none' io='native' discard='unmap'/>
<source dev='/dev/nvme1n1'/>
<target dev='vda' bus='virtio'/>
</disk>
方法2:NVMe-oFボリューム上のQCOW2(より柔軟)
# NVMe-oFブロックデバイスをマウント
sudo mkfs.xfs -f /dev/nvme1n1
sudo mount /dev/nvme1n1 /mnt/nvme-remote
# フラグメンテーション抑制のためクラスタサイズを大きくしてQCOW2イメージを作成
sudo qemu-img create -f qcow2 -o cluster_size=2M \
/mnt/nvme-remote/vm-disk.qcow2 200G
本番環境では方法1を選択しています。オーバーヘッドが少なく、余分なレイヤーを介しません。QCOW2はパフォーマンスをわずかに犠牲にしてスナップショットとthin provisioningを得る方式で、開発・テスト環境により適しています。
NVMe-oFのパフォーマンス最適化
ターゲット側のOSチューニング
# NVMe用I/Oスケジューラを無効化(none = 最小オーバーヘッド)
echo "none" | sudo tee /sys/block/nvme0n1/queue/scheduler
# キュー深度を増加
echo 1024 | sudo tee /sys/block/nvme0n1/queue/nr_requests
# read-aheadを無効化(NVMeは十分高速で、read-aheadは不要なオーバーヘッドになる)
sudo blockdev --setra 0 /dev/nvme0n1
# 高スループット向けにTCPバッファを最適化
sudo sysctl -w net.core.rmem_max=134217728
sudo sysctl -w net.core.wmem_max=134217728
sudo sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"
sudo sysctl -w net.ipv4.tcp_wmem="4096 65536 134217728"
VMネットワークからストレージトラフィックを分離する
ストレージトラフィックがVMネットワークと帯域幅を奪い合うことは、仮想化環境でレイテンシスパイクが発生する最大の原因です。完全に分離しましょう:
# eth1をストレージ専用NIC(10GbEまたは25GbE)として使用する場合
sudo ip addr add 10.10.0.100/24 dev eth1
sudo ip link set eth1 up
# NVMe-oFターゲットをストレージインターフェースにバインド
echo "10.10.0.100" | sudo tee /sys/kernel/config/nvmet/ports/1/addr_traddr
# ストレージインターフェース経由でInitiatorを接続
sudo nvme connect -t tcp -a 10.10.0.100 -s 4420 \
-n nvme-storage-01 -q 8
-q 8フラグは8つのキューペアを作成します。デフォルトの1キューに集中させるのではなく、複数のCPUコアが並列でI/Oを処理できるようになります。8コアサーバーでIOPSが約40%向上することを確認しました。
永続設定 — 再起動後の自動再接続
Target:nvmetcliで設定を保存する
# nvmetcliをインストール
sudo apt install nvmetcli # Ubuntu/Debian
sudo dnf install nvmetcli # RHEL/Fedora
# 現在の設定を保存
sudo nvmetcli save /etc/nvmet/config.json
# 起動時に設定を復元するsystemdサービスを作成
sudo tee /etc/systemd/system/nvmet.service << 'EOF'
[Unit]
Description=NVMe-oF Target
After=network.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/nvmetcli restore /etc/nvmet/config.json
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now nvmet
Initiator:nvmf-autoconnectで自動再接続
# discoveryの設定ファイルを作成
sudo mkdir -p /etc/nvme
sudo tee /etc/nvme/discovery.conf << 'EOF'
transport=tcp
address=10.10.0.100
trsvcid=4420
EOF
# ホストNQNを生成(Initiatorの一意識別子)
sudo nvme gen-hostnqn | sudo tee /etc/nvme/hostnqn
# 自動再接続サービスを有効化
sudo systemctl enable nvmf-autoconnect
homelabからの実践的なヒント
- Jumbo frames(MTU 9000):両端とスイッチで対応している場合は有効化する。1GbEではスループットが10〜15%程度向上するが、10GbE以上では効果は小さい。
- 本番環境での認証:
attr_allow_any_host=1をNQNホワイトリストに置き換える。特定のホストNQNのみサブシステムへの接続を許可する。 - モニタリング:
nvme smart-log /dev/nvme1n1でヘルスチェック、iostat -x 1でI/Oをリアルタイム監視。 - ファイアウォール:ターゲット側でポート4420/tcpを開放することを忘れずに。デバッグ中に見落として半日無駄にすることがある。
セットアップを確認するための簡易ベンチマーク
sudo apt install fio
# ランダムリードIOPS
sudo fio --filename=/dev/nvme1n1 --direct=1 \
--rw=randread --bs=4k --ioengine=libaio \
--iodepth=64 --numjobs=4 --time_based \
--runtime=30 --name=nvmeof-test
# シーケンシャルライトスループット
sudo fio --filename=/dev/nvme1n1 --direct=1 \
--rw=write --bs=1M --ioengine=libaio \
--iodepth=8 --numjobs=1 --time_based \
--runtime=30 --name=nvmeof-seq
IOPSがローカルNVMeの70〜80%に達していれば、セットアップは正常に動作しています。それを大幅に下回る場合は、ネットワークスループット(iperf3)と両端のCPU使用率を確認してください。多くの場合、ボトルネックはそこにあります。
Ceph、OpenEBS、Longhorn — これらのプラットフォームはいずれもトランスポート層でNVMe-oFを使用しています。カーネルレベルでの動作原理を理解しておけば、実際の障害対応時に途方に暮れることはなくなります。
