HN 日本語サマリー

← 一覧へ戻る
プログラミング

io_uring をリードヘッドなしで使用する

Io_uring Without Readahead (frn.sh)

75 pointsby porridgeraisin19 コメント

要約

この記事は、Linuxのio_uringサブシステムにおけるリードヘッド(readahead)機能の有無がパフォーマンスに与える影響を調査しています。リードヘッドなしのio_uringは、アプリケーション側での並行処理の欠如により遅延が発生する可能性がある一方、リードヘッドを有効にするとブロックレイヤーでのリクエストマージが進み、パフォーマンスが向上するケースがあることを示しています。

全文翻訳

Tursoにリードヘッドを実装するプルリクエストが開かれました。これは場当たり的な実装でしたが、io_uringを測定し、それについてさらに理解するための良い口実となりました。Tursoには2つのバックエンドがあります。syscallはpread(2)を使用します。io_uringはio_uringを使用し、データベースファイルをO_DIRECTで開き、バッファリングI/Oを使用するオプションはありません。O_DIRECTはカーネルのリードヘッドを奪うため、それを取り戻すにはアプリケーションで実装する必要があります。1プルリクエストの結果は印象的です。アプリケーションバッファを持つio_uringの方が高速です。なぜそうなるのか理解したいです。リードヘッドなしでは、io_uringは一度に1つのエントリしか発行しません。問題は並行性の欠如です。各読み取りは前の読み取りを待ちます。アプリケーションは、「この時点でページ100が必要だ」と知っています。これは、Tursoがページ100の読み取りSQEをサブミットし、それを待つことを意味します。スキャンは続行されます。次にTursoはページ101を必要とし、そのページに対する新しい読み取りSQEをサブミットします。リードヘッドがオンの場合、状況は変わります。Tursoはページ100を必要とし、シーケンシャルアクセスを検出し、1ページを要求する代わりに、ページ100から131までの読み取りをサブミットします。これにより、32個の読み取りが同時に進行します。以下の測定では、分析データベースの標準ベンチマークであるTPC-Hを、1.2 GiBのデータベースで使用しました。測定したクエリQ6は、ベンチマークで最大のテーブルであるlineitemをフルスキャンします。offはPRAGMA prefetch_pages=0を意味します。リードヘッドなしのプルリクエストのコードです。onは32ページのウィンドウを意味します。リクエストマージリードヘッドがI/Oパスで何を変えるのか、Tursoがいくつのリクエストをサブミットし、何がデバイスに到達するのかを確認したかったのです。そのため、Q6と並行してiostat -dxm 1 sdaを実行し、io_uring:io_uring_submit_reqトレースポイントをカウントしました。off (n=1) on (n=1) SQEs submitted 195,207 218,212 device requests ~196,000 ~16,300 rareq-sz 4.37 KiB 56.53 KiB %rrqm ~0 91-93% rareq-szはデバイスに到達する読み取りリクエストの平均サイズです。%rrqmは、発行される前に別のリクエストとマージされた読み取りリクエストの割合です。リードヘッドがオンの場合、Tursoは23,005個のSQEをさらに発行し、必要以上のページを取得するため、デバイスはより多くのバイトを読み取ります。しかし、デバイスが受信するリクエストは少なくなります。キュー内の2つのリクエストが隣接するセクターをカバーしている場合、ブロックレイヤーはそれらを1つの大きなリクエストに結合します。これは、両方のリクエストが同時にキューにある場合にのみ機能します。perfでマージトレースポイントをカウントしました。リードヘッドオフの場合、195,516個のbiosのうち140個しかマージされませんでした。キューにSQEが1つしかないため、マージするものがなく、これは理にかなっています。リードヘッドオンの場合、218,493個のbiosのうち202,539個がマージされ、デバイスはわずか15,951個のリクエストを受信しました。キュー内に複数のSQEがあったため、カーネルがそれらをマージしました。ポーリングスレッドTursoはsqpollでio_uringを使用します。sqpollは、作業が配信されたかどうか、およびカーネルが何かを行う必要があるかどうかをチェックするためにスピンするスレッドを使用します。これは、コストをかけて作業を追跡するスレッドです。prefetch_pages=32でsqpollの実行時間を計測し、どこに時間が費やされているかを確認しました。ウォールタイム8.22秒、ユーザータイム3.70秒、システムタイム8.46秒(7回の実行の中央値)。システムタイムはウォールタイムよりも大きく、これは2つのスレッドが同時にCPUを消費する場合にのみ可能です。サイクルはクエリコードにあると予想していましたが、次のようになっています。Q6はio_uringバックエンドでSQポーリングを使用しています。io_sq_thread、カーネルポーリングスレッドがサイクルの65%を占めています。そのため、SQポーリングなしでTursoを再構築し、setup_sqpollビルダーをプレーンなIoUring::newに置き換えました。これは、sqpollのセットアップが失敗した場合のTursoのフォールバックとしてすでに使用されています。プレーン/デフォルトリング: ウォールタイム8.62秒、ユーザータイム3.62秒、システムタイム1.27秒。ポーリングスレッドを削除すると、ウォールタイムはわずかに悪化し、システムタイムは大幅に減少しました。システムタイムがゼロでないのは、サブミットごとにio_uring_enter(2)を呼び出しており、Tursoは一度に1つのSQEをサブミットするためです。ポーリングが価値があるかどうかは、マシンによって異なります。Didonaらによる2022年のSYSTOR論文で測定されました。1つのNVMeドライブと1つのCPUコアでのサブミッションポーリングは、わずか13 KIOPS(毎秒13,000 IO操作)しか達成できませんでした。2つのスレッドは1つのコアを共有する必要があったため、順番に処理しました。使用しているボックスには4つのvCPUがあります。クエリはシングルスレッドで、1つのリングと1つのポーリングスレッドを使用しており、ポーリングスレッドには常に空きコアがあったため、クエリとCPUを競合することはありませんでした。キャッシュミスSQポーリングオフで、両方のバックエンドでQ6を再度計測しました。io_uringで8.55秒、syscallで3.02秒でした。差は顕著です。その追加時間がどこに行くのかを理解するために、perfで命令数とキャッシュミス数をカウントしました。このテーブルのカウンターに関する注意点: これらは再構築されたホスト(アイドル時間のためにHetznerに支払いたくない人)からのものです。そのため、絶対値は上記のタイミングと一致しません。backend cycles (median, n=7) instructions (median, n=7) IPC cache misses miss rate io_uring plain 5.374 B 21.497 B 3.999 10.666 M 13.255% syscall 4.734 B 21.297 B 4.499 5.889 M 7.691% io_uringは0.2 B多く命令を実行しますが、これはほとんど何もありません。しかし、4.8 Mのキャッシュミスが増加します。両方のバックエンドはDMAを使用します。ディスクハードウェアは、CPUが作業を行わなくても、データをRAMに書き込みます。しかし、2つのバックエンドの間にも違いがあります。バッファリングされた読み取りでは、ディスクはデータをページキャッシュに書き込みます。2次に、カーネルはページキャッシュからプロセスバッファにデータをコピーします。このコピーは通常のCPU作業です。CPUはバイトを読み取り、どこかに書き込みます。コピーの副作用として、データはCPUキャッシュ(L1/L2/L3)に配置されます。しかし、O_DIRECTでは、ディスクはコピー手順なしで直接プロセスバッファにデータを書き込むため、CPUはこのプロセスに関与せず、何もCPUキャッシュにコピーされません。この説明は仮説です。カウンターは、io_uringがsyscallよりも多くのキャッシュミスがあることを示していますが、ミスを欠落したコピー手順にトレースしたことはありません。コストこれらすべてを踏まえて、各モデルのコストについていくつかの意見があります。O_DIRECTを使用し、アプリケーション側でリードヘッドを使用しないio_uringは、リングに単一のSQEで実行されます。1つのSQEは並行性がないことを意味し、並行性がないとブロックレイヤーはマージするものがありません。これは私の測定で最も遅い構成でした。sqpollは、マシンに複数のvCPUがある場合に合理的なモデルです。しかし、アプリケーションがリソースをどのように使用するかを理解する必要があります。アプリケーションがすでにCPU負荷が高い場合、ポーリングスレッドはCPUを競合します。その場合、sqpollをオンにする前に、カーネルスレッドに1つのvCPUを割り当てる影響を測定するのが良いでしょう。プレーン/デフォルトio_uringも合理的です。なぜなら、バッチ処理ができるからです。1つのio_uring_enter(2)のみが複数のSQEをバッチ処理します。SYSTOR論文ではこれを測定しました。キュー深度64で1.01システムコール/I/Oです。Tursoは現在バッチ処理しないため、ページごとに1つのシステムコールを支払います。サブミッションループは、保留中のすべての操作に対してsubmit_and_waitをすでに呼び出していますが、ページャーは1ページを要求してそれを待つため、保留中の操作は常に1つだけです。モデル以外に、コストに関する2つのことがあります。バッファリングされた読み取りでは、カーネルはページキャッシュからプロセスバッファにデータをコピーし、このコピーはCPUを使用します。コピーには副作用があります。データはCPUキャッシュに暖かく配置されます。O_DIRECTはコピーをスキップしますが、データはいずれCPUに到達する必要があります。バッファリングされた読み取りでは、コピー中に発生します。O_DIRECTでは、キャッシュミスとしてクエリ中に発生します。リードヘッドはいくらかの作業を無駄にします。Tursoは23,005個のSQEをさらにサブミットし、クエリが使用しなかったページを取得し、デバイスはより多くのバイトを読み取りました。しかし、追加のリクエストはキューをいっぱいに保ちます。それらがなければ、キューは1つのリクエストを保持する状態に戻るでしょう。ScyllaDBブログには、I/O内部に関する素晴らしい説明があります。https://www.scylladb.com/2024/11/25/database-internals-working-with-io/ ↩︎ユーザーランドディスクI/Oに関する興味深いブログ記事です。興味があれば。↩︎