Rustのasyncでgoroutineの速度に勝つ

FUJITA Tomonori
nttlabs
Published in
4 min readSep 16, 2020

RustのgRPCの速度がGoよりも遅いため、デファクトの並列処理方法のasyncではなく、システムコールを直接使うという歪んだ愛の形で、Goよりも高速化を達成した前回

私たちのRustへの愛は裏切られたのか?私たちがRustの愛を裏切ったのか?2つの思いの間を行き来しながら、asyncでも、goroutineよりgRPCを高速に実行できるか試してみましょう。

システムコールよりもランタイム

前回は、ソケットをブロックしないように設定し、epollシステムコールでイベントを待つような、C言語のような実装でした。クラウドネイティブ時代に、Rustに加えて、そんな知識を習得している時間はないですよね!語り部を目指しているなどの特殊な理由で、epollについて知りたい場合は、前時代的な人に聞けば、Kqueueとepollの戦い、などの物語を朝まで聞くことができるでしょう。

非同期プログラミングの処理機能(ランタイム)は、上記のC言語のような部分を実装するので、私たちは、async/awaitというわかりやすいインターフェイスを使って、価値を生むロジックの実装に集中することができます。今回は、価値を生まないHTTP/2とgRPCサーバというロジックに集中することができますね。

性能測定結果

今回のベンチマークが必要とする必要最低限の機能を持つランタイムを実装し、前回同様、gRPCクライアント数に対する、1秒あたり処理したリクエスト数を測定しました。実装したライタイムは、前回実装したシステムコールを直接使う実装と、同様の性能を達成できました!

Throughput (requests per second)

前回の実装と比較して、async/awaitインターフェイスを実現するためのオーバーヘッドを懸念していましたが、今回の条件では問題にならなかったようです。

今回の実装が、tonicやgrpc-goよりも、高い性能を実現している理由は、解析しないと分かりませんが、ランタイムが、マルチプロセッサ間で何も共有していない、シェアード・ナッシング設計が理由の1つかもしれません。CPUごとにスレッドが作られており、全てのスレッドが、acceptシステムコールを介して、gRPCのクライアントを受け付けることができます。一旦、スレッドがクライアントを受け付けると、他のCPUで動いているスレッドと干渉することはありません。

tonicが使うtokioは、スレッドがアイドル状態になると、他のCPUで動作するスレッドから実行待ちのタスクを奪い、できるだけ多くのCPUが実行状態になるように設計されています。Goも同様の戦略で、goroutineを実行しています。リソースを安全に共有するため、スレッド間で、ロックなどの調停が必要になります。サーバでは、シェアード・ナッシング設計が有効で、C++のフレームワークなどでも採用されています。

ランタイム自体の性能だけでなく、async/awaitインターフェイスを介したランタイムの呼び出し方も性能に影響しそうです。ランタイムの呼び出しはシステムコールの実行につながるので、その呼び出し回数ができるだけ少なくなるように設計するのがよさそうです。

まとめ

Rustでは、想定するワークロードに最適なランタイムを実装できることが分かりましたね!今回のランタイムは、限定された機能のため、200行程度です。ファイルシステムのI/O、Goのチャネル相当、タイマーなど、様々な機能が必要な場合は、その難しさに泣きながら実装するか、tokioなどのランタイムを利用しましょう。

あと、なぜか、クライアント数が、3,000〜12,000の条件で、全ての実装の性能が前回の結果よりも高いので、どなたか調べて理由を教えてください。

--

--

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