Ubuntu Weekly Recipe

第843回UbuntuでNVMe over TCPを試す

去る10月にUbuntu DiscourseにてNVMe/TCPを使い仮想マシンをストレージレスでUbuntu Server 24.10をブートするというProof of Concept(PoC)デモが紹介されました。NVMe/TCPは2024年3月8日のUbuntu Weekly Topicsでも紹介されているように「iSCSIの後継」といえるものです。

このPoCについては、実際に試せるスクリプト群がGitHubのnvme-tcp-pocレポジトリ(以下、PoCレポジトリ)で公開されています。これを使えば、ネットワークの構成から仮想マシンのセットアップ、Ubuntu ServerのインストールやUEFIの設定までほとんど自動で済んでしまいます。つまり、動かしてみるだけならPoCレポジトリの案内に従えば(あまり問題に遭遇することなく)実現できます。

でも、それでは「なんとなく動いたことはわかる」ものの、一体何をどうしているのかが分かりづらいというのが正直なところです。せっかくなのでこれがどういう代物で何をやっているものなのか、手を動かして理解したいところです。

今回はこのPoCを参考に、NVMe/TCPでストレージレスの仮想マシンを構築・ブートさせる流れを解説します[1]

NVMe over TCP(NVMe/TCP)についての簡単な整理

実際の作業に移る前にNVMe over TCP(NVMe/TCP)について簡単に触れておきます。

NVMe自体はご存じの方も多いかと思いますが、SSD用に開発されたストレージ接続の通信規格です。NVMe登場以前はハードディスク用に作られたSATAやSASを使ってSSDも通信していました。しかし、これらの規格では高性能するSSDの性能を活かしきれなくなり、NVMeという規格が現れました。今どきのほとんどのPCはNVMe SSDが接続されているのではないかと思います。

通常、NVMeデバイスはPCI Expressバスを通じて直接マシンに接続されますが、NVM Express over Fabrics(NVMe-oF)は、ネットワーク越しにリモートのNVMeデバイスをあたかもローカルのNVMeデバイスのように利用できる仕組みです。NVMe-oFの中にも使用する伝送プロトコルで種別がありますが、そのうちの1つがNVMe/TCPです。これはその名の通り、これはTCP越しにリモートのNVMeデバイスに接続するものです。

冒頭で「iSCSIの後継」と触れ込みましたが、iSCSIは「TCP/IP越しにSCSIデバイスに接続して、あたかもローカルのSCSIデバイスのように利用できる」ようにするものです。少々乱暴で短絡的な整理・説明ですが、この「SCSI」の部分がより新しい規格である「NVMe」に置き換わると思えば、NVMe/TCPが「iSCSIの後継」というのもなんとなく腑に落ちるのではないかと思います。

NVMe/TCPでストレージを公開する(サーバー)側を「ターゲット」といい、ターゲットが公開するストレージに接続する(クライアント)側を「イニシエーター」といいます。このあたりの用語もiSCSIのそれと同じです。

なお、NVMe/TCPはNFSやCIFSのようなネットワーク上の共有ストレージ(NAS)ではない点には注意が必要です。NVMe/TCPデバイスをイニシエーター(マシン)「ローカルに存在するNVMeデバイス」と同じように扱います。よって、同じNVMe/TCPデバイスを他のイニシエーター(マシン)と共有すると、簡単に「知らないうちに誰かにNVMeデバイスのデータが書き換えられていた」という状態となります。不整合が発生することにもなるので、NVMe/TCPデバイスは複数のマシンで共有しないでください。

ホストマシンの設定

PoCにならい、本稿でもターゲットおよびイニシエーターは仮想マシンとして用意します。AMD64(x86_64)アーキテクチャ[2]のマシンにクリーンインストールしたUbuntu Desktop 24.04 LTSを仮想マシン(KVM)ホストとして用意します。また、本稿ではターゲット・イニシエーターにそれぞれ2GiBのメモリを割り当てます。マシンに搭載されているメモリ量や空きメモリには注意してください。

まずは、必要なパッケージをインストールします。端末を開き、次のコマンドを実行します。

host$ sudo apt install libvirt-daemon libvirt-clients virtinst virt-viewer virt-manager qemu-system-x86 libcap2-bin wget python3-jinja2 python3-yaml fusefat fdisk dosfstools git

インストール中に管理者ユーザーがlibvirtdグループに追加されますが、ログイン済みのセッションのままでは有効にならず、KVMを使用できません。一度ログアウトしてログインし直します。

