試行錯誤のおと

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

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 です。

Gentoo な Thinkpad X1 がぶっ壊れたので MacBook Pro に乗り換えた

先週まで会社の業務で出張に出ていたのだけど、出張先で PC が壊れてしまった。 使っていた PC は Thinkpad X1 carbon (1st Gen) 、 2012 年のモデルなので相当古い。 どういう状態かというと、バッテリ駆動しない(出張中に突然なった) + スピーカーの認識が悪くてときたま X11 を巻き込んでクラッシュする。

バッテリ駆動しない状態になるとデスクトップと変わらず、ラップトップとして役割を果たさないので単純につらい。 普段からただただ重い荷物を持って通勤しないといけないし、インフラエンジニアとして緊急時には対応しないといけないので仕事にも支障が出る。

というわけで、会社で支給されている MacBookPro 2015 に移行した*1。 しかし、 Thinkpad X1 には Gentoo Linux を入れて利用していたのでそう単純な話ではなく、というところで..。 そう、ここ 3 日くらい MacBook Proを捨ててThinkpad T460sを買ってgentooを入れた - joker1007の日記 のエントリーと逆のことをやってたのです!?
(僕が箱根をノロノロとヒルクライムしている間にこんな面白いことをされてたなんて!) d.hatena.ne.jp

スペック

使っていた Thinkpad X1 は 2012 年のもので 4 年もの。 スペックを比較するとリッチになったぞ!!

  • CPU: Intel Core i5 2.4 Ghz (Ivybridge) -> Intel Core i5 2.7Ghz (Broadwell)
  • RAM: 8 GB -> 16 GB
  • SSD: 256 GB (SATA) -> 256 GB (PCIe)
  • ディスプレイ: 1600x900(14インチ) -> 2560x1440(13インチ)
  • キーボード: 英字キーボード -> 日本語キーボード
  • バッテリ駆動: 3 時間 -> 10 時間
  • 重量: 1.36kg -> 1.58kg

CPU も世代が上がってさくさくで使いやすいし、何よりも解像度が上がってよくなった。 家では AC 電源に繋いだままの X1 が放置されているけれど、 移行のために X1 で作業するとドットが目立ってちょっと悲しくなる。

バッテリ駆動時間についてもともと X1 は 6 時間なのだけど、ヘタりにヘタって 3 時間になってしまっていた。 自分の使い方では問題なかったのでよかったけど、ダメになってしまっては意味がない。 MacBookPro は 10 時間も持つので AC アダプタを会社に置きっぱなしにできるのはちょっと嬉しい*2。 けれども重量が 220g も増えてしまったのはいただけない。 ずっしりとした重さを感じていて、リュックでの通勤には少しつらいけど支給の PC がモデル 2016 になるまでの我慢だと思ってこの際割り切った。

キーボードは支給 PC だと日本語キーボードしか選べないので、仕方なく日本語キーボードを使ってる。 キーマップを英字配列に変更していて、フラストレーションはたまるが時々を除いてそれほど困らない。 Insert キーや PageUp, PageDown がなくなったほうが辛いくらい。

総合的にはなかなかいいものだなあという所感。

ソフトウェア

OS が Gentoo Linux から OSX になったのが変化としてかなり大きい。

とはいうものの自分も昔は Snow Leopard まで使っていたので大丈夫だろうと思っていた。 (なぜ Mac をやめたかというと当時、 Lion へのアップデートが有料になり嫌なったのと Spaces がなくなったと聞いて Linux に逃げた。)

今に至るまでいくつか問題があったけれども、まずはいいところだけ書いてく。

  • Xcode のインストールに Apple ID が必要ない!
  • Homebrew サイコー!!
  • Karabiner のおかげでほとんどのキーバインドを矯正しなくていい!!!
  • mission control も悪くない!!!!

初めは Firefox (vimperator) も emacs も一部の X11 アプリも使えるし「いいぞ!」という感じだった。 パスワード管理も keepassx を使っていてそのまま移行できたのでかなりよかった! 会社資産の PC に個人のアカウントは入れると面倒なのでそれは移行していないけれど、必要なアカウントだけ選択して移行するのはちょうどいい。

