ISUCON 9 予選を通過しました!

ISUCON9 予選に 「tmp」 というチーム名で参加し、10,450 点でギリギリ予選を通過することが出来ました。 大学の同期で社会人1年目が2人と修士1年生が1人のチームでした、ISUCON7では学生枠で本戦に出場させてもらいましたが、今回は社会人枠 (?) として予選を通過することが出来たのでとても嬉しいです!

メンバー

最終構成

2台目の Nginx から 1,2,3 台目の App に振り分け、App から 1 台目の MySQL, 3 台目の Redis へそれぞれ振り分けました。AppはGo言語です。

f:id:skitazawa1121:20190909002922p:plain

チームでやったこと

以下チームでやったことです。だいたいこの順番でやったはずですが記憶はあいまいです。

序盤

競技が始まってすぐに秘伝のタレと呼ばれる初期設定の反映作業やデプロイツールの配置等を行い、その後は大まかに自分と @ranksai がアプリケーション、@boringsloth がインフラ周りという役割分担でやりました。序盤の流れは学生時代から数回ISUCONの過去問で練習を重ねていたためすんなりと行うことが出来ました。

getNewCategoryItems() の N+1 の改善

getNewCategoryItems() の中で items の数だけ売り手の情報やカタログの情報を SELECT していた部分が重そうだったので、そこを改善しました。 売り手情報を引っ張ってくる getUserSimpleByID() の部分は N+1 の 1 のクエリとJOINし、カテゴリ情報を引っ張ってくる getCategoryByID() の部分は親カテゴリが存在するかを再帰的にチェックする部分を上手くJOINする方法が思いつかなかったため、カテゴリIDからCategoryを引けるmapをキャッシュすることで対応しました。

複数台構成

上記の N+1 を自分と @ranksai がやってる間、@boringsloth が複数台構成にしてくれました。 途中、ポータルサイトServers にベンチ対象となるサーバをすべて登録しないといけない仕様に気付かなかった等いろいろと苦しんでそうだったので、自分はめっちゃ応援 (ひとまかせ) してました!

キャッシュをRedisに

N+1を解消する際にカテゴリをキャッシュしましたが、複数台構成にするにあたりこれをRedisに乗せました。ここは特につまるところなく出来たので個人的に良かったなと思っています。

Index を貼る

ISUCON8の予選でもそうでしたが、今回のベンチは /initialize に POST が来たときに sql/init.sh ファイルを別プロセスで実行することでデータベースの初期化を行っていました。 ここで、init.sh では一度データベースをDropした後にCreateしてデータを注入するため、事前にインデックスが張られていてもベンチが走るたびにインデックスが吹っ飛びます。競技中盤には上記の仕様を把握していたのですが、 sql/init.sh に足したインデックスを張るコマンドが上手く実行できてない (スクリプトの途中にエラーしてexitしてた?) 問題のせいで競技終了30分前まで思うように点数が伸びなかったです。

競技終了30分前に適切にインデックスを張り、その後30分間でログ出力をやめたりCampaignの値を変えてみたりを行い、最終的に 10,450 点で終了しました。以下は点数の遷移のグラフです。

f:id:skitazawa1121:20190909002919p:plain

やれなかったこと

Bcrypt の処理が非常に重いのを観測しましたが、残り時間も少なく特になにも対応できませんでした。競技終了後に感想部屋で、「APIを非同期化する」だったり「複数回benchを回してみて割と決まったユーザからしかログイン要求が来ないので、生パスワードをダンプしてsha256で保存しておく」などのアイデアを見てなるほどなあとなりました。

以下は競技終了直前の pprof です。Bcryptの処理がすごい重い・・・

(pprof) top -cum 30
Showing nodes accounting for 73.48s, 94.29% of 77.93s total
Dropped 721 nodes (cum <= 0.39s)
Showing top 30 nodes out of 54
      flat  flat%   sum%        cum   cum%
     0.01s 0.013% 0.013%     76.92s 98.70%  net/http.(*conn).serve
         0     0% 0.013%     76.56s 98.24%  goji%2eio.(*Mux).ServeHTTP
         0     0% 0.013%     76.56s 98.24%  net/http.serverHandler.ServeHTTP
         0     0% 0.013%     76.53s 98.20%  goji%2eio.dispatch.ServeHTTP
         0     0% 0.013%     76.53s 98.20%  net/http.HandlerFunc.ServeHTTP
         0     0% 0.013%     72.21s 92.66%  main.postLogin
         0     0% 0.013%     72.03s 92.43%  golang.org/x/crypto/bcrypt.CompareHashAndPassword
         0     0% 0.013%     72.01s 92.40%  golang.org/x/crypto/bcrypt.bcrypt
     0.03s 0.038% 0.051%     71.98s 92.36%  golang.org/x/crypto/bcrypt.expensiveBlowfishSetup
     3.54s  4.54%  4.59%     71.89s 92.25%  golang.org/x/crypto/blowfish.ExpandKey
    68.42s 87.80% 92.39%     68.42s 87.80%  golang.org/x/crypto/blowfish.encryptBlock
     0.02s 0.026% 92.42%      1.59s  2.04%  main.getNewCategoryItems
...

感想

全体的に「ISUCON」という競技において抑えるところは抑えることが出来たかなと思います。ただここから点数を伸ばすためにはもっと序盤や中盤の実装を早く済ませるための実装力が無いとダメだと個人的に痛感しました。普段からこの手のアプリケーションに触れて、世の中のスーパーなエンジニアを目指しもっと頑張りたいと思います。

余談

もともと競技日は開始30分前に母校の大学に集まってどこかの席を借りてやる予定 (と勝手に思って) ましたが、起床試験にfailした人やらそもそもリモート参加で大学に来ない人やらで朝からわちゃわちゃしてました。開始が10分遅れてくれて結構助かりました・・・。

〆はラーメン (と酒) !

f:id:skitazawa1121:20190909002916j:plain