Development
16 min read
42 views

Beyond epoll: Linux io_uringを用いた超低遅延リバーストンネルの設計

IT
InstaTunnel Team
Published by our engineering team
Beyond epoll: Linux io_uringを用いた超低遅延リバーストンネルの設計

Quick answer

Beyond epoll: Linux io_uringを用いた超低遅延リバーストンネルの設計: localhost tunnel answer

A localhost tunnel gives your local app a public HTTPS URL without opening router ports, which is useful for demos, QA, mobile testing, and provider callbacks.

How do I expose localhost without opening ports?

Use a reverse HTTPS tunnel. Your machine connects outbound to the tunnel service, and the public URL forwards requests back to your local app.

When should I use a localhost tunnel?

Use one for webhook testing, OAuth callbacks, client demos, QA previews, mobile device checks, and short-lived development reviews.

長年にわたり、Linux上の高性能ネットワーキングの基盤は疑う余地のないものでした。Webサーバーやロードバランサー、逆プロキシを構築し、悪名高いC10K(後にC10M)問題に対処しようとした場合、epollを使用していたでしょう。NGINXやHAProxy、Envoyといった業界大手は、このイベント駆動型の準備完了モデルの上に構築され、その堅牢性を世界中で証明しています。しかし、2026年において高スループットなローカルインゲスの境界を押し広げる中で、epollアーキテクチャの欠陥が明らかになってきました。

問題はもはや何千もの接続を管理することではなく、コンテキストスイッチのコストの苛烈さにあります。ネットワーク速度が数百ギガビット毎秒で測定され、レイテンシ予算がマイクロ秒単位に縮小されると、ユーザースペースとカーネルスペース間の連続的な振動は、致命的なCPUボトルネックとなります。

そこで登場するのがio_uringです。これは過去10年で最も重要なLinux I/Oの進歩です。最新のトンネルバイナリやリバースプロキシをこの高度な非同期I/O APIに移行することで、共有メモリリングバッファを活用し、システムコールのオーバーヘッドをホットパスから排除しています。このパラダイムシフトにより、数百万の多重化されたパケットをほぼゼロCPUスパイクで管理できる超効率的なネットワークプロキシの構築が可能になっています。

この記事では、epollio_uringを用いたネットワーキングの技術的な違いを解説し、io_uringを基盤とした非同期Linuxトンネルの動作原理、なぜio_uringリバースプロキシが高スループットなローカルインゲスアーキテクチャの設計を根本から変えているのか、そして2026年におけるセキュリティやエコシステムのトレードオフについて詳述します。


epollのボトルネックの構造

なぜio_uringリバースプロキシが大きな飛躍をもたらすのか理解するには、まずepollが超高スループットで失敗する理由を解き明かす必要があります。

簡単な歴史

epollはLinuxカーネル2.5.44で2002年10月に導入され、従来のselect()poll()のスケーラブルな代替として登場しました。従来のシステムコールは監視するファイルディスクリプタの数が増えるとO(n)の時間を要しましたが、epollは準備完了通知においてO(1)の動作を実現し、高並行性サーバーにとって重要な改善をもたらしました。これによりC10K解決策の時代を支え、今日のほぼすべての高性能サーバーランタイム(libuv(Node.js)、標準のGoネットワークポーラー、NGINXやHAProxyのイベントループ)に採用されています。

準備完了のパラダイム

epollは*準備完了通知*の仕組みです。逆プロキシが何千ものクライアントソケットを管理する際、カーネルに対して「これらのファイルディスクリプタのうち、データを読む準備ができているもの、または書き込みバッファの空きがあるものはどれか?」と問い合わせます。

典型的なepollを用いたプロキシの流れは次の通りです:

  1. プロキシがepoll_wait()を呼び出し、1つ以上のソケットが準備完了するまで待機(コンテキストスイッチ:ユーザースペース → カーネル → ユーザースペース)。
  2. カーネルが準備完了したファイルディスクリプタのリストを返す。
  3. 各準備完了ソケットに対し、read()write()のシステムコールを発行(コンテキストスイッチ:ユーザースペース → カーネル → ユーザースペース)。
  4. ソケットがEAGAINを返した場合(操作がブロックされる場合)、次のepoll_wait()サイクルまで待つ。