けれども当然、悪いところやハマったところもあって..。

  • Finder のキーバインドがカスタマイズできない。
  • X11Retina が有効じゃない。
  • タイル型ウィンドウマネージャ(xmonad)使いたい。
  • ファイルシステムで大文字小文字が区別されない。
  • 句読点入力がカスタマイズできない(mozc は設定書けたけど OSX でもできるのか不明)。
  • mission control の設定が見れない。
  • ebuild の資産が使えない。
  • UID が 501。
  • docker のバインドマウントの挙動が違う。
  • 色々なところで sudo を要求されてなんか嫌だ。

Linux だとファイラは thunar を使い、 gtk-can-change-accels=1 を有効にして自由にショートカットキーをカスタマイズしていた。 見た目を気にして nemo を使っていた時期もあってこっちはソースにパッチを ebuild の中で当てて、ショートカットキーをカスタマイズしてた。 たとえば Shift + Super + Hホームフォルダに移動や Control + L で直接パスを入力して移動を指定していたのでそのあたり自由に設定できない Finder はなかなかにつらい。

ハマったのは dotfiles をコピーしたら X11 用の設定までコピーしてしまい Xquartz が起動と停止を繰り返す上にエラーメッセージが出力されずに捕捉するまですごく追いづらかったり、 /etc/shells に書かないと zsh が使えなかったところ。 調査のときに Xquartz はユーザログイン時に launchd から startx が叩かれていることがわかったとか launchd の設定ファイルの文法とかあまり使う機会がないのに launchd に詳しくなってしまった..。

ハマるのは解決すればいいので仕方ないのだけれど、嬉しくない本当の理由はハマっても実務に活かせないところ。 例えば OSX の launchd や docker for mac でハマっても、実務で扱っているサーバは Linux なので systemd や sysvinit が init だったり、 docker も全然違うので実務には活かすことができない。 Gentoo なら同じ Linux のため手元の PC での作業をサーバにそのまま知識を活かせるし、coreutils のコマンド群を触っていても小さな発見があるので楽しい。 ローリングリリースで新しいパッケージも順次追加されるので、 eix-diff でどのパッケージが更新されたかといった情報を得たり、試すのが楽しみにもなる。

結局

使っていると光沢ディスプレイもテカテカが気になったり、タイプ中にタッチパッドが反応してトラックポイントが欲しくなってきたりハードウェア的にも不満が出てきてしまったので、結局 1 年もしないうちに Thinkpad X1 に戻してしまうんだろうなあと思って今は本当にこれでいいのかどうか試す目的で考えながら使っている。

*1:会社支給の PC に移行したのは色々複雑な理由がある。持ち込み規定が厳しくなってたり、また壊れたときのことを考えると億劫になった。

*2:X1 は AC アダプタを 2 個持っているので、アダプタは普段持ち歩いてなかったけど勉強会に行く時はアダプタ必須になるのでありがたい。

ISUCON6 にインフラエンジニアとして参加して優勝した!!

f:id:kizkoh:20161024092622j:plainISUCON6 にインフラエンジニアとして参加して優勝してきたので、なにやったかとか感想とか書いてく。 (僕はアプリを触っていない。アプリの話は他の人が書いてくれるはず。)

※ @methane さんが会社ブログに投稿されたので記事を更新

dsas.blog.klab.org

会社で出場してチーム名は「この技術部には問題がある!」、メンバは @methane @mecha_g3 さんと僕。 ちなみに @methane さんと @mecha_g3 さんは兄弟だけど、僕だけ他人です(笑)。

チームメンバ役割

本戦に向けて、会社でチーム練習してたので本戦の役割もできてた。

@methane さんが全体の構成を見てチーム全体をまとめる役割、 @mecha_g3 さんがアプリ、 僕がインフラの役割。@methane さんが戦略をたてて、各自自分で改善できるところは改善してく感じで進めることができた。なので、自分は結局アプリのコードを読まなかったけど、やるべきことはできたのでチーム構成最高だった!!

当日の朝

自分は朝に余裕を持ちたいので、当日は 6 時起床。前日は特になにもせずに寝ることだけに集中してた。

チームでは一番に到着したので、席確保して @methane さん、 @mecha_g3 さんがくるまで twitter 見ながらだらだら過ごす。落ち着きが大事。

競技開始

他のメンバにレギュレーションや構成を見てもらっている間にコードや公開鍵をデプロイしたり、オペレーションに必要なツールやリソースモニタリングに必要なパッケージを流し込んでいったりする。 今回は他のサーバ上からオペレーションできるよう踏台サーバとして Azure 外にサーバを置いていて、アプリ担当のメンバが本戦サーバにログインせずともアプリをデプロイしたりログを収集したりできるスクリプトを整備していた。 さらにその準備をしている片手間で初期状態のベンチをかける。

