multikernelのざっくり調査
最近LKMLに投稿されたmultikernelについて。
複数のカーネルをハイパーバイザを使わずに動作させるというもので、 物理的なCPUコアやメモリ領域を割り当てるということで動作するらしい。
コンテナとはカーネルレベルで分離されているので特定のワークロードによる影響、 いわゆるノイジーネイバー問題を避けることができる。 またVMに比べるとオーバーヘッドが小さかったり、ホストカーネルとは独立して動作するので、 VMMがクラッシュしても動作を継続できるというところが嬉しいところか。 あとはRTカーネルと通常のカーネルを同時に使ったり、ワークロードに応じてカスタムカーネルを使ったりとかができるところもメリットとして書かれている。
Youtubeに上がっているデモのホストカーネルをクラッシュさせてもspawnカーネルは動いているというのは面白い。
https://multikernel.io/faq.html
FAQを見るとunikernelとの違いでもVMに依存しないというところが書かれていたりして、 オーバーヘッドとかハイパーバイザへの依存がないところをアピールしているように見える。
他にもCPUやメモリのホットプラグ機能を使うことで動的にリソース割当を変更できることも記載されている。
https://lore.kernel.org/lkml/20250918222607.186488-1-xiyou.wangcong@gmail.com/
LKMLを見るとVMに対しての利点があるか不明とか、ゼロダウンタイムなカーネル更新の方法は外でもやっているとか、 素直に進んでいく感じではなさそう。
Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases を読んだ
Hybrid Logical Clock (HLC) を理解したかったので読んでみた。
前提として、各ノードはおおよそ同じ時刻を指す(ローカルな)Physical Time (PT) を持っている。 PT は NTP を使うことを想定している(が何でも良い)。 このとき、PT のように時間に関連しつつも、Logical Clock (LC) のように happened-before (HB) が成り立つものがほしい。 時間に関連していると Snapshot が取れるが、LC だとタイミングが狙えないということだと思う。
アイディアとしてはシンプルで、あるイベントのロジカル l.j をメッセージ元のイベントの l.m とローカルの l と PT と大きい方を取る。 こうすると HB を満たして PT に近いロジカルクロックができる。 ただロジカルを加算しないので、同じロジカルタイムのときは区別できるように c を更新する。 つまり HLC では (l, c) のような2つ組で時間を扱う。
ここでノード間の PT は NTP を使っておおよそ近いとなると、c はある程度の大きさに収めることができる。 論文では l と c をそれぞれ 48 bit、14 bit にして合計 64 bit の NTP と互換の形式にしている。 これは既存のソフトウェアで使いやすくしているらしい。
Milk-V Vegaを買った
Milk-VからRISC-Vを搭載したL2スイッチが出るということで購入してみた。 10Gポートが2つしかないがとりあえず自分の環境ではサーバとデスクトップ用で十分だ。 購入はAraceを使ったが春節なのとFedexだが途中までは4pxを経由したので、注文してから届くのに1ヶ月程度かかった。
パッケージは非常に簡素で箱にスイッチだけ入っていてマニュアルなども特にない。 電源は付属していないので、12V2AのDC5525コネクタのACアダプタが必要になる。
milk-v vega来た! pic.twitter.com/cyeW9NKfGO
— とし (@toshi_pp) 2024年2月26日
ドキュメントはオンラインにあるがWeb設定画面へのアクセスやシリアルの使い方ぐらいしかない。 デフォルトではTELNETやSSHは無効になっているので、一旦Web設定画面にアクセスして有効にする必要がある。 USBポートがありシリアル経由でメッセージを見たりOSにログインすることはできる。ただ設定用CLIなどはなさそうなので基本はWebで操作することになりそうだ。とはいえL2スイッチなので複雑なことはしなそうだが。

