HTTP Serverを実行するための簡素な非同期ランタイムを作った
TL;DR
async/awaitの裏で何が起こっているのか理解するためにランタイムと簡素なHTTP サーバーを作った。
内容としては以下の2番煎じに近いがいくつか違う点がある。動機
最近詳解UNIXプログラミング 第3版を読む機会があり、その中でselectやpollというシステムコールを知った。そこで、理解を深めるために(C++で)selectを使った簡素なHTTPサーバーを作ってみた。ところで筆者はActixで簡単なHTTPサーバーを建てたことがある。そこでRustの非同期ランタイムはこのシステムコールを使っているのだろうかと興味が湧いた。そのため、(Actixで)簡単なHTTPサーバーを立て、straceで呼ばれているシステムコールを観察した。すると、前述のselectやpollではなくepollが使われていることが分かった。また、非同期ランタイムについて調べて以下の記事を見つけて、意外と簡単に作れそうな気がしてきたため自作してみた。
上記の記事との相違点
肝となるReactorやExecutorの設計などはRustのasync/awaitを使ったecho serverの実装と同じであるが以下の点が違う。
-
futuresクレートは使用していない
- RustのFutureとそのRunnerを作ってみたを参考にWakerも自作した
- システムコールを直接呼ばずに
std::net
やnixを使っている- このため直接
unsafe
を使っているのはWakerを作成するのに最低限必要なものだけになっているはず
- このため直接
- エラー型はthiserrorを使用している
- シグナル処理を追加している
- signalfdを使っている
- echoサーバーではなくHTTPサーバーである
- 準備が出来たファイル記述子に対し、
epoll_ctl(EPOLL_CTL_DEL, ..)
を呼んでいない。- このために
epoll
に登録しているファイル記述子とイベントをユーザランドで保持している - これにより
epoll_ctl
を呼ぶ回数が少なくなっている
- このために
動作確認
1GbpsのLANに繋がっているLinuxマシン2台を使った。1台は今回作成したサーバーを起動しておき、もう1台からcurlを使ってアクセスする。ちなみにcurlに--parallel-max
というオプションがあるのだが、これは300より大きい値を指定しても50になってしまう。負荷試験として1000コネクション程度並列でリクエストを投げてみたが、問題なく動いているようだった。また、ファイルサイズが数百KB程度あれば950Mbps程度出せることが分かった。さらに、950Mbps程度出していてもCPU使用率は1コアの30%程度しか使われないことも分かった。このため、epollがきちんと機能していると判断した。
感想
async/awaitは魔法ではないことがよく分かった。
Discussion