また、仮想マシンへのOSのインストールに使用するISOイメージを任意の場所にダウンロードしておきます。本稿ではホームディレクトリ直下にisoディレクトリを用意して配置しているものとします。

  • ターゲット用:Ubuntu Server 24.04 LTS
  • イニシエーター用:Ubuntu Server 24.10[3]

ターゲット側の仮想マシンの作成

ISOイメージがダウンロードできたら「仮想マシンマネージャー」⁠virt-manager)を開きます。QEMU/KVMが選択された状態で「ファイル⁠⁠-⁠新しい仮想マシン」もしくはツールバーの一番左の「新しい仮想マシンの作成」ボタンをクリックするかして、仮想マシンを作成します。

ウィザードのステップ1ではOSのインストール方法に「ローカルのインストールメディア」を選択して「次へ」をクリックします。

ステップ2で「参照」をクリックすると「ローカルを参照」というボタンがあるので、先ほど配置したUbuntu Server 24.04 LTSのISOイメージを選択して「次へ」をクリックします。このとき「エミュレーターはパス...を検索する権限を持っていません。今すぐこれを訂正しますか?」と聞かれた場合は、⁠はい」を押して進んでください。

ステップ3では仮想マシンに割り当てるメモリとCPU数を選びます。特に要件はありませんが、メモリは前述の通り2GiB=2048MiB分割り当てておきます。

ステップ4についてはデフォルトのまま(25GiBのディスクイメージを割り当てる設定)で構いません[4]

ステップ5で「ネットワークの選択」を展開すると「仮想ネットワーク 'default' : NAT」が選択されているはずですが、一応確認し、必要に応じて調整しておきます。仮想マシンの名前はただの管理上の名前なので任意に設定して構いませんが、今回は"target"としておきます。

ステップ5で「完了」をクリックすると、Ubuntu Serverのインストール画面が表示されます。Ubuntu Serverのインストール自体は本連載の第820回を見てください。インストール中に特に変更・注意する点はありませんが、一般的なアドバイスとしては、このあとの作業をSSH越しに実施できる(コピー&ペーストで作業できる)ようOpenSSHサーバーをインストールするようにしておくとよいでしょう。

NVMe/TCPターゲットの設定

仮想マシンの作成とOSのインストールが完了したら、NVMe/TCPデバイス用のストレージを追加します。仮想マシンマネージャーから"target"マシンを開いた状態でツールバーの「i」マークをクリックすると、仮想マシンの構成・設定画面に移るため、左下の「ハードウェアを追加」をクリックします。デフォルトで「ストレージ」が選択されているはずですが、選択されていなければ選択します。

ストレージのサイズは任意ですが、このストレージにイニシエーターとなる仮想マシン側のOSをインストールすることは忘れないでください。今回はデフォルト値である20GiBのままで進めるため、そのまま「完了」をクリックします。

すると、追加したストレージがターゲット上でも次のように認識されるはずです。

target$ lsblk
NAME                      MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sr0                        11:0    1 1024M  0 rom
vda                       253:0    0   25G  0 disk
├─vda1                    253:1    0    1M  0 part
├─vda2                    253:2    0    2G  0 part /boot
└─vda3                    253:3    0   23G  0 part
  └─ubuntu--vg-ubuntu--lv 252:0    0 11.5G  0 lvm  /
vdb                       253:16   0   20G  0 disk

追加されたブロックデバイスである/dev/vdbをNVMe/TCPデバイスにしていきます。

NVMe/TCPデバイスを構成するにはnvmet-tcpカーネルモジュール(とその依存するモジュール)を読み込む必要があります。ちなみに、nvmetは"NVMe Target"という意味かと思います。

target$ sudo modprobe nvmet-tcp

カーネルモジュールが読み込まれると、/sys/kernel/config/nvmet/という設定用のツリーが作成され、このツリーの中で操作することでNVMe/TCPデバイスの設定をおこないます。

それでは早速、この中に移動します。

target$ cd /sys/kernel/config/nvmet/subsystems

ちなみにsubsystemsとあるのは、NVM Subsystemのことです。1つのNVM Subsystemにはこのあとの設定の中でも出てくるポートや名前空間を複数設定できます[5]。柔軟性があるとも複雑ともいえますが、今回はすべて1つずつのシンプルな構成します。

早速、サブシステムを作成します。といっても、subsystemsフォルダ直下にディレクトリを作成するだけです。NVMe-oFではNVMe Qualified Name(NQN)というものを使って、リモートのNVMeデバイスを識別します。ここで作成するディレクトリ名がそのままNQNになります。NQNには命名規則[6]があるため、これに沿ってディレクトリを作成します。

target$ pwd
/sys/kernel/config/nvmet/subsystems
target$ sudo mkdir nqn.2024-12.ubuntu-nvme-tcp-target