中身は単なるLinuxでソースコードも公開されているので色々いじることもできそうだ。
root@dev:/root> cat /proc/cpuinfo
processor : 0
hart : 0
isa : rv64imafdc
mmu : sv39
root@dev:/root> free
total used free shared buff/cache available
Mem: 229300 27596 196192 104 5512 201364
Swap: 0 0 0
root@dev:/root> df
Filesystem 1K-blocks Used Available Use% Mounted on
ubi0:ubifs 32112 17608 14504 55% /
devtmpfs 114576 0 114576 0% /dev
tmpfs 114648 0 114648 0% /dev/shm
tmpfs 114648 52 114596 0% /tmp
tmpfs 114648 32 114616 0% /run
root@dev:/root> lsmod
Module Size Used by Tainted: G
led_164 3084 1
xy1000_net 10144 1
iperfでスループットを計測したところ、きちんとワイヤーレート出た。
------------------------------------------------------------ Server listening on TCP port 5001 TCP window size: 128 KByte (default) ------------------------------------------------------------ [ 1] local 10.0.0.10 port 5001 connected with 10.0.0.2 port 53732 (icwnd/mss/irtt=14/1448/85) [ ID] Interval Transfer Bandwidth [ 1] 0.0000-10.0032 sec 11.0 GBytes 9.41 Gbits/sec
Reverse Engineering x86 Processor Microcodeを読んだ
この頃、Zenbleed、Inception、Downfall などのプロセッサーの脆弱性が話題になっている。 これらの脆弱性にはベンダーからの microcode アップデートで対応できると言われている。
はて、microcode とは何だったか。なんとなく雰囲気はわかっているものの勉強し直そうと思い適当に検索したら出てきた、Reverse Engineering x86 Processor Microcode という論文を読んでみることにした。
この論文は 2017 年に発表されていて、当時としては比較的モダンな AMD の k8/k10 マイクロアーキテクチャの microcode のリバースエンジニアリングを行って、 モダンな商用プロセッサの microcode がどのようなものでどのように実現されているかということを調べている。
まずはプロセッサだが、論理的には大きく2つの部分に分けられる。1つはレジスタファイルや ALU などを持つデータパスと、もう一つは、命令をデコードしデータパスのコントロールシグナルを発行するコントロールユニットだ。 コントロールユニットのデコーダはざっくり言えば命令からコントロールシグナルへのマッピングを行う。このようなマッピングは hardwired で実装することも可能だし、ROM 内に保存された表を引くことでも実装できる。 そして後者の実現方法を microcoded decode unit という。
microcode は大きく2つの構造があり、データパスのユニットのコントロールシグナルを直接発行する horizontal encoding と、データパスのユニットを駆動する RISC 命令のようなものを発行する vertical encoding というものがある。 vertical encoding による microcoded decode unit は ISA を microcode instruction に変換する一種のインタープリタとみなすことができる。 当然 microcode instruction は直接はデータパスを駆動できないので、実際のシグナルを生成するデコーダが必要になる。
AMD の k8/k10 マイクロアーキテクチャでは、vertical encoding の microcoded decode unit を採用しているらしい。 もちろん、すべての命令を microcode でデコードすると遅いので、レイテンシが求めれらるような単純な命令は hardwired なデコーダを使用する。 そのためモダンな x86 CPU ではハイブリッドなデコーダを採用していることになる。
AMD の microcode アップデートは ROM に書かれた microcode decoder の一部を書き換えることで行っている。 この ROM はレイテンシのため、トランジスタの配線の仕方によって実現されているらしく、物理的に書き換えることはできない。 そのため書き換えは ROM のパッチ先アドレスを格納する match register と RAM によって実装している。 つまり、microcode decoder の表引きを行うときに、アドレスが match register に合致するなら RAM を読み、そうでないなら ROM を読むことで書き換えを実現する。 microcode パッチは RAM に格納するので、起動時に BIOS や OS から毎回 CPU にパッチをロードする必要がある。
AMD の microcode アップデータは 2011 年まで暗号化されていなかったらしく(Intel は 1995 年から暗号化している)、筆者らは専用の OS を作成してアップデータを書き換えて挙動を見ることで、 どのような機能が microcode で実現されているか調べている。
microcode は 3オペランドの RISC のような命令列で表現されているらしい。 書き換え可能な命令は、microcode で実現された命令に限られ、hardwired でデコードされる命令は書き換え方法がわからないということだった。
論文を読んでみて microcode アップデートによる修正がどのように実現されているか知ることができた。 Zenbleed は一種のロジックミスなので、microcode 更新によって実行される microcode instruction 数が増減しそうだがそれほどペナルティはなさそうだ。 一方で、Inception や Downfall ではキャッシュインバリデートやフェンスが必要になるのでペナルティが多いのだろう。
X470D4U に OpenBMC を移植する
はじめに
ASRock Rack から発売されている X470D4U に対して OpenBMC を移植した。
X470D4U は IPMI 機能を搭載しており、管理用イーサネットポート(またはホスト共有ポート)を通じて Web ベースの管理インターフェースにアクセスできる。 こうした機能は、各種ベンダーのサーバにも搭載されていて、Dell だったら iDRAC、HP だったら iLO などが一般のご家庭では利用されていることと思う。
X470D4U の IPMI 機能は自分で使うには必要十分で動作も軽快なので特に不満はないのだが、後継製品の発売によるものかファームウェア更新がされなくなってしまっている。
当然 IPMI 機能にも脆弱性は存在し得るので、できれば更新されたソフトウェアスタックを利用したい。実際にファーウェアファイルを binwalk でみてみると バージョン 3.14 ベースの Linux kernel を使っていることがわかる。
上述の理由はモチベーションの一つだが、単に OpenBMC が面白そうというのも多分にある。
OpenBMC は Linux Foundation による OSS の IPMI ソフトウェアスタックで、IBM のコードをベースに Facebook なども開発に参加しているらしい。 ちょっと検索すると IBM は Power サーバの IPMI に使っていて、Facebook は DC のサーバの IPMI に使っているようだ。
X470D4U に搭載されている BMC は Aspped の AST2500 で OpenBMC でよくサポートされているようだ。また ASRock のマザーボードはいくつかオフィシャルでサポートされている他、Renze Nicolai 氏によって今回移植を行う X470D4U の後継である X570D4U の移植もある。 更に検索したところ、X570D4U に対する移植を X470D4U で起動したレポート(中国語)もあった。 実際に試したところ、IPMI ファームウェアを保存するフラッシュのサイズが異なることで起動に失敗したが、サイズ指定を修正したところ起動に成功した。
この時点でほぼ目処がついているが、実際にはボードの違いにより電源管理、センサーなどの微調整が必要になる。
準備
シリアルコンソールへのアクセス
OpenBMC が利用しているブートローダである U-Boot とのやり取りやカーネルメッセージを閲覧するためにシリアルコンソールを用意する必要がある。
ボード上の DEBUG と印字されているピンヘッダがあるので、ここに 3.3V レベルのシリアルケーブルを接続する。 X570D4U の情報を Renze 氏が提供してくれていて、ピンアサインはこれと同一になっている。
シリアル接続に詳しくなかったのでいろいろ検索したが、ピンとケーブルの TX と RX をクロスして接続し、GND 同士はそのまま接続する。(GND の接続はなくても大丈夫かもしれないが GND を接続すると電位が安定するのだと思う) 3.3V は対向に電圧を供給するために使うようで接続は不要らしい。
フラッシュの書き込み方
X470D4U に搭載されているのは winbond の 25Q256JVFQ というフラッシュメモリで、SOP 16 300 mil のパッケージのものが使われている。 このフラッシュは観音開きのソケットに収められていて、ソケットを開けるとそのまま取り外すことができる。 フラッシュの書き込みのために適当なプログラマーとソケットを購入しておくと良い。
また先の Renze 氏のページにあるフラッシュ書き込み用のピンヘッダーが X470D4U にもあるのでこちらを使うと、 フラッシュを取り外さずに書き込みができるようだ。 ただこのピンヘッダはハーフピッチ(1.27 mm)で接続するための適当なコネクタが見つけられなかったので実際には使っていない。
実は物理的なプログラマーを利用せずにフラッシュの書き込みができ、実際にはこちらの方法を利用した。 AST2500 はホスト側から任意のアドレスにアクセスするという機能(脆弱性と言われてしまったが)があり、これを使うとホスト側からフラッシュの書き換えができる。 このツールは ASRock のサポートサイトからバイナリをダウンロードできる。
ビルドと書き込み
コードは次のリポジトリの x470d4u ブランチに配置してある。
https://github.com/toshipp/openbmc
上記のリポジトリをクローンしたあと、次のようにビルドを行う。
cd ${PATH_TO_REPO}
. ./setup x470d4u ${PATH_TO_BUILD}
bitbake obmc-phosphor-image
ビルドは時間がかかる上にディスクも大量に消費するので注意すること。
ビルドしたアーティファクトは、${PATH_TO_BUILD}/tmp/deploy/images/x470d4u 以下に出力される。
フラッシュに書き込む raw イメージは、image-bmc ファイル(シンボリックリンク)になるのでこれをホストマシンにコピーして socflash を使って書き込む。
socflash_x64 -s image-bmc
書き込む前には、オリジナルイメージを以下のようにバックアップしておくのが良い。
socflash_x64 -b orig.img
書き込みが終わると自動的に BMC がリスタートするが、デバイスの状態が以前起動していたファームウェアによってされていた設定のままの場合があるので、一旦電源を落として起動するのが良い。
次からは、Web インターフェースの Firmware アップデート画面から更新ができる。
このときに選択するファイルは、obmc-phosphor-image-x470d4u.static.mtd.tar になるので注意する。
移植作業
すでにサポートされなくなってしまったみたいだが、移植作業の流れはドキュメントに記載されているので、これを参考に行った。
既存のシステムをコピーして始めるのが良いらしく、今回は同じく ASRock から発売されている E3C246D4I-2T をコピー元とした。
カーネル
X470D4U に必要となるカーネル機能は upstream に入っているので、カーネルに対する特別な開発は必要ない。 ただし、ボードごとにどのようなデバイスが搭載されているか、どのアドレスに設定されているかが異なるためにその設定を行う必要がある。 このような設定は、デバイスツリーを利用して行う。
OpenBMC ではデバイスツリーは Linux kernel のリポジトリに入れる方針になっているが、X470D4U の dts ファイルは現状存在しないので、OpenBMC 側の recipes-kernel に dts ファイルを追加する。
このファイルは Linux 側に含まれている aspeed-g5.dtsi を継承して、必要な部分だけ設定するようになっている。
いくつかの設定については後述するとして、一部の設定を説明する。
プレフィックスに & がつくノードは、ラベルを参照していて、aspeed-g5.dtsi に定義されているノードを上書きしている。
chosenノードは標準出力先の設定とカーネルパラメータを指定している。memoryノードは BMC に搭載されたメモリの物理アドレスとサイズを指定している。bmc-readyノードでは J0 ピン の GPIO の出力設定を行っている。このピンに電圧をかけていないと BIOS 起動が遅くなる。&fmcノードでは BMC ファームウェアが保存されているフラッシュの設定を行っている。データシート的にはデフォルトでも問題ないはずだが、やや不安定になる感じがあるので、spi-max-frequencyをデフォルトから下げている。&uart5ノードは BMC の DEBUG ヘッダにつながっているので有効にしている。&mac0ノードは BMC 専用イーサネットポートを表す。&mac1ノードはホストと共有しているイーサネットポートを表す。use-ncsiをつけることで、NC-SI を利用して通信ができるようになる。
BIOS ポストコード取得機能
OpenBMC では phosphor-host-postdというデーモンがホストの BIOS ポストコードの読み取りを行っている。
このデーモンが利用するデバイスを有効にするために、デバイスツリーで &lpc_snoop ノードを設定している。
SoL 機能
SoL は Serial over Lan の略で、シリアルコンソールを LAN 経由でアクセスするための機能のことをいう。
AST2500 では virtual UART という機能が存在していて、ホストと BMC がシリアルで接続されているように通信ができる。 ホスト側の BIOS にシリアル出力を SoL にリダイレクトするという設定があるので、これを有効にすることでホストのシリアル出力が BMC 側に転送される。
&vuart ノードでこの機能を有効にする。
KVM 機能
KVM はおそらく最も重要な機能の一つで、ネットワーク越しにホストの画面をみたりキーボード入力を行うことができる。
この機能は OpenBMC ではいくつかのコンポーネントが協調して実装されている。
ローレベルなところでは obmc-ikvm が VNC プロトコルサーバとして動作している。 このサーバが AST2500 が提供しているビデオキャプチャ機能をつかってホストの画面を取得し、USB ガジェットドライバを利用してホスト側にキーボード入力やマウス入力を行う。
キャプチャ機能は &video ノードで、USB ガジェットドライバは &vhub ノードで有効にしている。
この VNC サーバは BMC のローカルホストの 5900 番ポートで通信を待ち受けている。
Web インターフェースからこのサーバを利用するために、bmcweb が websocket によるプロトコル変換を行っている。 また、ブラウザ上の VNC クライアントとして noVNC が webui-vue に組み込まれている。
余談だが、USB ガジェットではキーボード入力はスキャンコードでやり取りされるが、noVNC は Javascript なのでスキャンコードではなくて実際に入力される文字しか取得できない。(例えば J と入力したときに、shift + j を使う必要がある) そのため obmc-ikvm では入力文字からスキャンコードの組み合わせを変換しているのだが、これは英語キーボードを前提としているので、日本語キーボードを使っていると入力できない文字がある。この問題を解決するには現状パッチするしかないようだ。
インベントリ機能
IPMI の仕様は FRU 情報について定義していて、この情報は EEPROM などに保存されている。
X470D4U は i2c バスの下に EEPROM がぶら下がっている。eeprom ノードがこの EEPROM を定義している。
entity-manager は EEPROM から FRU 情報を読み取って D-Bus に情報を出力する。
entity-manager は後述する dbus-sensors とも協調し、設定ファイルの json の Exposes にセンサーに関する設定を記載すると、読み取った FRU 情報に合致した設定ファイルのセンサー定義を D-Bus に出力する。
これらの設定は次の json ファイルに記載している。
entity-manager に限らず OpenBMC は D-Bus を使って情報のやり取りをしているので、busctl を使うことでデバッグができる。
busctl list、busctl tree、busctl introspect などを使うことで、各プログラムが出力する情報を確認できる。
また、各プログラムは D-Bus メソッドを提供していることもあるので、busctl call を呼び出すこともできる。
センサー機能
各種センサーは dbus-sensors が読み取って D-Bus に情報を出力する。
今回は電圧、ファンスピード、温度についてのセンサーの設定を行った。
電圧は、&adc ノードと iio-hwmon ノードを設定してドライバを設定している。
読み取った値がどこの電圧値なのかと実際の電圧値への変換係数は entity-manager の設定の ADC タイプで設定する。
どのインデックスがどこの電圧値なのかという情報は、公式ファームウェアの画面から推測できる。
また、公式ファームウェアに SDR.dat というファイルが含まれるので、IPMI の使用を見つつ次のようなスクリプトを使うと、名前とインデックスの対応が取れる。
(インデックスと名前についての情報はおそらく著作権的には問題ないと思われるが、若干グレーかもしれない。)
import sys data = open(sys.argv[1], "rb").read() entries = data.split(b"\x5a\x40") for e in entries: print(e[4-1]) if e[13-1] == 2: idlen = e[48-1] & 0xf name = e[49-1:49-1+idlen] ownerid = e[6-1] ownerlun = e[7-1] sensnum = e[8-1] entid = e[9-1] entins = e[10-1] assert(ownerid == 32) assert(ownerlun == 0) if entid != 7: continue unit1 = e[21-1] unit2 = e[22-1] unit3 = e[23-1] m = e[25-1] assert(e[26-1] == 0) assert(e[27-1] == 0) assert(e[28-1] == 0) assert(e[29-1] == 0) Rexp = e[30-1] >> 4 Bexp = e[30-1] & 0xf nominal = e[32-1] print(f"{name}, sn={sensnum}, entins={entins}, m={m}, {Rexp}, {Bexp}, {nominal}")
電圧の変換係数も公式ファームウェアの画面と hwmon の値からそれっぽい値を作って、 entity-manager のリポジトリに含まれる ASRock のボードの設定ファイルに書かれている近い値を設定した。
いくつかの電圧値はホスト側の電源が落ちていると 0 に近い値が出力される。そのようなものには "PowerState": "On" を設定して電源が入っているときだけ有効にしている。
またバッテリー電圧は、GPIO の G0 ピンをアサートしてやらないと正しい値が読み取れないらしい。
これは BridgeGpio によって設定できる。
この挙動がわからず悩んでいたが、ほかのボードの設定を見ていて気がついた。
ファンスピードは、&pwm_tacho ノードを設定することでドライバを設定している。
自分の環境では CPU ファンしかファンコントローラーに接続していないので、
reg や fan-tack-ch の値は間違っているかもしれない。
温度センサーは &i2c1 ノードの下に NCT6779 というチップがぶら下がっているらしい。
このチップはホスト側の電源を利用しているので、ホスト側が起動していないとドライバを読み込んでもデバイスが認識できない。
そのため、デバイスツリーでは設定せずに、entity-manager で "PowerState": "BiosPost" を設定して BIOS 起動が終わってから動的に初期化するようにしている。
ちなみに i2c バスのスキャンは i2cdetect コマンドを使って行えるが、実際に何があるかはわからないので、他のボードの設定やボードのマニュアルを見つつ推測した。
なにか良い方法があるのだろうか。
電源管理
電源管理は x86-power-control で行う。 このプログラムは、GPIO を使って電源オンオフやリセットを行う。 また、電源状態や、BIOS ポストの完了の取得も GPIO を使って行う。 つまり GPIO ピンアサインがわからないといけないのだが、マニュアルにはそのような情報が記載されていない。
とりあえず他のボードの設定を参考に、電源オンオフやリセットを試したところ正しく動いた。
電源状態や BIOS ポストの完了はどうもボードによって異なるらしいので、gpiomon コマンドを使って GPIO ピンの監視をしつつ電源をつけ消ししてそれっぽいところを使うようにした。
スクリーンショット



