Edited at

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

More than 5 years have passed since last update.

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で返ったりするので、これくらい単純な予選の問題だと結構言語が重要になるかもしれない。