ISUCON4 予選お疲れさまでした! 予選問題の Ruby 初期実装などを担当した @sora_h です。
予選はたのしんでいただけましたでしょうか? 本記事では、ざっくりとそこそこのスコアを出す解き方を紹介しようと思います。
※@rosylillyによる、高得点を出すことを重点に置いたピーキーな解答例はこちらです
前提
- 一人でやる
- 一応8時間経過時点でスコアをとる
- ただし出題者であるので問題の把握などの時間は短縮されていることに注意。
- Ruby の実装を利用する
- ある程度、現実味のあるチューニングが主
- ベンチマーカーの実装を利用したりしない
また、この記事で出来た実装は GitHub に掲載しています: https://github.com/sorah/isucon4-qualifier-sorah
初期スコア
とりあえず立ち上げて動かした時のスコアは success:6030 fail:0 score:1303 でした (workload=1)
チューニング
ここからチューニングしていきます。
失敗数のカウンタを Redis にのせかえる (2 時間経過)
問題を読むとログを残す必要は一切ない事がわかるので、とりあえず mysql でログを取るのをとめて、Redis をカウンターとして利用するように変更します。
ただし、MySQL でも適切な index を張ればスコアが出せます。この辺は好みだと思われます。
-
isu4:user:<id>
,isu4:ip:<ip>
をカウンタとして連続失敗数を記録 (INCR
) -
isu4:last:<id>
,isu4:nextlast:<id>
を最終ログイン記録に利用
スコアはこの時点で "success:29430 fail:0 score:6357" (workload=1)
これはあまりスコアに影響はなかったのですが、少し後で users テーブルも redis にのせることにより、
完全に mysql をとりのぞいています。
nginx で静的ファイルを配り、app process との接続を UNIX ドメインソケットに
見出しのまま。この時点でのスコアは workload=1 で "success:52550 fail:0 score:11351".
workload=16 にしたら 23994 点を記録しました。
erb が遅いのでレンダラーを erubis に入れ替える
erb を高速にレンダリングできる erubis を導入しました。
Sinatra を捨てる (4 時間経過)
ここで Sinatra の実装を見てみたら、意外に重そうだった事を思い出したので、捨てて、Rack アプリケーションとして実装しなおしました。
workload=16 で success:158590 fail:0 score:34265 を記録。
アプリケーションの細かいチューニング (6 時間経過)
引き続きボトルネックはアプリケーション側にあるので、気になったところをどうにかしていきます。
プロファイリングには stackprof.gem を利用しました。
-
commit 3fe6afa1520cc7b954eaffaf3905c3e8564e356a
- erb の layout を実現するのに 2 度目からはブロックつかうのやめてみる
-
commit 6f224b3a943641987f8e815502914899e7299912
- セッションストアをやめて素のクッキーを利用してみたり
- ログインの概念がなくなるのでこれでいいのかという思いはある (現実味がない)
-
commit b45dc4fa424c75d0592724d036e4b8ef10af9bd6
- Redis の呼び出し回数を減らしてみたり
-
commit a1259da455859972cf4cf810644b55b415f1b224
- Redis の高速なクライアント hiredis を利用してみたり
この前後でスコアは 37228 点。
トップページのほぼ静的ページ化 (7 時間経過)
5 種類しかないので起動時にレンダリングしておいてクッキーの値を見て切り替えるようにしました。
実際予選上位ではこれ以外にも、URL のクエリストリングを利用しているチームもあったようです。
この時点でスコアは 39144 点でした。
ルーティングの改善 (およそ 8 時間経過)
自前で書いたルーティング処理が遅いので高速化しました。
この時点で、予選当日の競技での 8 時間が経過しました。8 時間経過時点でのスコアは workload=16, score=40324 点でした。
そして、ここからさらに粘ります。
アプリケーションの細かいチューニング (2)
この辺からはわりと試行錯誤しています。本当に効果あるかどうか微妙な変更もある気がする。
- HTML から空白, 改行を取り除いた
- mypage で erb を呼び出すのをやめた
- erb はトップページで起動時に 1 度だけ呼び出されるだけになりました
- Rack application のレスポンスとして配列が利用できるので、この時点での文字列結合はせず、配列で間に動的な文字列を挿入しています。
- commit c5386ba3908dab728f5909b1cc600155586b81b9
- commit e1545953c1bfde45568d55261c159359e236404a
- 文字列結合(式展開) する際、to_s メソッドコールが必要になる Symbol より、そのまま String を渡した方が早いのでそうする
- Ruby 2.1 から導入された frozen string 最適化を利用しています。
- commit 1178490d1fe5ece12cddcfead2cc403a65ae546e
- 極力 Rack::Request の処理を利用しないようにした
- 中身を見るとやや複雑なことをしているので、今回の要件に合わせて必要最小限な実装に入れ替えました。
- POST パラメータのパースだけは面倒そうな予感がしたので Rack::Request に任せたままになっています。
- commit ea7acfc4b5292995bd582dc52a59482a8bc21259
- commit 2e63663e36f94dc83b6e8a2c88082588dc363318
- Redis の呼び出しを減らした
- エラー内容の時点で pre-render しておいたトップページの配列インデックスを渡すようにした
- 条件分岐の削減がされます。
- commit 7ad4c0eb0ce86d3285d3b04b0ba0940a8aa257b1
- redis の自動 save を止めて /report で save するようにした
- Redis のキーに user id を採用していたが、ここを login (ユーザー名) にする事で redis への問い合わせを減らした
-
isu4:user:<login>
キーを必ず引いて、user id を取得するのをやめました。 - パスワードのチェックがされるタイミングでのみ、このキーに対してクエリを発行します。
- commit 3a9bfab4726c357c94fbab3b38de17e0113a1ffa
- commit cbe027d57dc0bd5647700bcc98adcb7b288122b4
-
- unicorn のワーカーを増やして workload を 8 に 下げた
- commit 29e9db9e5d5bc4847b60bd6e8728109096e92696
- この時点で workload=8 score=43621
- nginx で etag 止めたり open_file_cache を入れたりした
- 不要な Set-Cookie ヘッダを生成するのを止めた
- nil.to_i は 0 になる。条件分岐する必要はなかったのでやめた
- rack server に返す値で、body は each メソッドを持ってる必要があるが、each 持ってなかったら配列でつつんであげる丁寧なおせっかいをやめた
その他
パフォーマンスには影響ありませんが、Can't assign local address と Too many open files 対策で以下を追加しています。
# /etc/sysctl.conf net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_tw_reuse = 1 # /etc/security/limits.conf * hard nofile 65535 * soft nofile 65535
(net.ipv4.tcp_fastopen = 2055
も追加していたんですが、スコアに大した影響はありませんでした)
最終スコア (8時間+)
12:19:44 type:info message:launch benchmarker 12:19:44 type:warning message:Result not sent to server because API key is not set 12:19:44 type:info message:init environment 12:19:58 type:info message:run benchmark workload: 10 12:20:58 type:fail reason:Request cancelled because benchmark finished (1min) method:GET uri:/stylesheets/isucon-bank.css 12:20:58 type:fail reason:Request cancelled because benchmark finished (1min) method:GET uri:/images/isucon-bank.png 12:20:58 type:fail reason:Request cancelled because benchmark finished (1min) method:GET uri:/stylesheets/bootflat.min.css 12:20:58 type:info message:finish benchmark workload: 10 12:21:03 type:info message:check banned ips and locked users report 12:21:06 type:report count:banned ips value:654 12:21:06 type:report count:locked users value:4650 12:21:06 type:info message:Result not sent to server because API key is not set 12:21:06 type:score success:209927 fail:3 score:45350
workload=10 でこのようなスコアになりました。Ruby レベルではほぼ限界にチューニングしてあると思います。
(これでも上位チームはおろか Ruby を利用したチームのトップにすら勝てていなくて、皆さんすごいなあと思っています)
MySQL の場合
Redis を採用したのは筆者の趣味なので、同じ実装で MySQL に戻し少し手を加えたところ、 workload=10 で success:191040 fail:0 score:41270 を記録しました。
オリジナルの MySQL 関係から変更した点:
- すべてにおいて users.id を利用しなくして、users.login を利用するようにした
- index 追加と my.cnf に数行追加
MySQL 版はこちら: https://github.com/sorah/isucon4-qualifier-sorah/tree/mysql
まとめ
Ruby 実装を利用した解き方の一つを紹介しました。
運営チームは ISUCON4 本選も楽しんでいただけるような問題を準備中です。お楽しみに!