ソケットAPIが遅すぎる?新たなio_uringを試す!

FUJITA Tomonori
nttlabs
Published in
4 min readNov 30, 2020

新しいAPIが作られるたびに、私たちは、古いAPIを置き換えるだけで高速化という夢をみます。何度夢破れても、高速なAPIが追加されたと聞けば、試さずにはいられませんよね!

今回は、Linuxカーネル5.1で追加されたio_uringを使って、Rustのasyncランタイムを実装し、gRPCサーバのベンチマークを実行してみました。

io_uringとは

io_uringは、ファイルシステムとネットワークの非同期I/Oのために開発されました。同期よりも非同期のほうがおしゃれ、そういう雰囲気ありますよね!クラウドネイティブも、非同期にAPIを介して、なんかやってるやつですよね。

io_uringのインターフェイスは、高い性能を目指し、1)アプリケーションとカーネル間でのメモリコピーを避ける、2)複数のI/O要求を一度にカーネルに伝えることができる、という工夫がされています。

下図のように、アプリケーションとカーネルは、アプリケーションがI/O要求を伝えるためのSubmission Queue(SQ)、I/O要求の完了結果を受け取るためのCompletion Queue(CQ)、という2つのリングバッファを共有します。例えば、アプリケーションがソケットからリードするためには、ファイルディスクリプタとバッファ、その長さなどの情報で、SQの末尾(tail)の位置のエントリを更新し、システムコールでカーネルに通知します。カーネルは、I/Oが完了したら、その結果をCQの末尾(tail)の位置のエントリに保存します。アプリケーションはシステムコールを使うことなく、CQのエントリをチェックし、I/Oの完了結果を得ることができます。また、アプリケーションはシステムコールを使って、カーネルが新たなI/O完了結果をCQに追加するまで待つこともできます。

io_uringのインターフェイス

Rustのasyncランタイム実装と性能

以前実装した、epollを使ったRustのasyncランタイムを、io_uringを使うように変更し、両者の性能を比較した結果が下図になります。

Throughput (requests per second)

epoll版の方が早い!また、私たちの夢は破れたのでしょうか。

ランタイムのI/O処理

2つのソケットを並列処理しているアプリケーションがソケットからリードを試みた際の、epoll版とio_uring版ランタイムとの動作の違いを考えてみます。

epoll版ランタイムは、ノンブロッキングなソケットを利用し、リードを試みて、データがなければエラーを受け取り、I/O完了時に再開するための準備をして、もう一方のソケットからリードを試みます。ベンチマークのような過負荷な状態では、リードを試みた際に、たいてい、データが届いており、完了時のための再開準備をせずに、即時、ソケットからリードしたデータを処理し、レスポンスをライトするなどの処理を継続することができます。

io_uring版は、ソケットにデータが届いているかどうかに関係なく、常に、I/O要求をSQに追加し、完了時の再開準備をします。ランタイムは、もう一方のソケットのリードを試みて、また、I/O要求をSQに追加、完了時の再開準備をして、CQをチェックし、要求が完了したソケットの処理を再開します。すでにデータが届いているケースでは、epoll版の方が処理が軽そうですね。

まとめ

今回は評価していませんが、io_uringは、非同期I/Oの完了ではなく、epoll同様、ソケットが読み書きできる状態になったことをCQで通知する機能をサポートしており、epoll版ランタイムと同程度の性能は実現できそうです。また、アプリケーションとランタイムをio_uringに最適化した設計に変更すれば、epoll版ランタイムの性能を超えることもできるかもしれません。

何度夢破れても、夢をみたい。他のエンジニアに夢をみせたい。NTTでは、そんな夢みがちな仲間を募集中です。連絡お待ちしています。

--

--

FUJITA Tomonori
nttlabs

Janitor at the 34th floor of NTT Tamachi office, had worked on Linux kernel, founded GoBGP, TGT, Ryu, RustyBGP, etc. https://twitter.com/brewaddict