ディレクトリを作成したら、その中に移動し、すべてのホストからの接続を受け入れるように設定します[7]

target$ cd nqn.2024-12.ubuntu-nvme-tcp-target/
target$ pwd
/sys/kernel/config/nvmet/subsystems/nqn.2024-12.ubuntu-nvme-tcp-target
target$ echo 1 | sudo tee attr_allow_any_host

続いて、サブシステムに名前空間[8]を作成します。

target$ pwd
/sys/kernel/config/nvmet/subsystems/nqn.2024-12.ubuntu-nvme-tcp-target
target$ sudo mkdir namespaces/1

名前空間に実体となるデバイス/dev/vdbを割り当てて、有効化します。

target$ cd namespaces/1/
target$ pwd
/sys/kernel/config/nvmet/subsystems/nqn.2024-12.ubuntu-nvme-tcp-target/namespaces/1
target$ cat device_path
(null)
target$ echo /dev/vdb | sudo tee device_path
/dev/vdb
target$ cat device_path
/dev/vdb

target$ echo 1 | sudo tee enable

続いて、イニシエーターとの接続に使用されるポートを作成します。

target$ sudo mkdir /sys/kernel/config/nvmet/ports/1
target$ cd /sys/kernel/config/nvmet/ports/1
target$ echo 0.0.0.0 | sudo tee addr_traddr # ローカルマシンのすべてのアドレスでリスンするように設定
target$ echo tcp | sudo tee addr_trtype     # トランスポートタイプをTCPに設定
target$ echo 4420 | sudo tee addr_trsvcid   # ポート番号を4420に設定
target$ echo ipv4 | sudo tee addr_adrfam    # アドレスファミリーをIPv4に設定

最後に先ほど作成した名前空間とポートを対応させます。

target$ sudo ln -s /sys/kernel/config/nvmet/subsystems/nqn.2024-12.ubuntu-nvme-tcp-target/ /sys/kernel/config/nvmet/ports/1/subsystems/

ここまで設定するとdmesgに次のような出力があるはずです。

target$ sudo dmesg |grep nvmet
[ 3923.477064] nvmet: adding nsid 1 to subsystem nqn.2024-12.ubuntu-nvme-tcp-target
[ 4527.316441] nvmet_tcp: enabling port 1 (0.0.0.0:4420)

nvmeコマンドでもサブシステムが見えるか確認しておきます。

$ sudo apt install nvme-cli
$ sudo nvme discover -t tcp -a 127.0.0.1 -s 4420
...
=====Discovery Log Entry 1======
trtype:  tcp
adrfam:  ipv4
subtype: nvme subsystem
treq:    not specified, sq flow control disable supported
portid:  1
trsvcid: 4420
subnqn:  nqn.2024-12.ubuntu-nvme-tcp-target
traddr:  127.0.0.1
eflags:  none
sectype: none

これでターゲット側の設定が完了しました。イニシエーターからの接続の際に使用するため、ターゲット側のマシンのIPアドレスを確認・メモしておきましょう。

なお、/sys/kernel/config/nvmet/以下は揮発性で、OSを再起動すると設定が消えます。今回は順を追って流れを説明するために1つずつコマンドを実行しましたが、永続化させたいのであれば、PoCレポジトリにあるように一連の流れをシェルスクリプトにして、システム起動時にsystemdのサービスユニットとして実行するといった対応が必要です。

イニシエーター側の仮想マシンの作成

次はイニシエーター側のマシンの準備です。NVMe/TCPを使ってマシンをブートさせようとすると、仮想マシンで使用するファームウェア(UEFI)がNVMe-oF(NVMe/TCP)に対応している必要があります。NVMe-oF(NVMe/TCP)対応のファームウェアもPoCレポジトリ上にあるため、事前にホームディレクトリへクローンしておきます。

host$ git clone https://github.com/canonical/nvme-tcp-poc.git

また、仮想マシンで使用するファームウェアを指定したいのですが、仮想マシンマネージャーだと複雑なXMLを手で編集する羽目になります。よって、ここはコマンドで対応することにします。

host$ virt-install \
--autoconsole graphical \
--noreboot \
--name initiator \
--disk none \
--memory 2048 \
--virt-type kvm \
--location /$HOME/iso/ubuntu-24.10-live-server-amd64.iso \
--network network=default \
--boot loader=$HOME/nvme-tcp-poc/resources/OVMF_CODE.fd,loader.readonly=yes,loader.type=pflash,loader.secure=false,nvram.template=$HOME/nvme-tcp-poc/resources/OVMF_VARS.fd,menu=on

