Device)は、Linuxで利用可能なリモートでブロックアクセスを行うための仕組みです。Socketを通じてサーバに送ることができます。NBDを通じて手元のマシンに接続することなどができます。iSCSI等のプロトコルと比較するととてもサーバの実装が容易で、基本的にはネゴシエーション・ブロック範囲へのRead,Writeの処理を正しく扱うことができれば最低限の自作の実装が行えます。https://adventar.org/calendars/4021社内勉強会で行ったものを再編集して掲載しています。NBDの概要NBDはLinuxのカーネルモジュール実装・クライアントの実装・サーバの実装と主に3つのコンポーネントが協調して動作します。クライアントの実装は、引数から受け取った情報を元にサーバへと接続した後にネゴシエーションと呼ばれる接続の初期段階の処理を行い、ソケットをnbdカーネルモジュールに渡します。ソケットがnbdモジュールに移ると、実際にブロックデバイスとしてのRead/Write等のコマンドを受け付けるようになります。以下は簡単なクライアントの利用方法です。コマンドで接続先のホスト名・ポート番号接続するデバイス名を指定し、接続が完了すれば利用できるようになります。Socketのサーバを建てるだけです。そのため、適切なアクセス権が有れば特権でなくとも動作させることができます。ブロックデバイス等が指定できます。./device.imgまた、サーバが複数のブロックデバイスを公開する事を想定したプロトコル設計がなされています。いう名前のデバイスが有るかを確認し、存在すればその情報をクライアントに返します。サーバの設定ファイルを記述することで、名前と実デバイスは自由に対応付を行うことが出来ます。クライアントに返されるデバイスを「Export」と呼び、その情報の内容は概ね以下のとおりです。ブロックアクセスに最低限必要な項目も含まれていることが分かります。(バイト単位の数値)プロトコルの概要それでは、通信の内容を追っていきます。全体を簡単に紹介します。NBDの通信は3つのフェーズで構成されます。(ブロックのやり取り)Handshakeのフェーズでは、プロトコルやハンドシェイク内の機能の交換・サーバが公開するブロックデバイス情報(Export)の交換と接続先Exportの決定を行います。Transmissionのフェーズでは、実際にRead/Write/Flush等の命令がソケットに流れます。ハンドシェイク内でフラグを渡せば、Trim/Discord等のコマンドも流れるようになります。SocketをListenするサーバを作る・クライアントを作ります。特殊なことはせず、サーバだとこんなイメージです。1ハンドシェイクは大きく分けて2つのフェーズに分けることが出来ます。ここでは初期段階の後半のハンドシェイクに関わるバージョン情報をフラグとして交換します。ハンドシェイクの際は必ず同期通信を行い、相手の返答があるまで双方はバイト列を送信することは出来ません。始まる行がサーバから送信するバイト列・C:から始まる行がクライアントから送信するバイト列を表します。表すマジックナンバーを送信します。続けて、ハンドシェイク内で使用する機能を表すビット積を32bitで送信します。サーバ・クライアントは、各々が対応する機能のビットを立てたフラグを送受信し、互換性が無ければソケットを切断します。2ハンドシェイクの後半では、オプションの交換を行います。このフェーズは、クライアントがサーバに情報を問い合わせるという形で進行します。様々なオプションのタイプが用意されており、Export情報・TLS接続情報などがやり取りされます。していますが、他のOptionTypeも存在します。表すマジックナンバーを送信します。続けて、OptionType・リクエストボディの長さ・任意長のリクエストボディを送信します。サーバも同様に返信します。等になるまで受信を繰り返します。OptionTypeには以下のようなバリエーションがあります。OptionTypeによって、ペイロードの扱い方はそれぞれ異なります。実装すれば、Linuxのnbd-clientコマンドからの接続を行えるようになります。返信が終了すればそのまま次のTransmissionフェーズへと移行します。これだけ実装すればLinuxクライアントから接続可能NBDOPTINFOとほぼ同じ挙動サーバ側の返信が終わった時点で、即時にTransmissionフェーズへ移行するペイロードを埋めた状態での、NBD_OPT_GOのリクエスト例を示します。OptionTypeにNBD_OPT_GOを指定し、リクエストボディの長さを今回は14byteとします。リクエストボディの中には、クライアントが要求するExportの名前・その情報の項目を詰めます。サポートされるブロックサイズを教えて)続いてサーバからの返信内容です。以下では、2つの情報について返信を行っています。いう事でも分かると思います。TRIM(Discord)などの機能の可否を返しています。その他、BlockSize等を返すことも出来ます。要求されている情報を返さない・要求されていない情報を返すことも許容されます。不明なレスポンス等がある時は、無視することも行えますが接続を切断することが推奨されます。ここまでの処理で、以下の事が行えました。次は実際のデータ転送を行います。phaseこのフェーズに入ると、非同期な通信が可能となります。基本的な動作は、Offset/Length指定して読み書きするだけです。コマンドへの送信には必ずハンドル番号を付与し、返信にも同じハンドル番号を付与することで、非同期のコマンド送受信を管理します。以下は読み取りを行う際の例です。64bitのハンドル番号・64bit整数のアドレス値・32bitのバッファ長を送信し、レスポンスを得ています。(バイト列)以下は書き込みを行う際の例です。先程の情報に加え、クライアントがバイト列を送信します。phaseにて適切に設定を行っていれば利用が可能となります。基本的はNBDは1つのソケットに直列化したコマンドを送受信するので、高速化のためのMultiQueueなどは利用できない点は注意が必要です。Goのライブラリ以上がNBDプロトコルの詳細でした。非常にシンプルで様々な言語で楽に実装できると思ったので、手始めにGoのライブラリを作成しました。https://github.com/kamijin-fanta/nbd-go薄いラッパーとして動作し、2つのインタフェース・6個程度のメソッドを実装するだけで任意のブロックストレージが作れます。サンプルコードとして、50行余りで書いたオンメモリストレージを入れています。https://github.com/kamijin-fanta/nbd-go/blob/master/examples/inmemory/inmemory.goあまりエラー処理を真面目にやっていないので、実際に使うとなれば変更を加えようかなと考えています。