Rustで最速のサーバソフトウェアを目指す!
最速を目指さないなら、なんのためのRustなのか!
メモリの安全性?信頼できる並列性?そんなものは飾り、最速あっての、Rust愛です。
サーバソフトウェアの実装を容易にする非同期処理(async/await)機能も充実してきて、「RustでHTTPサーバを書いてみよう」というような情報をたくさん見かけます。しかし、最速という観点が欠け、Rust愛が足りていないようです。
Webフレームワークなど、数多くのOSSソフトウェアに利用されている、最も人気の非同期ランタイムのTokioを試してみました。
Tokioを利用したサーバアーキテクチャ
Tokioの公式ガイドなどを見て、同じようなコードで、HTTPサーバのような、TCPプロトコルで多数のクライアントからのリクエストを処理するようなサーバソフトウェアを実装すると、下記の図ようなアーキテクチャでリクエストが処理されます。
CPU数とほぼ同じ数のスレッドが作られ、それぞれのスレッドが複数のクライアントを担当します。1つのスレッドが、epollシステムコールを使って、全てのクライアントのソケットと新たにクライアントを受け付けるListenソケットを監視、リクエストが届いたことを他のスレッドに通知します。このアーキテクチャには、以下のような問題があります。
- Listenソケットが1つしかないので、新たなコネクションのリクエストに最速で反応することができない。
- 1つのスレッドが、全てのソケットをepollで監視し、他のスレッドに通知するため、リクエストに最速で反応することができない。
最速のサーバアーキテクチャ
最速を目指したサーバアーキテクチャが下記の図です(独断)。これは、EnvoyやNginxで使われているはずです(Nginxはスレッドではなくプロセスを使う)。
CPU数とほぼ同じ数のスレッドが作られ、それぞれのスレッドが複数のクライアントを担当する点は、前のアーキテクチャと一緒です。大きな違いは、下記の2点です。
- それぞれのスレッドが、epollシステムコールを使って、担当するクライアントのソケットのみを監視する。
- SO_REUSEPORT機能を使い、同一ポートを待つListenソケットを複数作り、それぞれのスレッドが、新しいクライアントを受け付ける。
Linuxカーネルが新たに受け付けたソケットは、ハッシュ関数で、複数あるListenソケットのいずれかに割り当てられます。独自の割り当てアルゴリズムも、eBPFを使うことで実現できます。
まとめ
Tokioが最速を実現できないなら、非同期処理ランタイムを手作りしようと、ワクワクしているところでしょうか?
OS自作、結婚生活など、物事が楽しいのは、最初だけと、みなさんも経験から知っているはずです。Tokioも使い方次第で、最速のサーバアーキテクチャが実現できるので、ランタイムを自作する必要はありません。Tokioを使って、最速を目指すコードの例を置いておきます。