先ほど仮想マシンマネージャーで仮想マシンを作成したところなので、このコマンド(オプション)が何をしているかはだいたいわかるかと思います。そのため、イニシエーター側の仮想マシンでポイントとなる部分だけを挙げておくと次のとおりです。

  • --disk noneとして、ストレージレスにします。
  • --locationオプションで事前にダウンロードしたUbuntu Server 24.10のISOイメージを指定しています。
  • --bootオプションでNVMe-oF(NVMe/TCP)対応のファームウェアを利用するように指定しています。

コマンドを実行するとvirt-viewerが開き、先ほどターゲット側のマシンをインストールするときに見たような画面が表示されます。そのままvirt-viewerを使ってインストール作業を続けても構いませんし、仮想マシンマネージャーからinitiatorマシンを開いて切り替えても構いません。使いやすい方で操作を進めてください。

インストーラーの画面を次々進めていると、そのうち次のようにストレージが見つからないというエラーに遭遇するはずです。ストレージをアタッチしていないのでエラーが出ること自体は当たり前です。

図1 必ず遭遇するエラー
図1

インストール先となるストレージが必要です。NVMe/TCPデバイスを接続しましょう。

といってもインストーラー画面からはこれ以上どうしようもないので、右上の"Help"を開いて、"Enter shell"からLiveセッションにログインします[9]

Liveセッションが開いたら、次のコマンドを実行し、先ほど用意したNVMe/TCPターゲットに接続します。

live-session# nvme connect-all -t tcp -a <ターゲットのIPアドレス> -s 4420

すると、次のようにNVMe/TCPデバイスがローカルのNVMeデバイスのように見えているはずです。

live-session# nvme list
Node                  Generic               SN                   Model                                    Namespace  Usage                      Format           FW Rev
--------------------- --------------------- -------------------- ---------------------------------------- ---------- -------------------------- ---------------- --------
/dev/nvme1n1          /dev/ng1n1            deef6bef77cfc6f1e98c Linux                                    0x1         21.47  GB /  21.47  GB    512   B +  0 B   6.8.0-50
live-session# lsblk |grep nvme
nvme1n1 259:1    0    20G  0 disk

NVMe/TCPデバイスが表示されたら、Ctrl+DもしくはexitコマンドでLiveセッションを抜けます。そして"Refresh"を押してストレージ一覧を読み込み直します。すると、今度はインストーラー側でも次のようにNVMe/TCPデバイスが表示されるはずです。

図2 NVMe/TCPデバイスが接続され、インストール先候補として表示されるようになった
図2

残りのインストール作業はそのまま進めます。

インストールが完了したら、あとは再起動するだけ……というわけにはいかず、Ubuntu DiscourseのPoCに関する投稿にもある通り、既知の問題への対処がまずは必要です[10]。調整せずに作業を続けるとブート中にNVMe/TCPデバイスでI/Oエラーが発生し、システムの起動に失敗します。

ということで、再びLiveセッションに入り、/target/etc/cloud/cloud.cfg.d/90-installer-network.cfgを次のような内容に変更して保存します。

network:
  ethernets:
    nbft0:
      dhcp4: true
      critical: true
  version: 2

