ISUCON9 に参加しました
初参加で無事敗北しました。お疲れさまでした #isucon pic.twitter.com/BbdD6PKAQL
— ふるさっくす (@furusax) 2019年9月8日
というわけで、会社の人に誘われて ISUCON に参加してきました。
ISUCON って?
お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル、それがISUCONです。 (公式Blogより引用)
スマホアプリ全盛期の昨今、そのバックエンドで動いてるサーバーの役割はとても大きいです。
ゲームとかアプリのサービスイン初日にサーバーが落ちる……なんてことがよくありますよね。
最近だとポケモンマスターズとかファミペイあたりで発生しました。セブンペイ?なんですかそれは
もはやサービス初日にサーバーがダウンするなんて日常茶飯事になりつつありますが、もちろん落ちないに越したことはないわけです。 ISUCONは自分で運営しているWebサービスがそんな状況に陥った時、どうすればその危機から脱出することができるのか、 インフラ/サーバーサイドエンジニアの実践的な対応力を競う大会です。
ISUCON9 について
1〜3人のチームで出場します。去る9月7日と8日にオンライン予選会が開催されました。 オンライン予選で勝ち抜いた上位30チームが10月5日に開催される本選に出場できます。
ちなみに、運営はLINE、問題作成はさくらインターネットとメルカリ(ここ重要)、サーバー提供はAlibaba Cloudでした。
事前準備
過去問を解いた
さすがに無策で突っ込むと死ぬのは目に見えていたので、一週間前の休日を利用して過去問を解いてみることにしました。 解いたのは前回大会 (ISUCON8) の予選問題です。
その時の流れは省略しますが、最終的に講評にも書いてある GROUP BY event_id HAVING MIN(reserved_at) = reserved_at のクエリはMySQLでは結果が不定になる
問題にブチ当たって撃沈しました。
これはヤバイんじゃないか……と漠然とした不安を抱えながら当日に突っ込みました。
対策メモを作った
弊社でQiita Teamを契約しているので、そこに当日使いそうなコマンドとか設定をあらかじめ記載しておきました。 そうすると当日いちいち調べなくてもコピペするだけで良いので安心です。
例えば、普段MySQLしか使ってないのにPostgreSQLで実装とかされたら確実に死んでしまいます。 当日使いそうなコマンドについては、下のようにMySQLとPostgreSQLの対応表を作りました。
当日について
【業務連絡】起床成功しました
— ふるさっくす (@furusax) 2019年9月7日
リポジトリはすでに公開されているので、どんなことをしたかはここを追いかければ良いです。
基本的に私がアプリ側の改修、 @ManabuSeki がインフラ(サーバー、Nginx、MySQL)周りのチューニング、 @shogo82148が全体的なフォローや旗振りを担当しました。
作戦
もはやどこのポストを読んだのか覚えていませんが、 alp
コマンドでアクセスログを集計してボトルネックを探す方法をとりました。
他に変わったことはあまりやってません。 せいぜい、残り1時間になったら再起動試験をしっかりやって失格にならないようにしたことぐらいです。
自分がやったこと
まず GET /users/transactions.json
が死ぬほど重いことがわかったので、ここから手をつけました。
items
テーブルに全然インデックスが貼られてなくてフルスキャンされていたので、色々迷いながら複合インデックスを貼りました。
https://github.com/shogo82148/isucon9-qualify/pull/1
その後、同エンドポイントのN+1問題が気になったので、 items
テーブルのSELECTに users
テーブルをJOINしてみました。
https://github.com/shogo82148/isucon9-qualify/pull/3
これの改修で結構バグを生んでしまい、結構な時間をかけてしまいました。 そしてせっかく実装したのにあまりスコアが伸びなかったのも辛かったです。直前に @shogo82148 さんが入れてくれた カテゴリーテーブルのオンメモリ化 の方がはるかに効果的でした。今思うとそこまで時間をかける改修内容じゃなかった気がします。
ここまで来てもまだ重い。と思って実行時間を調査するログを仕込んでみたところ、このエンドポイントの処理時間のほとんどが 外部サービスAPIの応答時間に支配されていることがわかりました。こいつです。 このAPIコール1回あたり数百msの時間を要していて、for文の中で複数回呼ばれていました。 さすがにこれは遅いよねということでgoroutinで並列化したところ、スコアが一気に倍に跳ね上がりました。
https://github.com/shogo82148/isucon9-qualify/pull/4
さすがにこれはテンション上がりました。たった一瞬ですが暫定4位に躍り出ます。
その後、ソースコードとにらめっこしながらいくつか改修を入れましたが、大きな効果は得られませんでした。
https://github.com/shogo82148/isucon9-qualify/pull/5 https://github.com/shogo82148/isucon9-qualify/pull/6 https://github.com/shogo82148/isucon9-qualify/pull/7
この辺りで単純な改修は万策尽きた感が出てきまして、キャンペーンを打ち出すことを考えます。
https://github.com/shogo82148/isucon9-qualify/pull/9
当然のようにサーバーが負荷に耐えられなかったので、MySQLを専用サーバーに逃がしたり、アプリを複数台で立ち上げて負荷分散したりとインフラ側で調整が行われましたが、 この辺りから 私にできることが無くなってしまっていました。無力である。 ただ、最終的にキャンペーンを始めると件の外部サービスAPIが 大量の502エラーを返してベンチマークが通らなくなるという事態に陥ってしまい、 泣く泣くキャンペーンを諦めて競技終了となりました。
最終スコア
上記のような修正に加え、インフラ側をWeb+アプリとDBの2台構成にした結果、 8,050 イスコイン
で着地しました。
最終順位は57位。全参加者の上位10%には残ったものの、残念ながら本選に行くことはできませんでした。
予選を終えて
前回の予選問題を解いた時の教訓を元に、アクセスログの解析からボトルネックを探して順番に解決していきました。 一部いい感じにハマった改修もありましたが、全体を通すとあまり効果的でない部分も多かったと思います。
当日の感触と他の参加者の感想とかをサラッと読んだ感じから、どうもキャンペーンを打ち出せないと本選にはいけないような雰囲気でした。 確かに予選のマニュアルにも、キャンペーンを打ち出すとユーザーが爆発的に増えるって書いてあります。 結構暴論ですが、最初からキャンペーンを打ち出してからベンチマークが通るまで改修しまくる、という作戦の方が有効だったかもしれません。
あとは私自身のGoスキルが低すぎて、半ばモブプロのような形になってしまいました。 私自身たくさん勉強になったのですが、もう少しチームに貢献できる部分があればよかったな……
ともあれ
初出場でしたが、とても楽しく予選を戦うことができました。 問題はユーモアが効いてるだけでなく、自分たちでは改修できない外部サービスAPIが重くてボトルネックになるという 最近見たどこかの世界で本当に起こっていそうな実践的な内容でした。 講評や解説、他の参加者のポストが出てきたらまた色々振り返りたいと思います。
というわけで、運営の皆様、一緒に参加してくれたお二方、ありがとうございました。 来年こそは予選突破して本選に進みたいです。