ISUCON4予選の問題で31万点を出すためにやったこと

  • 40
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

perlで36万点出してる記事があり、面白そうなのでできるところまでチューニングしてみることにした。
↑の記事のhtml minifyとかtcp fast openはやってない。なお、benchmarker-v2でスコアを計測している。

書いたコードは k0kubun/isucon4-qualifier においてある。

Ruby

出場したチームメンバーが共通で書けるのがRubyなので、まずはRubyで解き直してみた。

score workload やったこと
1,385 1 Rubyの初期状態にNewRelicをいれた状態
1,411 1 nginxとunicorn, アプリとmysqlの間をunix domain socketでつないだ
2,588 1 login_logに(user_id, succeeded)でインデックスを張る
7,116 1 login_logに(ip, succeeded)でインデックスを張る
10,263 1 静的ファイルをnginxで配信
26,667 15 workloadを上げるため、limits.confでファイルディスクリプタの上限を変える。sysctl.confでtcp_tw_recycleとtcp_fin_timeoutを小さくしてローカルポートが枯渇しないようにする。
28,055 15 innodb_buffer_pool_sizeを1Gにする
28,860 15 unicorn worker_processes 10 -> 4
29,229 15 なんかfailするのでnginxの各プロセスのファイルディスクリプタ上限(worker_rlimit_nofile)を4096にする
41,777 15 NewRelicを外す
42,433 10 nginxのworker_processes 1 -> 4
41,728 10 login_logの処理をRedisに載せ、usersはハッシュでメモリに載せる(なぜかスコアが上がらない…)
48,054 10 トップページへのリダイレクトでflashの種類ごとにquery stringを変え、nginxでトップページのhtmlを配信する
50,429 8 redisとの通信にunix domain socketを使う
123,958 8 img tagとcssのlink tagをscriptタグから追加するようにし(自身のタグを取り除く)、ベンチマークで静的ファイルにリクエストが飛ばないようにする。元ネタ

この辺で飽きた。
アプリのコードに手をいれなくとも4万点行くので、4万点くらいなら1時間かけずにいけることになる。
Redisに載せる実装でスコアが上がらなかったのはなんか僕の実装がおかしいだけだと思う…

Go

1人でやるなら最初からGoで取り組む気マンマンだったのであんまりRubyのほうは真剣にやってない。
ここからGoで解き直した。

戦略

オンメモリで作りたいが、「ベンチマーク実行時にアプリケーションに書き込まれたデータは、再起動後にも取得できること。」を違反しないように作らなければならない。

そのため、普通に起動するとmysqlにクエリを吐くモードで動作させ、init.shを叩いたらオンメモリで動作するベンチ用モードになるようにする。
このモードの間必要なデータはmap型変数に入れ、INSERTクエリはキューにためておいて/reportでjson書き出す直前でBULK INSERTする。これが終わったら通常のモードに戻す。

これにより、ベンチ中以外の任意のタイミングで再起動してもデータの整合性がとれる実装になる。
ベンチを実行中に異常終了して再起動するようなチェックは流石にないだろうという想定に基づいている。

score workload やったこと
23,875 9 OS, DBの設定やインデックスを少し引き継いだままGoの初期実装にいれかえた
33,181 9 nginxで静的ファイルを配信
90,072 9 scriptタグを使ってタグを挿入しベンチマーク時に静的ファイルにリクエストが飛ばないようにする
102,326 9 martiniをやめてginにする
115,306 9 ipのban判定をオンメモリで行う
126,098 9 login時のusersのSELECTをオンメモリに変える
144,124 9 userのlock判定をオンメモリで行う
163,962 9 ベンチのGOGCを上げる(元ネタ)。currentUserをオンメモリに変える
170,140 9 LastLoginをオンメモリで取得する
213,948 9 INSERTを/reportでまとめて発行するようにし、ベンチ実行中のINSERTの発行をやめる
241,866 9 トップページをginで処理(query stringで分岐して文字列を返す)できるようにする。nginxをstopしginで80番を受ける。
271,130 10 mypageをtemplateではなくfmt.Sprintfで処理するようにする
315,232 15 gin.Default()ではなくgin.New()を使い、ログを書き出すミドルウェア等を外す

RubyでデータをRedisに乗っけてたときはスコアがなぜか上がらなくて辛かったが、こっちは毎度順調に上がってよかった。
RubyでもRedisじゃなくて普通にHashでメモリに乗せればよかったかもしれない。

RubyとGoで実装してみた感想

Goだと最終的にワンバイナリで全て受けられるようにできるのは大きいと思う。
Rubyで書くとタイムアウトするやつがGoだと普通にすぐ終わったりするし、Rubyだとどうしても1ms切れないやつがGoだと数十μsで返ったりするので、これくらい単純な予選の問題だと結構言語が重要になるかもしれない。