変更が済んだら、先程と同様にLiveセッションを抜けて、今度こそインストールを完了します(ここで一旦、仮想マシンの電源が切れるはずです⁠⁠。

NVMe/TCPでシステムをブートするようUEFIを構成・操作する

NVMe/TCPでシステムをブートするにはUEFIがNVMe/TCPデバイスを認識し、そのデバイスからOSをブートできなければなりません[11]

仮想マシンマネージャーから仮想マシンを起動したら、次のような表示が出ているうちにEscを連打して、UEFIの設定画面に入ります[12]

図3 バーが伸びきる前にEscを押す
図3

すると次のような設定画面に遷移します。

図4 無事に設定メニューに入った状態
図4

矢印キーとEnterキーを使い"Device Manager" - "NVMe-oF Configuration" - "Attempt 1"と遷移してください。

するとNVMe-oFデバイスの設定画面に移りますので、上から順に次のように設定(特に記載のないものはデフォルトのままで構いません)します。

  • NVM Subsystem: Enabled
  • Network Device List: 使用するNICのMACアドレスを選択する(今回は1つしかありませんので、Enterを押していれば設定できます)
  • Enable DHCP: X(チェックを入れる)
  • NVM Subsystem NQN: nqn.2024-12.ubuntu-nvme-tcp-target
  • NVM Subsystem Address: <ターゲット側のマシンのIPアドレス>
  • NVM Subsystem Port: 4420

そして、同じ画面の下の方にある"Save Changes"を押して、設定を保存します。

設定を保存したら、Escで図4の画面まで戻り、"Continue"を押します。すると、青背景のボックスに"Configuration changed. Reset to apply it Now. Press Enter to reset"というメッセージが表示されます。

言われたとおりにEnterを押して、再びEsc連打に備えます。Esc連打チャレンジに成功するとまた図4の画面が表示されますので、今度は"Boot Manager"に遷移します。

設定がうまくできていると、次のように"UEFI NVMeOF Device - Linux"から始まるエントリが見えているはずです。これがNVMe/TCPターゲットです。

図5 NVMe/TCPターゲットが見えている
図5

このエントリにフォーカスしてEnterを押すと、Ubuntu Server 24.10がブートします。

ストレージレスの仮想マシンですが、起動後に確認するとNVMe(/TCP)デバイスがルートボリュームとなっていることがわかります。

initiator$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sr0          11:0    1 1024M  0 rom
nvme0n1     259:1    0   20G  0 disk
├─nvme0n1p1 259:2    0  953M  0 part /boot/efi
└─nvme0n1p2 259:3    0 19.1G  0 part /

dmesgの中身をnvmeでgrepしてみてもよいでしょう。次のような出力が見えて、NVMe/TCPターゲットに接続していることが伺えます。

$ sudo dmesg |grep nvme
[    3.152001] nvme nvme0: creating 2 I/O queues.
[    3.154376] nvme nvme0: mapped 2/0/0 default/read/poll queues.
[    3.159688] nvme nvme0: new ctrl: NQN "nqn.2024-12.ubuntu-nvme-tcp-target", addr 192.168.122.155:4420, hostnqn: nqn.2014-08.org.nvmexpress:uuid:34859525-128E-45A7-AAA4-3AEE826B44F2
[    3.168029]  nvme0n1: p1 p2
[    4.076132] EXT4-fs (nvme0n1p2): orphan cleanup on readonly fs
[    4.076635] EXT4-fs (nvme0n1p2): mounted filesystem aecf3fdc-09fe-42c0-aec1-d39c78a39d58 ro with ordered data mode. Quota mode: none.
[    4.989983] systemd[1]: Starting modprobe@nvme_fabrics.service - Load Kernel Module nvme_fabrics...
[    5.027524] EXT4-fs (nvme0n1p2): re-mounted aecf3fdc-09fe-42c0-aec1-d39c78a39d58 r/w. Quota mode: none.
[    7.545300] nvme nvme1: new ctrl: NQN "nqn.2014-08.org.nvmexpress.discovery", addr 192.168.122.155:4420, hostnqn: nqn.2014-08.org.nvmexpress:uuid:34859525-128E-45A7-AAA4-3AEE826B44F2
[    7.554014] nvme nvme1: Removing ctrl: NQN "nqn.2014-08.org.nvmexpress.discovery"

ちなみに、筆者は試しにServer版でインストールしたあとに、ubuntu-desktopパッケージを入れてDesktop化しようとしました。しかし、前述の既知の問題と同じような状態となってNVMe/TCPターゲットとの接続が切れてしまい、再起動してもシステムごと壊れていまったようで、起動しませんでした。詳細は追えていませんので筆者の推測になりますが、ubuntu-desktopパッケージと依存関係を持つネットワーク関連のパッケージがインストールされる際に、ネットワークの再設定が走って発生する事象ではないかと考えています。

その他にも「すべてのホストから接続を許可する」といったようにセキュリティ的にも甘い部分があります。また、そもそもシステムを起動させるたびにUEFIを操作してNVMe/TCPデバイスからブートさせる操作が必要です。

いかにも「PoCのデモ」という状況ですが、高価な機器を用意しなくとも手軽にNVMe/TCPを試せるので、先進的なストレージレス環境でちょっと遊んでみるには十分かと思います。お手元でチャレンジされる場合は「あくまでお試し」という遊び心は忘れないようにしましょう。

ちなみに、勘のよい方はお気づきかと思いますが、実はイニシエーターからネットワーク接続できるところに存在すれば、ターゲット側は仮想マシンである必要はありません(イニシエーターはNVMe-oF(NVMe/TCP)対応のファームウェアを使う必要があるため、そういうマシンを持っていないのであれば、仮想マシンを使うことになります)[13]。ターゲット側のマシンを別のPCにするなりにしてイニシエーター側のマシンと物理的に離してみれば「より本格的」なNVMe/TCP環境を味わえるので、チャレンジしてみても面白いでしょう。

おすすめ記事

記事・ニュース一覧