コンテキストスイッチの隠れたコスト

epoll_waitは従来よりもスケールしますが、実際のI/O操作は依然として個別のシステムコールを必要とします。read()write()accept()close()のたびにCPUのコンテキストスイッチが発生します。スイッチ中、CPUはユーザースペースのレジスタ状態を保存し、TLBの特定のエントリをフラッシュし、カーネル空間にジャンプし、ポインタを検証し、操作を実行し、再びジャンプバックします。リクエストが1万件/秒の場合、このオーバーヘッドは無視できるレベルです。しかし、100万パケット/秒になると、CPUプロファイルの大部分を占めるようになります。高並行性の環境では、プロキシは実際のアプリケーションロジックよりも多くのCPU時間をユーザースペースとカーネル空間間の移動に費やすことになります。

さらに、epollはネットワークI/OとファイルI/Oを厳密に分離しています。Linux上の通常のファイルは常にepollによって「準備完了」とみなされますが、ディスク上のファイルに対する読み込みは依然としてブロックする可能性があります。これにより、ファイルI/O用の別スレッドプールを維持しながらepollループと並行して動作させる必要があり、ミューテックスの競合やメモリオーバーヘッド、調整の複雑さを招きます。


io_uringの登場:非同期I/Oの再定義

Facebook(現Meta)の Jens Axboe によって開発されたio_uringは、2019年5月にLinuxカーネルメインラインにマージされました。これは、従来のLinux AIOインターフェースの制限を解決するために設計されました。従来のAIOは直接I/Oのみをサポートし、非決定的なブロッキング動作に悩まされ、1つのI/O操作に対して少なくとも2つのシステムコールを必要としました。io_uringは、準備完了モデルを完全に捨て、*完了モデル*に置き換えています。

io_uringを使ったアプリケーションは、「このソケットは準備完了か?」と尋ねるのではなく、「バッファを渡すので、このソケットからデータを読み込み、その完了を通知してください」と伝えます。

共有メモリリングバッファ

io_uringの魔法は、その名前の通り、共有メモリリングバッファにあります。io_uring_setup()を通じてインスタンスを初期化すると、ユーザースペースとカーネルの両方からアクセス可能な2つのリングバッファ(円形)を作成します:

  • Submission Queue (SQ): ユーザースペースのアプリケーションがここにSQE(Submission Queue Entry)を書き込みます。SQEはI/O操作を記述します:readvwritevaccept、タイマーなど。
  • Completion Queue (CQ): カーネルがここにCQE(Completion Queue Entry)を書き込みます。I/O操作が完了すると、結果(読み書きされたバイト数やエラーコード)がCQにプッシュされます。

これらのキューは共有メモリに存在するため、アプリケーションは多くのI/O操作をシステムコールなしでキューに入れることが可能です。キューが満たされたら、io_uring_enter() syscallを1回呼び出してカーネルに処理を開始させます。カーネルはリクエストを非同期に処理し、結果を直接CQに書き戻します。

操作をバッチ処理することで、io_uringはシステムコールのコストを多くのI/O操作に分散させて即座に吸収します。高性能な非同期Linuxトンネルにとって、これは大きな改善です。しかし、io_uringはこれだけにとどまりません。

統一されたI/Oモデル

epollと異なり、io_uringはネットワークI/OとファイルI/Oの両方に対して単一のインターフェースを提供します。プロキシはソケットの読み取り、書き込み、sendfile操作、ディスクの読み取りをすべて同じリングに送信し、結果を同じCQで受け取ることができます。これにより、ファイルI/Oを処理するための別スレッドプールが不要となり、ネットワークストリームとローカルファイルキャッシュの両方を扱うプロキシのアーキテクチャが大幅に簡素化されます。


ほぼゼロシステムコールのネットワークプロキシ設計

io_uringを用いたプロキシ設計者の目標は、システムコールの削減、特にホットパスでのほぼ排除です。これを可能にするのがIORING_SETUP_SQPOLLというフラグです。

SQPOLL:io_uring_enterをバイパス

