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 を利用しました。

この前後でスコアは 37228 点。

トップページのほぼ静的ページ化 (7 時間経過)

5 種類しかないので起動時にレンダリングしておいてクッキーの値を見て切り替えるようにしました。
実際予選上位ではこれ以外にも、URL のクエリストリングを利用しているチームもあったようです。

この時点でスコアは 39144 点でした。

ルーティングの改善 (およそ 8 時間経過)

自前で書いたルーティング処理が遅いので高速化しました。

この時点で、予選当日の競技での 8 時間が経過しました。8 時間経過時点でのスコアは workload=16, score=40324 点でした。

そして、ここからさらに粘ります。

アプリケーションの細かいチューニング (2)

この辺からはわりと試行錯誤しています。本当に効果あるかどうか微妙な変更もある気がする。

その他

パフォーマンスには影響ありませんが、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 本選も楽しんでいただけるような問題を準備中です。お楽しみに!