3500 点くらい。

ここでスタートの準備が整ったので戦略会議をする。

フロントは https で nodejs(react) のアプリケーションが待ち構えていて、バックエンドにアプリ、その後ろに MySQL がある構成だった。 nginx を(ちゃんとした http サーバを立てたくて)使いたいので、フロントを nginx で受けて nodejs とアプリに nginx 分けようという戦略になって、これが最終提出時の構成にもなった。 初期実装は php で動いてて、プロセスに apache がいてこれを切り替えようとして systemctl list-units するも apache2.service がなくて焦るが、ps で見ると -DFOREGROUND で動いていたので、 あぁ docker で動いているんだなと思って docker ps したら起動中のコンテナ一覧が表示されてホッとした(レギュレーションのスライド読んでなかった)。 起動中のコンテナ一覧には node や MySQL があって、各サービスも docker-compose の docker で動いていた。 これらは Linux ネットワークスタックの処理をいくつか通過するためネットワークパフォーマンスが悪くなるし、何よりも問題が発生したときに追うのが面倒という理由で外だししていくことにした。 @methane さんに nodejs と MySQL のインストールを手伝ってもらいながら、 docker exec しつつ中身を一通り調査しながら systemd のサービスを作成していった。

すべてのコンテナを潰し、 nginx で 443 を listen するようにして、ベンチかけたらアクセスログHTTP/2.0 が記録されてたので予想通り!!

9500 点くらい。

ここで 12 時半くらいだったと思う。

/api へのアクセスはアプリだったのでアプリに飛ばしたり、静的ファイルの配信を nginx で gzip_static を使って配信するようにしたり、 /api/streamproxy_buffering を off にして 14 時くらい。

16000 点くらい。

たぶん、ここまでアプリのコードは触られていなかったんじゃないかと思う(うろ覚え)。 だいぶ落ち着いてきたのでアクセスログの集計スクリプトを整備していく。

@mecha_g3 さんがデータをオンメモリ化されたり、 @methane さんに img/SVG を go で返すようにしてもらったりして、設定変更した nginx のリバプロ先を tcpdump で念のため確認してた。

39000 点くらい。

余裕もできて、16 時になったので複数台に展開したりするための準備を進めていく。 sysctl.conf や別のインスタンスMySQL を用意したり、 node を動かす準備をしていく。

17 時になって最終構成。

        +  +------------------------------------+-------------------+-------------------+
        |  |                        listen 8081 |       listen 8081 |        isten 8081 |
        |  |                                    |                   |                   |
+isu01----------+   +isu02+---------+   +isu03+---------+   +isu04+---------+   +isu05+---------+
|       |  |    |   |               |   |       |       |   |       |       |   |       |       |
|  +----v--+-+  |   |  +---------+  |   |  +----v----+  |   |  +----v----+  |   |  +----v----+  |
|  |  nginx  |  | +---->  mysql  |  |   |  | nodejs  |  |   |  | nodejs  |  |   |  | nodejs  |  |
|  +----+----+  | | |  +---------+  |   |  +----+----+  |   |  +----+----+  |   |  +----+----+  |
|       |       | | |               |   |       |       |   |       |       |   |       |       |
|  +----v----+  | | +---------------+   +---------------+   +---------------+   +---------------+
|  |   app   +----+                             |                   |                   |
|  +----^----+  |                               |                   |                   |
|       |       |                               |                   |                   |
+---------------+                               |                   |                   |
        |listen 8080                            |                   |                   |
        +---------------------------------------+-------------------+-------------------+

ここからのスコアはこんな様子。

f:id:kizkoh:20161024042237p:plain

isu03, isu04, isu05 で nodejs を動かしているんだけど、これを 2 プロセスにするとなぜかスコアが大幅に落ちて 3万点 ぐらいになってしまって、ボトルネックの調査に行き詰まる。 結局、ボトルネックが不明だったので、 1 プロセスに戻した状態で最高記録を叩き出した。 その後、インスタンスの再起動試験をやっていくつかベンチをかけてみたけれど、 8 万点を越えることができなかったのが不完全燃焼。 なぜ、スコアが下がってしまったのか今でも分からない(停止 -> 起動ではなくて、再起動してたんだけど Azure のインスタンスガチャ?)。