IORING_SETUP_SQPOLLを用いてio_uringインスタンスを初期化すると、Linuxカーネルはそのリング専用のカーネルスレッドを生成します。このスレッドは、io_uring_enter()によるウェイクアップを待たずに、共有されたSubmission Queueを継続的にポーリングします。

このモードでのプロキシの動作は次の通りです:

  1. プロキシアプリケーションがネットワーク操作(読み取り、書き込み、accept)を共有メモリのSQに書き込む。
  2. io_uring_enter()を呼び出さずに済む。
  3. 専用のカーネルスレッドが新しいSQEsを即座に検知し、ネットワーク操作を実行。
  4. 結果をCQに書き込み。
  5. プロキシアプリケーションが結果を共有メモリのCQから読み取る。

データの送受信が継続している間、各I/O操作ごとにコンテキストスイッチをトリガーしません。アプリケーションはユーザースペースに留まり、操作を共有メモリに投入し、結果を読み出すだけです。残りはカーネルスレッドが処理します。

重要なポイントとして、カーネルのポーリングスレッドが一定時間アイドル状態になると(sq_thread_idleでミリ秒単位に設定)、io_uring_enter()を通じて再び起動させる必要があります。継続的な負荷下にあるプロキシは、このスレッドを無期限に維持し、コストを完全に回避できます。

特権要件は、SQPOLL導入以降大きく変化しています。初期のカーネルはCAP_SYS_ADMINを必要としましたが、カーネル5.11ではCAP_SYS_NICEに緩和され、2023年のカーネル5.13以降は、現代のカーネルでは特権不要となり、コンテナなどの高権限を必要としない展開も現実的になっています。

固定バッファと登録済みファイル

残るオーバーヘッドを排除するために、最新のio_uringプロキシはio_uring_register_buffers()io_uring_register_files()を使用します。従来のepollプロキシでは、read()write()のたびにカーネルがユーザースペースのメモリポインタを変換し、ファイルディスクリプタテーブルを参照していました。登録済みの固定バッファやファイルを事前に登録することで、メモリページをピン留めし、ファイルディスクリプタのマッピングをキャッシュします。これにより、SQEを登録済みバッファインデックスで送信した場合、カーネルは個々の操作のルックアップをスキップし、NICとユーザースペースメモリ間の直接DMAパスを可能にします。

マルチショットaccept

逆プロキシにとって非常に強力な機能は、Linux 5.19で導入されたIORING_OP_ACCEPTIORING_ACCEPT_MULTISHOTフラグです。従来のaccept()は、新しい接続ごとに再送信が必要でしたが、マルチショットacceptでは、1つのSQEが新しい接続ごとにCQEを継続的に生成し続け、再送信の必要がありません。数百万の短命な接続を扱う高並行性のインゲスプロキシにとって、これにより再送信のオーバーヘッドが排除されます。


ゼロコピー・ネットワーキング:Linux 6.15の最前線

io_uringネットワーキングにおける最も重要な最近の進展は、2025年のLinux 6.15で登場したネイティブなゼロコピー受信(ZC Rx)です。それ以前は、io_uringはゼロコピー送信(ユーザースペースバッファからNICへ直接データ送信)をサポートしていましたが、受信はカーネルからユーザースペースへのコピーが必要でした。

新しいZC Rx機能は、ハードウェアの受信キューを設定し、受信パケットのペイロードを直接ユーザースペースメモリにDMAします。カーネルはTCP/IPスタックを通じてパケットヘッダを処理しますが、ペイロードデータはカーネルメモリに触れません。ソケットからの「読み取り」は、データが既に到着した場所をカーネルが通知する仕組みとなります。この機能を用いたデモでは、200 Gbit/sのリンクを単一のCPUコアで飽和させることに成功しました。

これにより、io_uringを用いたプロキシは新たな段階に進みます。単なるシステムコールのオーバーヘッド削減だけでなく、ハードウェアレベルでの真のゼロコピー受信への道を開き、DPDKのようなフレームワークの複雑さを排除します。


2026年の非同期Linuxトンネル:Rustとランタイムの変遷

