SR-IOVがVMネットワーキングの考え方を変えた理由
20台以上のKVMゲストが稼働するベアメタルハイパーバイザーを運用していると、同じ壁に何度もぶつかっていた。仮想ブリッジ(virbr0)とvirtioネットワーキングがCPUサイクルを消費し、iperf3ベンチマークではっきりと現れるレイテンシを引き起こしていたのだ。ソフトウェアスタック経由では、10GbE NICのVMへの帯域はせいぜい4〜5 Gbps程度。物理ハードウェアを使う目的がパフォーマンスなのに、これは痛すぎる。
SR-IOV — Single Root I/O Virtualization — は、その問題に対するハードウェアレベルの解答だ。NIC自体が複数の軽量なVirtual Functions (VFs)を提供し、ゲストOSに直接マッピングされることで、データパスからホストカーネルを排除する。複数のクラスターで本番運用してきたが、十分な安定性を発揮している。チューニングなし、DPDKなしで、10GbEカードのスループットが安定して9.4〜9.8 Gbpsに到達する。VFをゲストに直接渡すだけでいい。
このガイドでは、ハードウェアサポートの確認、IOMUの有効化、VFの作成、KVMゲストへの割り当てまで、全工程を解説する。Ubuntu 22.04とRHEL 9、Intel X550とMellanox ConnectX-4カードで検証済みだ。
前提条件とハードウェアの確認
SR-IOVには3つのレイヤーでのサポートが必要だ。NIC自体、CPU/チップセット(IOMMU)、そしてBIOSだ。設定ファイルに触れる前に、この順序で確認しておこう。
NICのSR-IOVサポートを確認する
# NICのPCIアドレスを探す
lspci | grep -i ethernet
# SR-IOV対応の確認("Single Root I/O Virtualization"を探す)
lspci -v -s 0000:03:00.0 | grep -i sriov
# またはsysfsで直接確認する
cat /sys/bus/pci/devices/0000:03:00.0/sriov_totalvfs
sriov_totalvfsから数値(Intel X550なら63、一部のMellanoxカードなら8など)が返れば、NICはSR-IOVに対応している。ファイルが存在しない場合は非対応だ。
IOMUのサポートを確認する
# Intel CPU
grep -o 'vmx' /proc/cpuinfo | head -1
# AMD CPU
grep -o 'svm' /proc/cpuinfo | head -1
# IOMUがすでに有効かどうか確認する
dmesg | grep -e DMAR -e IOMMU | head -20
「DMAR: IOMMU enabled」や「AMD-Vi: Interrupt remapping enabled」のような行が表示されれば問題ない。何も出ない場合は、BIOSでIOMUがオフになっているか、カーネルにまだ渡されていないかのどちらかだ。次のステップで対処する。
インストール:IOMUの有効化とドライバーの読み込み
カーネルブートパラメーターでIOMUを有効にする
GRUBの設定を編集して、ブート時にIOMUフラグを渡す。正確なパラメーターはCPUベンダーによって異なる:
# GRUB設定を編集する
sudo nano /etc/default/grub
# Intel向け — GRUB_CMDLINE_LINUX_DEFAULTに追加:
# intel_iommu=on iommu=pt
# AMD向け — 追加:
# amd_iommu=on iommu=pt
# 編集後の行の例:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on iommu=pt"
iommu=ptフラグはパススルーモードを有効にする。ホスト側に留まるデバイスのIOMUトランスレーションオーバーヘッドをスキップする。これがないと、ホスト自身のNICやストレージコントローラーのDMAトランザクションまでアドレス再マッピングを経由することになる。
# 適用して再起動する
sudo update-grub # Debian/Ubuntu
# sudo grub2-mkconfig -o /boot/grub2/grub.cfg # RHEL/CentOS
sudo reboot
再起動後にIOMUを確認する
dmesg | grep -e DMAR -e IOMMU
# 期待される出力: "DMAR: IOMMU enabled" または "AMD-Vi: AMD IOMMUv2 loaded and initialized"
必要なパッケージのインストール
# Ubuntu/Debian
sudo apt install -y qemu-kvm libvirt-daemon-system virt-manager
sudo apt install -y linux-modules-extra-$(uname -r) # 追加ドライバーモジュール
# RHEL/CentOS
sudo dnf install -y qemu-kvm libvirt virt-install
sudo dnf install -y kernel-modules-extra
設定:Virtual Functionsの作成とKVMへの割り当て
物理NICにVirtual Functionsを作成する
まず物理ファンクション(PF)のインターフェース名を確認し、次にVFを作成する:
# PCIアドレスからインターフェース名を取得する
ls /sys/bus/pci/devices/0000:03:00.0/net/
# 出力: ens3f0
# Virtual Functionsを4つ作成する(必要に応じて数を調整)
echo 4 | sudo tee /sys/bus/pci/devices/0000:03:00.0/sriov_numvfs
# VFが作成されたことを確認する
lspci | grep "Virtual Function"
ip link show ens3f0
echoコマンドの後、ip link showを実行すると、物理インターフェース配下にvf 0からvf 3が、それぞれ固有のMACアドレスとともに表示されるはずだ。
VF数を再起動後も永続化する
sysfsへの書き込みは再起動後に消えてしまう。systemdサービスを使って自動的に復元しよう:
# 永続化のためのsystemdサービスを作成する
sudo tee /etc/systemd/system/sriov-setup.service <<'EOF'
[Unit]
Description=SR-IOV Virtual Functionsを有効にする
After=network.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo 4 > /sys/bus/pci/devices/0000:03:00.0/sriov_numvfs'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now sriov-setup.service
パススルー用のVF PCIアドレスを確認する
# VFのPCIアドレスを一覧表示する
lspci | grep "Virtual Function"
# 出力例:
# 03:10.0 Ethernet controller: Intel Corporation Ethernet Virtual Function 700 Series
# 03:10.2 Ethernet controller: Intel Corporation Ethernet Virtual Function 700 Series
# VFのIOMUグループを確認する
for d in /sys/kernel/iommu_groups/*/devices/*; do
echo "Group $(basename $(dirname $(dirname $d))): $(basename $d) $(lspci -nns $(basename $d) 2>/dev/null | cut -d' ' -f3-)"
done | grep "03:10"
各VFは独自のIOMUグループに割り当てられるべきだ。グループを共有するとパススルーの競合が発生する。これはボードによって異なるハードウェアルーティングの問題であり、ソフトウェアだけでは解決できない。
virt-manager / virsh経由でKVMゲストにVFを割り当てる
方法は2つある。XML編集かvirshコマンドだ。XMLの方が明示的で監査しやすい:
# 追加するVFデバイスのXMLスニペットを取得する
virsh nodedev-dumpxml pci_0000_03_10_0
# VMの定義を編集し、<devices>内に追加する:
sudo virsh edit vm-name
<devices>内にこのブロックを追加する:
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x03' slot='0x10' function='0x0'/>
</source>
<address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
</hostdev>
managed='yes'属性は、VMの起動/停止時にlibvirtが自動的にVFをホストドライバーからデタッチし、再アタッチするよう指示する。手動でのドライバーバインディングは不要だ。
オプション:VFに特定のMACアドレスを設定する
# 物理インターフェースens3f0のVF 0にMACアドレスを設定する
sudo ip link set ens3f0 vf 0 mac 52:54:00:ab:cd:01
# 確認する
ip link show ens3f0
DHCPサーバーがMACアドレスでリースを管理している場合に役立つ。これを設定しないと、VFは起動のたびにランダムなMACアドレスを取得してしまう。
確認とモニタリング
ゲスト内でVFが認識されていることを確認する
VMを起動し、ゲスト内から確認する:
# KVMゲストの内部
lspci | grep Ethernet
# virtio-netではなく、IntelまたはMellanoxのデバイスが表示されるはず
ip link show
ethtool -i eth0 | grep driver
# driver: iavf (Intel Adaptive Virtual Functionドライバー)
# または: mlx5_core (Mellanox)
iperf3によるスループットのベンチマーク
# 受信側(別のホストまたはVM)
iperf3 -s
# SR-IOV VFを使用するゲスト側
iperf3 -c <receiver_ip> -t 30 -P 4
# 比較:virtio-netインターフェースでの同じテスト
# SR-IOV結果:10GbEで約9.4 Gbps、CPU使用率 約8%
# virtio-net結果:10GbEで約4.5 Gbps、CPU使用率 約35%
ホストからVF統計情報をモニタリングする
# ホストからVF別統計情報を監視する
watch -n 1 'cat /sys/class/net/ens3f0/device/sriov/0/stats 2>/dev/null || \
ip -s link show ens3f0'
# VFのリンク状態とトラストモードを確認する
ip link show ens3f0
# VFのトラストモードを有効にする(一部のユースケースでプロミスキャスモードに必要)
sudo ip link set ens3f0 vf 0 trust on
# VFのエラーをdmesgで確認する
dmesg | grep -i 'vf\|iavf\|mlx5' | tail -20
よくある問題のトラブルシューティング
- sriov_numvfsへのechoの後にVFが表示されない:
dmesg | grep sriovを確認する。多くの場合、BIOS設定(SR-IOV Enable、ARI Forwarding)を先にオンにする必要がある。 - libvirtがVF付きのVMを起動できない:まず
virsh nodedev-detach pci_0000_03_10_0を手動で実行してからVMを起動してみる。自動デタッチとドライバーバインディングが競合することがある。 - IOMUグループに複数のデバイスが含まれている:これはACS(Access Control Services)の問題だ。一部のカーネルにはACSオーバーライドパッチがある。あるいは、IOMUグループ全体をゲストに渡す方法もある。
- ホストの再起動後にゲストがVFを失う:先ほど作成したsystemdサービスが有効になっているか確認し、再起動後に
systemctl status sriov-setup.serviceをチェックする。
SR-IOVはvirtioよりも設定に手間がかかる。これは否定しようがない。しかし、VFが設定されてsystemdサービスが稼働すれば、後は気にしなくていい。ゲストには実際のNICが見えているのだから。
ホストのCPUオーバーヘッドは約35%から10%以下に低下する。レイテンシはサブミリ秒になる。データベースレプリケーション、NFSストレージバックエンド、高頻度ログパイプラインなど、大量のトラフィックを処理するワークロードでは、このオーバーヘッドの差がすぐに積み重なる。BIOSの設定とudevのセットアップに費やす1時間は一度きりのコストだが、スループットの改善は毎日続くのだ。

