試行錯誤のおと

日々の試行錯誤した結果です。失敗することが多い記録、それだけでっす!

ISUCON6 本戦の Server-Sent Events を Rust で!

この記事は Rust Advent Calendar 2016 - Qiita の 20 日目の記事です。(3 日ぐらい遅れてしまいました。スミマセン><)

ISUCON6 本戦 Rust 実装

さて、以前に ISUCON6 で優勝したことを記事に書いたのですが、自分はメンバの中で実装が遅い & プログラミングよりインフラの方が得意ということで実際の競技では肝心のコードは触ってないです。

kizkoh.hatenablog.com

そこで、今回は復習と早くも来年の ISUCON7 (未定) に備え、練習も兼ねてアプリのチューニングに挑戦することにしました。 Go でチューニングに挑戦してもよかったのですが、今回は Rust Advent Calendar 2016 のネタと Rust でチューニングしていくためのスタートラインとして、 Rust 実装を用意することにしました。

リポジトリは以下になります。

github.com

webapp/rust に Rust の実装があります。動作を確認している環境は以下の通りです。

$ rustc --version
rustc 1.15.0-nightly (ac635aa95 2016-11-18)
$ cargo --version
cargo 0.13.0-nightly (806e3c3 2016-10-26)

# Linux でのみビルドと動作を確認しています。

ISUCON6 本戦の問題といえば、お題はお絵かきアプリでフロントエンドの NodeJS + React のバックエンドにアプリサーバがあり、フロントエンドのリクエストに対してバックエンドが Json を返すというモダンな構成の問題でした。

isucon.net

お絵かきの過程をほぼリアルタイムに共有するために、フロントエンドはバックエンドからの Server-Sent Events を受け取ってブラウザに応答を返します。

つまり、 Rust で Server-Sent Events を扱う必要があるのですが、実装について設計を考慮すべき箇所やハマりどころがあったため、今回取り上げて解説します。 (その他もハマりどころがあったのですが、コードから読み取れると思いますので、特段取り上げません)

Server-Sent Events の実装

Rust 実装を始める際、 WAF の選定から行いましたが、今回は軽量であるということと Hyper に強く依存していることから Hyper の API を扱いやすいという観点で、 nickel.rs を採用しました。

github.com

Server-Sent Events を実装しているエンドポイントは get_api_stream_rooms_id() 関数になります。

624 fn get_api_stream_rooms_id<'mw>(req: &mut Request, mut res: Response<'mw>) -> MiddlewareResult<'mw> {

https://github.com/kizkoh/isucon6-final/commit/d76b3c5d09035c0384fd43052520d566e8e6534c#diff-792cca0794d088b75ffbaef6e6aa8715R624

Server-Sent Events ではストリームに対して書き込みを行います。

nickel.rs では Write トレイトを実装する Response<'a, D, Streaming> から write(&mut self, buf: &[u8]) メソッドを呼び出すことで、 Hyper を経由してストリームに書き込みを行うことができます。 Response<'a, D, Streaming> を得るには Response<'a, D, Fresh> から start(self) メソッドを呼び出して、型を変更します。

694     let mut streaming = match res.start() {
695         Ok(streaming) => streaming,
696         Err(_e) => {
697            let _result = writeln!(&mut std::io::stderr(), "nickel streaming start error");
698            exit(1);
699         }
700     };

https://github.com/kizkoh/isucon6-final/commit/d76b3c5d09035c0384fd43052520d566e8e6534c#diff-792cca0794d088b75ffbaef6e6aa8715R694

しかし、この時点で res の move が発生するため、以降 res を参照することができなくなります。 つまり、ストリームには書き込めるのですが res を使って HTTP レスポンスを返すことができません。 そのため、他の実装ではエラー処理の際に HTTP レスポンスを返す箇所で Rust 実装では HTTP レスポンスを返すことができていません。 この問題は今後の課題です。

ストリームに書き込めるようになったところで、実際の Server-Sent Events 実装 について触れていきます。 Server-Sent Events の実装に利用するメソッドは write(&mut self, buf: &[u8]) メソッドと flush(&mut self) メソッドになります。 これらのメソッドをまとめ、エラー処理を含んだ処理が print_and_flush<'mw, T: AsRef<str>>(mut streaming: Response<'mw, (), Streaming>, msg: T) 関数になります。 なお、write, flush はいずれの処理も move するため、 streaming を return することで所有権の返却を行い継続してストリームに対して書き込みできるようにしています。

704     streaming = print_and_flush(streaming, msg).ok().unwrap();

https://github.com/kizkoh/isucon6-final/commit/d76b3c5d09035c0384fd43052520d566e8e6534c#diff-792cca0794d088b75ffbaef6e6aa8715R704

最後にこのエンドポイントでは以下のようにして bail() を呼び出して、ストリームを中断しています。

777     streaming.bail("")
778     // Ok(Action::Halt(streaming))

https://github.com/kizkoh/isucon6-final/commit/d76b3c5d09035c0384fd43052520d566e8e6534c#diff-792cca0794d088b75ffbaef6e6aa8715R777

Ok(Action::Halt(streaming)) を return することで、ストリームを終了することもできるのですが、これではストリームを再開する時に Last-Event-ID がリクエストのヘッダに付与されないため、 bail() を呼び出して中断しています。

ここまでが Server-Sent Events の実装の一連の流れです。

まとめ

この記事では Rust で Server-Sent Events を扱う方法について触れました。

内容としては前節でまとまっているので、Rust 実装について少し触れておきたいと思います。 今回やったこととしては他言語の実装を Rust 実装に置き換えるだけなのですが、所有権や Trait という概念がある以上、やはり他の一般的な言語間での移植以上には時間がかかりました。 しかし、これらの概念を理解するためには、もとのコードも洗練されておりとても良い課題だったかと思います。 Web アプリケーションを Rust に移植するなどや ISUCON6 の他言語の実装を参考に Rust を学習される方がおられましたら、自分のコードが役に立てば幸いです。

また、この記事ではパフォーマンスについて触れることはできませんでしたが、トップページのロードは Go 実装より Rust 実装の方が高速のようです。 簡単な計測としてキャッシュを無効にしてブラウザのプロファイリング結果だけ見たところ、 Go 実装で 7.5s 前後だったロード時間が Rust 実装だと 6.7s 前後に短縮される結果が得られました。

この結果を踏まえ次回以降の記事では、さらにチューニングやプロファイル過程を紹介できればと思っています。

この記事は Rust Advent Calendar 2016 - Qiita の 20 日目の記事として書かれました。 21 日目は 11Takanori さんで RustでCSVを操作する - Qiita です。