io_uringへの移行は単なるカーネルAPIの置き換えではなく、プロキシの内部ランタイムの再考を要求します。epollは準備完了に依存しているため、従来のプロキシはソケットが準備完了した瞬間にだけポインタを渡していました。一方、io_uringでは、*バッファ所有権の移譲*(「バッファレンタル」とも呼ばれる)にモデルが変わります。カーネルが非同期に読み書きを行うため、操作完了までメモリバッファの所有権を保持し続ける必要があります。もしプロキシがバッファを変更または破棄すると、メモリ破損が発生します。

Rustのスレッド・パー・コアランタイム

io_uringを最大限に活用するため、現代のトンネルバイナリの開発者はRustと、スレッド・パー・コアの専用ランタイムを採用しています。代表的な選択肢は次の通りです:

  • Monoio(ByteDance / CloudWeGo):純粋なio_uring/epoll/kqueue Rust非同期ランタイムで、スレッド・パー・コアモデルを採用。Linuxカーネル5.6以降でio_uringサポートを必要とし、バッファレンタルをネイティブに実装。ByteDanceのベンチマークでは、MonoioベースのゲートウェイはNGINXより最大20%高速で、RPC実装ではTokioベースに対して26%の向上を示しています。
  • Glommio(元々Datadog):io_uringを基盤とした協調型スレッド・パー・コアランタイムで、カーネル5.8以降と512 KiB以上のロック済みメモリ(RLIMIT_MEMLOCK)を必要とします。特徴的なのは、各スレッドに対して3つのio_uringインスタンス(メイン、レイテンシ重視、ポーリング)を作成し、遅延とスループットのバランスを細かく制御できる点です。

両者とも共有しないモデルを採用し、CPUコア間の作業の奪い合いを排除しています:

  • ワークスティールなし:各CPUスレッドは独立したio_uringインスタンスを持ち、自身のクライアント接続のサブセットを管理。
  • バッファレンタル:プロキシはメモリバッファの所有権をランタイムに「レンタル」し、ネットワーク読み取り完了時に所有権を返却。
  • キャッシュ局所性:タスクはCPUコア間を移動しないため、L1/L2キャッシュが高効率に維持され、ミューテックスやロックのオーバーヘッド、スレッド間同期も不要。

静的HTTPファイルサーバのベンチマークでは、io_uring Rustランタイムは単一スレッドで約656,000 req/s、標準のTokioは約399,000 req/sであり、約64%の差があります。4スレッド時には、io_uringランタイムは1.1百万req/sを超え、tokio-uringやMonoioがリードしています。4スレッド構成では、io_uringベースのランタイムはGoのfasthttpを約2.3倍上回ります。

インゲストンネルのシナリオでは、ローカルプロキシが受信した multiplexed ストリーム(HTTP/3 over QUICなど)を復号し、ローカルマイクロサービスに転送する場合、このアーキテクチャは特に効果的です。単一のリングが外部からのネットワーク読み取り、ローカルサービスへの書き込み、キープアライブ用のタイマーイベントをバッチ処理します。

エコシステムの現状

エコシステムの成熟度について正直に述べると、Glommioは元々Glauber Costa氏がDatadogで開発し、その後開発が縮小しています。Monoioはパッチを受け取り動作していますが、新しいio_uring機能のAPIサポートは進化するカーネルインターフェースに追いついていません。Apache Iggyのような高性能メッセージブローカーは、2026年初頭にio_uringのスレッド・パー・コアアーキテクチャへの移行を詳細に記録し、実際に困難に直面しています。Rustランタイムはio_uringの primitives(リクエストチェーン、ワンショット受信/送信API、登録バッファプール)を完全には公開しておらず、Cレベルのliburingに比べて制約があります。

エコシステムは成熟しつつありますが、io_uringの最先端機能をフルに活用したい開発者は、liburingのC APIにより近いレベルで作業する必要があるかもしれません。


epollとio_uringのネットワーキング:実世界のニュアンス

epollは死んだのか?絶対に違います。標準的なWebサービスやAPI、低〜中程度のトラフィックアプリケーションにおいては、epollは成熟し、既存のランタイムに深く組み込まれ、十分に機能しています。Node.js(libuv)や標準のGoはepollを内部で使用し、毎日何百万もの運用負荷を処理しています。