蓋を開ければ優勝できてたんだけど、最高点数で表彰されなかったのが悔しかった。

最後にボトルネックになっていたのはネットワークだったんだろうなーと今は思ってる。 けれども、これを早々と見つけていても構成を 17 時の時点で構成を変えるのは難しかっただろうなとも思っていて、早くも来年の課題が見えてきて来年が楽しみになってる。

技術的トピック

直接スコアにコミットしたのは docker を潰すところと nginx の設定をガリガリやったところ。

docker はいつか来るだろうなと思っていたので、割とやることは自明で気持ちよく潰せたので楽しかった。 docker-compose は触ったことがなかったけど(今だと swarm でやることになるのかな?)、 docker-compose がどんな役割かは知っていたので、勘で設定ファイルを systemd 化していくことはできた。

http2 も来るだろうと思っていて let's encrypt の準備をしていた。 ドメインが割り当てられることはやっぱりなかったけれど、 nginx.conf の雛形を放り込んだら http2 をちゃんと喋ってくれたので、よかった。

あとはオペレーションのはなし。

今回はサーバが 5 台用意されていて、 2 年前に出場したときよりも 2 台増えていてオペレーション力が求められた。 けれども、基本的にサーバが何台増えようともできることは同一にしておく必要があって、以下のようにホストやプライベート IP のリストをつくっておいて利用するのが簡単でハマりどころも少なくていい。

ISUIPS="10.6.1.5 10.6.1.6 10.6.1.7 10.6.1.8"
for i in ${ISUIPS[@]}; do echo $i; ssh $i 'hostname'; done
for i in ${ISUIPS[@]}; do echo $i; rsync -n -avRiK /etc/systemd/system/react.service $i:/; done
for i in ${ISUIPS[@]}; do echo $i; rsync -avRiK /etc/systemd/system/react.service $i:/; done

echo -n ${ISUIPS[@]} | xargs -d' ' -P${#ISUIPS[@]} -I {} ssh -qt {} 'sudo journalctl -f -u isu-go'
echo -n ${ISUIPS[@]} | xargs -d' ' -P${#ISUIPS[@]} -I {} ssh -qt {} 'sudo tail -F /var/log/nginx/access.log'

for i in ${IP のリスト} みたいな書き方は普段のオペレーションでも使っていて練習ではなくて業務のノウハウ的なもの。 ホスト名の扱いに慣れていれば fabric や pdsh みたいなものを使わなくても ssh や xargs といった標準のツールで十分オペレーションできるし戦える。 こんなのを毎回打つ人として本戦ではインフラエンジニアまたはオペレーションエンジニアが必要なのかなあと思う(ミスるとクリティカルだし)。

感想

初めて出場した ISUCON は ISUCON4 だった。入社 1 年目で、そのときも @methane さんと出場して 5 位。 昨年はアプリ担当で出て go で出場するも、少しスコアを上げただけで予選敗退した。 このときはインフラ担当がいなくて、アプリとインフラを自分だけでやってたので本当つらかった思いがある。

そして、今回。予選はいつもなら各自で戦うんだけど今回は予選からチームで戦って役割を分担した。 予選は nginx.conf の設定をするだけであまり活躍する場はなかったけれど、本戦はインフラエンジニアがかなり活躍できて本当に楽しかった。 一方、途中でオペミスして、ファイルをふっ飛ばしてしまうこともあり @methane さんに ansible をトレースして助けていただいたり、構成の変更を練ってもらったり本当に感謝。

記事だとめっちゃ綺麗にうまくいったようにまとめているけど、実際は準備してた道具がほとんど封印された状態。 mysql の sys スキーマや nginx の webdav, image-filter, mruby, グラフィカルなリソースモニタリングの準備していたけど予想を上回る内容の問題で ISUCON って感じ! HTML5 の SSE や react など聞いたことはあるけど触れたことのない技術が問題として出題されて、どんな技術にもより貪欲でありたいと思わされた。 僕にとって ISUCON は自分自身のベンチマークだと思っていて自分がどれだけ成長できているかを実感できる素晴しい機会なので、ともあれ来年も今後もずっと挑戦していきたい。

最後に運営の皆様、参加者の皆様、会社の皆様、お忙しい中サポート & 応援ありがとうございました。

https://bit.ly/isucon6 からギフトできるようですので是非!!