ISUCON7優勝しました [MSA]

ISUCON7優勝しました

ISUCON7

ISUCONとは

お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル、それがISUCONです。過去の実績も所属している会社も全く関係ない、結果が全てのガチンコバトルです。

運営・主催・協賛・関係者の皆様、非常に楽しいイベントでした。
本当にありがとうございます。

本戦

試合開始前

まず会場に行く途中に不幸な事故がありました。

本戦の問題

本戦の問題はクッキークリッカー+ソーシャルなゲームでした。
部屋を作成し、作成した部屋に入った人達で椅子をクリックして、アイテムを購入する。
そして、クリック/購入を部屋の参加者全員に伝えるためにwebsocketでほぼすべての通信をおこなう。

チームでの作業の流れ

チーム構成は下記のとおりです。

  • ken39arg => アプリ、インフラ
  • mizkei => アプリ
  • suzuki => インフラ

言語はGoを選択しました。

  1. レギュレーションをみんなで読む
  2. デプロイ出来るようにする
  3. とりあえず初期状態でベンチマーク実行
  4. nginxとMySQLのログ設定
  5. MySQL使って時間を取得していた箇所を修正
  6. 商品情報をキャッシュ
  7. 昼食
  8. websocketの接続先を分散させる
  9. 商品の価格などをキャッシュ
  10. 部屋ごとのゲーム状況のキャッシュ
  11. 購入可能アイテム判定時の無駄処理削除
  12. 4台でアプリを実行する
  13. 再起動後に不整合が発生しないように修正
  14. ログ消し
  15. そして、優勝
  16. 懇親会

流れだけ見るとキャッシュが多かったですが、不整合をおこさず、 再起動後も部屋への参加一人目が少し遅くなるのみで、再起動前の状況は再現されるので、 そこまでめちゃくちゃなことではないかなと思います。

レギュレーションをみんなで読む

とりあえずチームメンバーみんなで読みました。
websocketの接続先を指定する方法があるんだね、ぐらいのことは言ってました。

とりあえず初期状態でベンチマーク実行

とりあえず初期実装のgolangにアプリケーションを変更して、ベンチ回しました。
なんだかよく覚えていませんが、なんかわちゃわちゃして、実行するまで時間がかかってしまいました。

この間に私はアプリを読んでいましたが、big.Intが使用されており、ありえないほど大きな数字を扱っているので、MySQLやRedisでうまいことするのは無理そうという感想だけつぶやいていました。

何も触らない状態での実行でscoreは 10401

nginxとMySQLのログ設定(ken39arg, mizkei)

とりあえず、nginxとMySQLのスローログを吐くようにしました。
結果から言うとこの2つはほとんど意味はありませんでしたが、私が設定するのに手間取りました。

デプロイ出来るようにする(suzuki)

とりあえず、変更を反映する際にコマンド一発でできるようにしてもらいました。
これもケアレスミスなどあって、少し導入に時間がかかってしまいました。

MySQL使って時間を取得していた箇所を修正(ken39arg) 12:04

MySQLcurrent_time を利用して現在時刻を取得していた箇所があったため修正。
導入で色々もたついていたので、一個目の小さい変更すらもう昼を過ぎていました。

scoreの記録はなし(確か変化なかったと思います)

昼食

豪華なお弁当が支給されましたので、美味しくいただきました。

商品情報をキャッシュ(ken39arg) 12:31

購入する商品は変更のないデータであったため、起動時にメモリにのせるようにしました。

scoreは初期と全く同じ 10401

websocketの接続先を分散させる(mizkei) 13:07

ここまで弁当を美味しく食べるぐらいしか仕事していなかった私ですが、 websocketの接続先を分散させることにしました。

このアプリケーションでは最初にリクエストしてきた時にどこにwebsocketをつないで欲しいかをクライアントに返却することができます。
とりあえず部屋への参加リクエストが来たら、まずメモリにある情報から接続先の部屋を探し、なかった場合はMySQLに部屋名を挿入するようにしました。
また、MySQLへのインサートがduplicateだった場合(mysql.MySQLError.Number == 1062)はselectをして、接続先を引き、メモリに保存しました。
これにより、同じ部屋の人たちは同じサーバーに接続することができるようになるため、その後のキャッシュもしやすくなる予定でした。 もちろん、どれか一つのサーバーが再起動されてもDBから引いてくるので確実に同じサーバーになります。
ただ、websocket以外のリクエストはあまりにも少なかったので、ベンチマークがリクエストする対象は1つだけに設定しました。

この修正自体はscoreをあげるものではなかったので変わらず 9784

商品の価格などをキャッシュ(mizkei) 15:47