io_uringの採用が最も効果的となるのは、次のような特定の運用閾値です:

  • 非常に高いリクエストレート:システムコールのオーバーヘッドが明確なCPU負荷となる場合。
  • 混合I/Oワークロード(ネットワーク + ディスク):統一された非同期モデルがアーキテクチャを簡素化。
  • テールレイテンシの敏感さepollのスレッドモデルがスケジューリングのジッターを引き起こす場合。
  • ゼロコピー受信パス:NICハードウェアとカーネル6.15+がサポートする直接DMAによるユーザーメモリへの受信。

Red Hatの開発者ドキュメントはこの点について適切に評価しています:io_uringはファイルI/Oにおいて明らかな効果を発揮していますが、ネットワークI/Oにおいては、既にノンブロッキングAPIを持つため、利点はシステムコールに縛られた負荷次第です。実際の条件下でベンチマークを行い、アーキテクチャの変更前に評価してください。


セキュリティの観点:二律背反のインターフェース

本番環境におけるio_uringの議論は、そのセキュリティ面を無視できません。2023年6月、Googleのセキュリティチームは、2022年に提出されたカーネルバグバウンティの60%がio_uringに関連していたと報告しています。Googleはio_uringに関する脆弱性に対し約100万ドルの報酬を支払っています。その結果、Androidではサードパーティアプリ向けに無効化され、ChromeOSでは完全に無効化され、Googleの本番サーバーでは制限されています。

注目すべきCVEは以下の通りです:

  • CVE-2021-41073:不適切なメモリ管理によるローカル権限昇格
  • CVE-2023-2598:範囲外アクセスによるLPE
  • CVE-2023-21400:カーネル5.10のダブルフリー脆弱性、Google Pixel 7でのPoCで成功例あり

攻撃の表面積は、io_uringの複雑さと従来の監視を回避できる能力に由来します。EDRツールやシステムコールを捕捉する侵入検知システムは、read()write()sendmsg()recvmsg()をインターセプトしても、リングバッファを通じて動作するプロセスには効果的に盲目です。straceのような標準ツールも、アクティブなデータパス中は沈黙します—システムコールが発生しないためです。

本番展開において推奨される対策は次の通りです:

  • カーネルレベルのio_uringトレースポイントを計測するeBPFツールを使用し、システムコールのインターセプトに頼らない。
  • /proc/sys/kernel/io_uring_disabledio_uring_groupを用いて、マルチテナント環境でのio_uringインスタンス作成を制限。
  • パッチ済みのカーネルバージョンに展開を限定し、5.15 LTSや6.x LTSのセキュリティバックポートを適用。
  • セキュリティポリシー(seccomp)でio_uringアクセスを明示的に制御。

io_uringの課題と対策

その強力さにもかかわらず、io_uringリバースプロキシの設計には特有のエンジニアリング課題があります:

カーネル依存性。 io_uringはバージョン5.1で登場しましたが、重要なネットワーク機能はその後のリリースで追加されました:マルチショットaccept(5.19)、特権不要の信頼性の高いSQPOLL(5.13)、ゼロコピー送信(5.15)、ゼロコピー受信(6.15)です。古いカーネル(例:RHEL 8.xの4.18)を搭載したエンタープライズLinuxで高度なio_uringトンネルを展開すると、epollへのフォールバックや機能喪失が発生します。ターゲットのカーネルバージョンは明示的に設定すべきです。

メモリ消費。 リングバッファやピン留めされた固定バッファはロックされたカーネルメモリ(RLIMIT_MEMLOCK)を必要とします。Glommioは最低512 KiBのメモリを推奨しています。多くのリングを用いると、OOMのリスクやシステムチューニングが必要となります。各SQPOLLリングは専用のカーネルスレッドも占有し、CPUコアを消費します。

順序とシリアル化。 CQEは順不同で到着することもあり、SQEが逐次的に送信されていても(IOSQE_IO_LINKIOSQE_IO_HARDLINKで明示的にリンクしない限り)、ストリーム指向のTCPソケットでは複数の送信や受信を明示的な順序付けなしに行うと、安全ではありません。カーネルはポールのアーム中に実行順序を再調整する可能性があります。リクエストのuser_dataポインタを追跡する必要があります。

