ISUCON7 本戦で惨敗しました

チーム新卒としてISUCON7 本戦に参加し、見事に惨敗しました。

f:id:abcang:20171126162254p:plain

本戦の問題

本戦の問題は、ルームを作って複数人で協力できるクッキークリッカーみたいなゲーム「Chair Constructor Online」でした。 通信はWebSocketを使用していて、アクションが厳密に同期される非常に複雑なアプリケーションでした。 また、計算が複雑ということでテストが用意されていたのには驚きました。テストは非常に役に立ったのでありがたかったです。

自分が主にやったこと

序盤

WebSocketが使われていたのでどの言語で進めるか悩みましたが、WebSocketがボトルネックになる問題ようなではないだろうと予想して、当初の予定通りRubyで進めることにしました。

最初はプロファイラを仕込んで時間がかかっているところなどを調べていました。 今回はN+1や遅いクエリが全然なく、ほとんどが計算の部分だったのでstackprofが非常に役に立ちました。

あとは、DBのコネクションを使いまわすようにしました。(あとでもとに戻した)

中盤

get_powerやget_priceはcountに対して計算結果はいつも同じなので、計算結果をキャッシュするようにしました。 今思えばredisに入れておけば他のサーバでも使えて、再起動後でも利用できて更に効果があったかも。

あとは1000ミリ秒後の未来をシミュレートする処理が遅かったので最適化しました。

addingとbuyingで一致する時間と、もともとのループで使用していた最初の時間(current_time + 1)と最後の時間(current_time + 1000)だけをループで使用するようにしました(9行目)。 今思えばcurrent_time + 1の方は必要なかった気がします。 また、total_milli_isuは線形増加するので、間の時間をtotal_powerに掛けた値を足すように変更しました(11行目)。

そして、アイテムの購入可能判定では、item_on_saleにその時間を入れるのではなく、一つ前のループの時間から1ミリ秒づつチェックするように変更しました(23-33行目)。 一つ前のループから現在のループまでの線形増加によるtmp_total_milli_isuの値がしきい値を超えていなかった場合、buyingによってtotal_milli_isuが増えていることになるのでそのループ時の時間をitem_on_saleに入れています(32行目)。

終盤

使用されるサーバが偏っていたので、WebSocketの接続先を返すサーバを一つにして、ベンチ対象もその1台にしようとチームメイトに提案しました。 それまではそれぞれのサーバがラウンドロビンで接続先のサーバのホストを返していました。 WebSocketの接続先を返す専用のサーバを1プロセスだけ立てて、すべての接続先の管理を1プロセスに任せることで、より均等に接続先を振り分けることができました。

あと、DBでtoo many connectionsが出ていると言われたので、DBのコネクションを使いまわすのをやめました。 今回はプロセス数とスレッド数、サーバ数も多く、WebSocketで接続されっぱなしになっていたから起きたのだろうと思います。 なんでもかんでも使い回せばいいわけじゃないということがわかって勉強になりました。

チームメイトが主にやったこと

  • motさん
    • mitemをDBから読まないように
    • ミドルウェア周りの調整
    • 全サーバーの使用状況などの監視
  • syusuiくん
    • ある時点での椅子合計数とパワーの合計を保存

反省と感想

今回は、ある時点の計算結果をDBに保存する部分でDBがボトルネックになってしまい、そこからredisに変えようとしたけどうまく動かずにタイムアップという感じでした。最初からオンメモリは危険だろうと思っていたので、最初からオンメモリで考えれていたらもう少し点数が伸びていたのかもしれないです。

いつものISUCONではN+1の解消やDBのindexの改善ばっかりをやっていたので、今回はあまり貢献できなかった気がします。 ただ今回は去年とは違い、初期実装よりも点数が上がり、なんとか10000点も超えることができたのでよかったです。

abcang.hatenablog.com

最後に、運営の皆様ありがとうございました!