一つ前の修正からだいぶ時間が空いていますが、この間にken39argがMySQLを使ってなんとかうまく処理する方法を見つけようと模索し、私はうーんと唸っていました。

各サーバーのCPU、メモリ、帯域、スロークエリ、などなど、どれも原因ぽさがないので、どうしようかと思い、 とりあえずpprofを入れて、重いやつのキャッシュをはじめました。

商品の価格情報などは単純な値ではなく、計算式があり、購入やステータスを出すたびにそれを計算していたため、その計算が発生しなくなるよう、キャッシュをおこないました。

しかし、scoreは何の変化もなし

部屋ごとのゲーム状況のキャッシュ(ken39arg) 16:07

MySQLを利用して1時間ほど悩んで実装していたそうですが、駄目ということになり、その実装はマージされることなく、closeされました。

そこでだいぶ大きめのキャッシュ修正を入れました。
椅子をクリックした際に増加する椅子について、MySQLから引いていたデータをメモリに持たせるようにして、 集計にかかる時間を短縮しました。
websocketの接続先を分散させる修正で同一の部屋であれば、必ず同一のサーバーに接続しているので、部屋ごとの情報をキャッシュしました。

ただ、何回か一回は落ちてしまったので、なかなかマージできず、16時を過ぎて、ようやくマージされました。
また、ほぼ同時に商品購入履歴についてもキャッシュを導入しました。

ただし、scoreは落ちて 6504

購入可能アイテム判定時の無駄処理削除(mizkei) 16:39

アプリを書いていた私とken39argは何をしているかを確認するために、互いのPRを見るようにしていました。
そして、私はどう考えてもここまでの修正でスコアが上がることはあっても下がることはないだろう、 と思っていたため、なにか勘違いしている可能性があると思い、一人でレギュレーションを再度読みました。
ここで「1秒以内にステータスやリクエストが返ってこない場合、クライアントは切断します。」という記述を見つけました。
ISUCON常連であれば当然レギュレーションをしっかりと読んでいるでしょう。しかし、私達のチームはほぼ初心者の集まりなので、その部分が抜けていました。

ここまでの修正はスコア計算の計算式に入るaddIsuやbuyItemを高速化する修正がメインで、ステータスの取得は放置状態でした。
そこでステータスを計算する際に1ms毎の未来を1000ms先まで計算する処理(つまり、1000回ループする)で、 big.Intの比較処理があまりにも時間をとっていたので、その部分をループの外で計算しておき、計算を減らしました。

ここでステータスを返す処理が1秒に収まったのでしょう。 これまでのすべての変更が効いて、scoreは 48745

4台でアプリを実行する(ken39arg, suzuki) 16:47

DBのいるサーバーのリソースがだいぶ余っているので、DBのサーバーでもアプリを動かすようにしました。(suzuki)

また、DBのサーバーの負荷を上げ過ぎないようにするために、DBにたてたアプリにはwebsocketの接続は少なくなるようにして、4台で処理するようにしました。(ken39arg)

scoreは伸びて 57103 に達し、特別賞獲得。

壁を作りました。

再起動後に不整合が発生しないように修正(ken39arg) 17:11

椅子追加や商品購入のキャッシュは再起動で失われる実装になっているのは承知していたので、 レギュレーション通り、任意のタイミングで再起動されても問題ないように、 部屋作成(再入場)時にMySQLからデータの復元をおこなうようにしました。
この段階から再起動して、ベンチマークを回しつつ、再起動後もちゃんとデータが復元されることを確認していました。

ログ消し(ken39arg) 17:45

ログ出力やpprofなどを消しました。

そして、優勝

正直、運が良くて準優勝だろうという話をしていました。
理由として、2,3位のチームは30000点でしたが、ISUCON強者はまず2台で最適化して最後にスケールさせるという話を聞いており、前年も1時間を切ってから、スコアを伸ばしているので、最初から4台すべて使っていた私達のチームは10万点が必要なのでは、と思っていました。

結果は優勝で、非常に嬉しかったですが、まだできることがあったのでやりきったという感じはありませんでした。
あとまだ誰もに気づかれていないと思いますが、記念写真の時にトロフィーの裏表も間違えていたので、その点も心残りです。

懇親会

入刀しました。

懇親会で出題者の方に話を聞いて、私達のチームでもまだまだ全然想定されるところまで達していないことを知りました。
非常に悔しい。

まとめ

ISUCON7で優勝しました。
運営の方々はありがとうございました。本当にお疲れ様でした。
そして、私が社内でISUCON参加メンバーを募集した際に、手を上げてくださったチームメンバーの二人にも感謝です。
応募の締め切り一週間前までメンバーが揃っていなかったので、出場せずに終わるかなとかも思ってましたが、無事出場することができてよかったです。