デバッグの不透明さ。 straceのような標準ツールは、ホットパスのゼロシステムコールプロキシにはほぼ無力です。デバッグにはbpftraceやカスタムeBPFプログラムを用いて、io_uringの内部トレースポイントを直接監視し、リング状態やカーネルワーカースレッドを検査する必要があります。これは運用上の負荷となります。

キャンセレーションの安全性。 非同期操作中にバッファをドロップまたは再利用すると、メモリ破損を引き起こします。Rustの所有権モデルやMonoio、Glommioのバッファレンタルはこれに対処しますが、アプリケーションコードがこれらの抽象化に正しく構造化されている必要があります。


2026年に注目すべきポイント

io_uringの動向は今後も急峻です:

  • PostgreSQL 18は、データとWALのI/Oに対してオプションのio_uringバックエンドを導入し、早期ベンチマークではコールドスキャンの速度が3倍に向上し、登録済みバッファとSQPOLLを有効にした場合は11〜15%の総合的な向上を示しています。
  • NICレベルでのゼロコピー受信(Linux 6.15)は、ペイロードデータをNICから直接アプリケーションメモリに移動させるアーキテクチャの扉を開きます。
  • カーネル7.0(2026年4月リリース)は、IORING_SETUP_NO_SQARRAYモードとIORING_SETUP_LINEAR_SEQNOを導入し、小さく頻繁なバッチ送信のためにSQEsをL1キャッシュに保持する非循環キュー方式を採用しています。

結論:変革への慎重な評価

epollアーキテクチャは20年以上にわたり大きな価値を提供してきましたが、陳腐化しているわけではありません。ただし、NVMeアレイが何百万のIOPSを提供し、400 Gbit/sのNICがデータセンターの現実となる高スループットなローカルインゲスの最前線で運用するチームにとっては、準備完了モデルのシステムコールコストは明確なボトルネックです。

io_uringのカーネルAPIは、そのまま置き換えではなく、ユーザースペースとカーネルスペースの通信方法を根本的に再構築します。共有メモリリング、バッファレンタルのセマンティクス、SQPOLLモード、そしてハードウェアレベルのゼロコピー受信は、epollベースの設計では実現できないパフォーマンスを引き出すアーキテクチャを提供します。

ただし、いくつかのトレードオフも存在します:カーネルバージョンの要件、複雑で進化し続けるセキュリティリスク、straceの可視性の制限、そしてRustエコシステムのランタイムがio_uringの最深部の機能を完全に公開していない点です。これらは、計画的に進め、最新のLTSカーネルをターゲットにし、eBPFを用いて計測し、実負荷下でのプロファイリングを行えば、問題にはなりません。

超高スループットなローカルインゲスや逆トンネル、低遅延APIゲートウェイのインフラを構築している場合、io_uringは真剣に検討すべきです。すべてのワークロードに適しているわけではありませんが、その適合する場合、epollとの差はわずかではなく、アーキテクチャの差となります。

Continue from this article into the most relevant product guides and workflows.

Related Topics

#io_uring reverse proxy, epoll vs io_uring networking, asynchronous Linux tunnel, zero-syscall network proxy, high-throughput local ingress, Linux kernel networking, io_uring API, replacing epoll proxy, ultra-low latency ingress, shared memory rings linux, asynchronous I/O devops, reducing proxy CPU overhead, high-performance reverse tunnels, linux systems engineering 2026, user space context switching, eradicating CPU bottlenecks, massive concurrency network proxy, multiplexed packet routing, io_uring performance tuning, advanced linux proxy architecture, software-defined network ingress, edge proxy optimization, next-gen reverse proxies, linux network stack tuning, C10M problem io_uring, high-throughput tunneling binaries, local proxy syscall overhead, devsecops infrastructure scaling, asynchronous system calls, kernel-level packet processing

Keep building with InstaTunnel

Read the docs for implementation details or compare plans before you ship.

Share this article

More InstaTunnel Insights

Discover more tutorials, tips, and updates to help you build better with localhost tunneling.

Browse All Articles