まとめ
今回の移植でおおよそ公式ファームウェアの機能と同等の機能を持った IPMI を OpenBMC で実現できた。
i2c デバイスや GPIOのピンアサインの特定さえできればそこまで作業量は多くないので公式の保守がなくなっていたり、OSS の IPMI が欲しい人は試してみると良いかもしれない。
ところで電源を GPIO でオンオフしたり、画面キャプチャしたり USB ガジェットを繋げば IPMI ってできちゃうからラズパイとかでも良さそうだよなと思ったら、そのものずばりの PiKVMというのがあるんですね…。
ublk ドライバを試す
Linux の 6.0 では Userspace block device driver(ublk driver)という機能が追加された。
名前の通り、ユーザーランドでブロックデバイスを実装するためのドライバになっている。
ドキュメントは以下にある。 https://github.com/torvalds/linux/blob/v6.0/Documentation/block/ublk.rst
このドキュメントによると、既存の仮想ブロックドライバをユーザーベースに移行したいというモチベーションがあるらしい。
利点としては以下が挙げられている。
- カーネルで使えない言語やライブラリを利用できる
- アプリケーション開発者がなれているデバッグツールが使える
- クラッシュしてもカーネルパニックしない
- カーネルに依存せず更新できる
- バグがあってもカーネルよりもセキュリティリスクが少ない
- テストやデバッグでパラメータを変えて実験できる
ユーザースペースプログラムのことを、ublk serverと呼称している。
実装には、blk-mq リクエストベースドライバを利用していて、各 IO に tag が振られる。
ublk serverも同様に tag を割り当てて、それが一対一にマッピングされるらしい。
リクエストのやり取りには io_uring を利用していて、ドライバとのやり取りで使うだけでなく、
ublk server内の IO にも使うことが推奨されている。
ここに実装の一例がある。 https://github.com/ming1/ubdsrv
楽に実装するためのライブラリもある。 https://github.com/ming1/ubdsrv/tree/master/lib
このドライバをコントロールするためのグローバルなコントロール用のデバイスとして、/dev/ublk-controlがある。
上述の実装を見ると、このデバイスとのやり取りも io_uring を使うらしい。
新しいデバイスを追加するためには、ublk serverが新しいデバイスとの IO をやり取りするためのデバイスを用意する必要がある。そのためのコマンドをコントロールデバイスに発行すると、/dev/ublkc*が作られる。
その後、ブロックデバイスの追加コマンドを発行すると/dev/ublkb*にブロックデバイスが作られる。
ublk serverはキューごとにスレッドを作成し、io_uringをハンドルする必要がある。
server がUBLK_IO_FETCH_REQを発行すると、ドライバがIOリクエストの転送を始める。
IOはublksrv_io_descにエンコードされており、serverが処理を終えたら結果をUBLK_IO_COMMIT_AND_FETCH_REQコマンドを使って返す。このコマンドで次のリクエスト処理も開始される。
将来の開発計画として、コンテナから ublk を使えるようにしたり、zero copy できるようにしたいそうだ。
それでは実際に使ってみる。実行環境は 2022/11/23 時点の最新の arch linux。
デフォルトではキューの数が1だったので、ublk addの-qオプションでキューの数を32個に増やしている
sudo pacman -S autoconf-archive git clone https://github.com/ming1/ubdsrv.git cd ubdsrv autoreconf -i ./configure make sudo modprobe brd rd_size=4194304 sudo modprobe ublk_drv sudo ./ublk add -t loop -q 32 -f /dev/ram0 sudo mkfs.ext4 /dev/ublkb0 mkdir -p t sudo mount /dev/ublkb0 t cd t sudo fio --name=rw --size=250M --rw=randrw --nrfiles=8 --ioengine=libaio --iodepth=16 --direct=1 --overwrite=1 --runtime=60 --time_based --numjobs=4 --group_reporting
rw: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=16
...
fio-3.33
Starting 4 processes
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
Jobs: 4 (f=32): [m(4)][100.0%][r=703MiB/s,w=703MiB/s][r=180k,w=180k IOPS][eta 00m:00s]
rw: (groupid=0, jobs=4): err= 0: pid=32492: Wed Nov 23 15:35:29 2022
read: IOPS=180k, BW=704MiB/s (738MB/s)(41.3GiB/60001msec)
slat (nsec): min=1150, max=116402, avg=8394.65, stdev=4924.49
clat (nsec): min=940, max=766996, avg=167269.66, stdev=49871.85
lat (usec): min=16, max=775, avg=175.66, stdev=52.93
clat percentiles (usec):
| 1.00th=[ 67], 5.00th=[ 77], 10.00th=[ 86], 20.00th=[ 143],
| 30.00th=[ 159], 40.00th=[ 167], 50.00th=[ 174], 60.00th=[ 178],
| 70.00th=[ 184], 80.00th=[ 192], 90.00th=[ 206], 95.00th=[ 253],
| 99.00th=[ 322], 99.50th=[ 343], 99.90th=[ 388], 99.95th=[ 420],
| 99.99th=[ 498]
bw ( KiB/s): min=599648, max=1156024, per=100.00%, avg=721015.53, stdev=21708.24, samples=476
iops : min=149912, max=289006, avg=180253.88, stdev=5427.06, samples=476
write: IOPS=180k, BW=703MiB/s (738MB/s)(41.2GiB/60001msec); 0 zone resets
slat (nsec): min=1200, max=176111, avg=8791.47, stdev=5048.88
clat (nsec): min=910, max=783474, avg=168873.39, stdev=50225.29
lat (usec): min=18, max=799, avg=177.66, stdev=53.40
clat percentiles (usec):
| 1.00th=[ 68], 5.00th=[ 77], 10.00th=[ 87], 20.00th=[ 145],
| 30.00th=[ 161], 40.00th=[ 169], 50.00th=[ 176], 60.00th=[ 180],
| 70.00th=[ 186], 80.00th=[ 192], 90.00th=[ 208], 95.00th=[ 255],
| 99.00th=[ 326], 99.50th=[ 343], 99.90th=[ 392], 99.95th=[ 424],
| 99.99th=[ 502]
bw ( KiB/s): min=599456, max=1154560, per=100.00%, avg=720465.01, stdev=21605.03, samples=476
iops : min=149864, max=288640, avg=180116.25, stdev=5401.26, samples=476
lat (nsec) : 1000=0.01%
lat (usec) : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.01%, 50=0.23%
lat (usec) : 100=13.17%, 250=81.43%, 500=5.16%, 750=0.01%, 1000=0.01%
cpu : usr=8.49%, sys=31.36%, ctx=29876275, majf=0, minf=52
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=100.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.1%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=10813820,10805346,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=16
Run status group 0 (all jobs):
READ: bw=704MiB/s (738MB/s), 704MiB/s-704MiB/s (738MB/s-738MB/s), io=41.3GiB (44.3GB), run=60001-60001msec
WRITE: bw=703MiB/s (738MB/s), 703MiB/s-703MiB/s (738MB/s-738MB/s), io=41.2GiB (44.3GB), run=60001-60001msec
Disk stats (read/write):
ublkb0: ios=10807636/10799329, merge=0/42, ticks=255538/258038, in_queue=513576, util=99.89%
以前作ったNBDベースのメモリーバックエンドのユーザーランドブロックデバイスで同様にベンチマークを取ると以下のようになった。 こちらもublkと同様にサーバは32スレッドで動かしている。
rw: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=16
...
fio-3.33
Starting 4 processes
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
rw: Laying out IO files (8 files / total 250MiB)
Jobs: 4 (f=32): [m(4)][100.0%][r=655MiB/s,w=653MiB/s][r=168k,w=167k IOPS][eta 00m:00s]
rw: (groupid=0, jobs=4): err= 0: pid=49755: Wed Nov 23 16:16:34 2022
read: IOPS=166k, BW=648MiB/s (680MB/s)(38.0GiB/60001msec)
slat (nsec): min=1730, max=664245, avg=10800.29, stdev=5755.01
clat (nsec): min=1920, max=1455.3k, avg=180044.58, stdev=27830.83
lat (usec): min=19, max=1472, avg=190.84, stdev=28.91
clat percentiles (usec):
| 1.00th=[ 120], 5.00th=[ 137], 10.00th=[ 147], 20.00th=[ 159],
| 30.00th=[ 165], 40.00th=[ 174], 50.00th=[ 180], 60.00th=[ 186],
| 70.00th=[ 194], 80.00th=[ 202], 90.00th=[ 215], 95.00th=[ 227],
| 99.00th=[ 255], 99.50th=[ 269], 99.90th=[ 306], 99.95th=[ 326],
| 99.99th=[ 379]
bw ( KiB/s): min=625760, max=714384, per=100.00%, avg=664019.76, stdev=4227.49, samples=476
iops : min=156440, max=178596, avg=166004.92, stdev=1056.89, samples=476
write: IOPS=166k, BW=648MiB/s (679MB/s)(37.9GiB/60001msec); 0 zone resets
slat (nsec): min=1660, max=532044, avg=11640.12, stdev=6046.44
clat (usec): min=12, max=1441, avg=182.55, stdev=27.93
lat (usec): min=29, max=1460, avg=194.19, stdev=29.04
clat percentiles (usec):
| 1.00th=[ 124], 5.00th=[ 141], 10.00th=[ 149], 20.00th=[ 161],
| 30.00th=[ 169], 40.00th=[ 176], 50.00th=[ 182], 60.00th=[ 188],
| 70.00th=[ 196], 80.00th=[ 204], 90.00th=[ 217], 95.00th=[ 231],
| 99.00th=[ 260], 99.50th=[ 273], 99.90th=[ 310], 99.95th=[ 334],
| 99.99th=[ 392]
bw ( KiB/s): min=626976, max=717352, per=100.00%, avg=663490.35, stdev=4212.33, samples=476
iops : min=156744, max=179338, avg=165872.59, stdev=1053.08, samples=476
lat (usec) : 2=0.01%, 10=0.01%, 20=0.01%, 50=0.01%, 100=0.05%
lat (usec) : 250=98.53%, 500=1.42%, 750=0.01%, 1000=0.01%
lat (msec) : 2=0.01%
cpu : usr=5.53%, sys=34.42%, ctx=23019937, majf=0, minf=44
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=100.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.1%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=9955048,9947240,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=16
Run status group 0 (all jobs):
READ: bw=648MiB/s (680MB/s), 648MiB/s-648MiB/s (680MB/s-680MB/s), io=38.0GiB (40.8GB), run=60001-60001msec
WRITE: bw=648MiB/s (679MB/s), 648MiB/s-648MiB/s (679MB/s-679MB/s), io=37.9GiB (40.7GB), run=60001-60001msec
Disk stats (read/write):
nbd0: ios=9935485/9927606, merge=0/42, ticks=239988/241003, in_queue=480991, util=99.89%
sudo fio --name=rw --size=250M --rw=randrw --nrfiles=8 --ioengine=libaio 13.68s user 83.45s system 157% cpu 1:01.61 total
ublkは700MiB/s程度出ているが、nbdでは650MiB/s程度なのでublkのほうが少し速い。 またnbdではサーバ内で確保したメモリーに書くだけなので、ublkも同様にすればより速くなるかもしれない。
なお実験は、CPUがAMD Ryzen 9 3950X 16Coreでメモリーは32GiB(DDR4-2133)の環境を使った。
nftables メモ
nftables は iptables と比べて色々変わっているのでメモ。
- ruleset はカーネル内に存在する nftable のすべての情報を書き出す。iptables-save に相当するもの。
- table はチェインやセットなどを保持するためのコンテナ。アドレスファミに理紐付いている。iptables と異なり事前定義されてはいない。
- chain はルールを保持するコンテナ。iptablesと違って事前定義チェインは存在しない。base チェインと regular チェインという二種類ある。base はエントリーポイントで、regular は他のチェインからジャンプしてくるときに使う。 base チェインは type、hook、priority が必須パラメータになっている。
- rule はパケット処理ルール。expression と statement から作られる。
- expression は定数値やパケットから得られる値。各種演算子や他の expression と組み合わせて複雑な expression を作れる。 expressionではmetaを使うとパケットのメタデータを取得できる。 インタフェースとかはわかるけど、l4protoとかpalyloadでは?という気もする。
- statement はアクションを示す。statement は評価を打ち切るか継続するかするものがある。acceptやdropは評価を打ち切る。continueは次のルールを実行する。statement を指定しないと continue 扱い。
構文が明に書いていなくて、イマイチわかりにくいところがある。
expression では、ip saddr 127.0.0.1 と書けるけど、比較演算子を使って ip saddr == 127.0.0.1 みたいにも書けるらしい。
https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes#Rules https://wiki.nftables.org/wiki-nftables/index.php/Building_rules_through_expressions
パーサを見てみると、statementの一部として、match_stmtが定義されていて、match_stmt は relational_expr であるらしい。
この relational_expr を見てみると、OP_IMPLICIT というオペレータがあって、どうやらこいつが、OP_EQ と同じような役割を果たすらしい。
http://git.netfilter.org/nftables/tree/src/parser_bison.y#n2843 http://git.netfilter.org/nftables/tree/src/parser_bison.y#n4650
あと面白いのは、concatenations という機能で、set などとのマッチを行うときに、expression の値を連結したものをマッチすることができる。
ip saddr . ip daddr { 127.0.0.1 . 1.1.1.1} みたいな書き方で、python でいうと (ip saddr, ip daddr) in {(127.0.0.1, 1.1.1.1)} みたいな意味になるようだ。
https://wiki.nftables.org/wiki-nftables/index.php/Concatenations