isucon12 チーム「パカパカアルパカ」で予選突破しました #isucon

昨年と同じく前職TVer同僚の @わんこ@teraken とチーム パカパカアルパカ として isucon12 に参加してきました!
昨年、そして数年前もいつも良いところ(30-40位くらい)で本選出場できなくて悔しい思いをしてきましたが、今年はついに初本選出場出来ました!嬉しい!

スコアは 30616 でした(最高 35383)。14位でした。

isucon.net

最終構成

こんな感じ。サーバ分散ほとんどできてないのが悔やまれる…

  • App1
  • App2
    • Goアプリ / SQLite (結果的にはちょびっとだけ負荷分散されてる気がする)
  • App3
    • なにもしてない (本当はapp2と同じ構成にするつもりだったけど app2 が想定より使われなかったので却下)

なお、SQLiteMySQL化は しておりません


問題テーマ

isuportsという、isuconシステムをSaaS化し企業に提供するというマルチテナントサービスのリーダーボードの高速化をして欲しい!というテーマでした。
isuconのisucon化?


前提:チーム内での進め方について

今回、進めるにあたって事前にチームでいくつか共有したことがありました。その中で以下の2つがうまく機能したかなと思います。

  1. レギュレーションは みんなで ちゃんと読む
  2. コスパの良い改善から手を付ける

1.については、過去の経験からわりと個々に流し読みしてしまってスコアに関する重要な記載を見逃すことが多かったのが反省でした。そこで今回は画面シェアしてみんなで読み進めるということをしました。
これが結構良くて、自分が見逃してるところを「ああこういうことねー」とか言いながらみんなで補填しあって進められ、内容をほぼほぼ理解して進められたのがよかったです。

2.に関しては過去の反省も踏まえて、効果ありそうでもいきなり難易度の高いところを手を付けるのではなく 明らかに改善できて簡単にできそうところから着実に潰していく というのを意識しました。
これをしていくことによって変なところでハマらず序盤から着実にスコアアップすることができ、結果としてこの積み重ねが生きて正しくスコアアップ出来たと思います。

いきなりのやらかし

レギュレーション確認後、自分のAWSアカウントでCloudFormationのスタックを起動したのですが、なんとエラーが出てしまいます。
めちゃくちゃ焦って運営さんにも質問してしまいました…お手数をおかけしまして申し訳ありませんでした!
そして右往左往した結果ログインしているAWSアカウントが間違っていることが発覚し、メンバーのサポートによって正しいアカウントでスタックを作り直すことが出来ました。これは本当に焦りました…。ここで15分くらい潰してしまったのが悔やまれます

ちなみにこれは完全にフラグでした(実はログイン成功していなかったという 😇


メンバーロール

以下のような感じです。バランスが良い。

  • toritori0318: 全体設計したり足回りやったりアプリ周り改善したりデータストア周り改善したり
  • わんこ: アプリゴリゴリ改善してく
  • teraken: 全般で細かい部分を拾ってくれる。メンタル強

初手

みんなの考察(他にもあったかも)。

  • 外部認証は前回と似てる感じっぽい
  • 429ステータスコードはよくわからんけど一応頭に入れておく
  • とりあえずリーダーボードはRedisだよね。あと採番やキャッシュでも使えそうなので確実に使う方針で
  • アプリがdocker-composeで動いとる
  • SQLite????しかもいっぱいファイルある!どうしよ
  • MySQL側はデータ少ないけどSQLite結構いっぱいある…
  • flock?

SQLiteどうするか?問題

SQLiteなんてまともにチューニングしたこともなく本当に悩みましたが、以下の理由によりMySQLに置き換えるのは一旦やめました。

  • おそらく移行だけで相当な時間が取られる(データ移行だけじゃなく、データ整合・コード変更など影響が大きい)
  • かつ、MySQLに移したからといってパフォーマンスが上がる確信が持てなかった
    • 既にファイル分散できているし、実は使い方によっては現状の構成のほうがパフォーマンス良い可能性もある
  • SQLiteのサーバ分散対応に関してはいくつかアイデアがあったのでなんとかなるかも
  • いくつかはRedisに持たせられそう

結果MySQL移行はリスクが高く不確実性も高そうだったため、最初の方針に則りまずは コスパの良いできる改善を着実にやっていく ことにしました。


やったことまとめ

※結構口頭ベースでやり取りすることが多く、あんまりSlackに残っていなかったため間違っている部分もあるかもしれません!!また、分かりづらい文章になってしまってすみません。

序盤

自分はとりあえずいつもどおりデプロイシェル、モニタリング系のツール入れたりalias入れたり周辺準備を進めてベンチ実行。alp / pt-query-digest / SQLITE_TRACE_FILE / pprof などで状況把握しメンバーに共有。
やっぱりranking APIをなんとかしないとなーという感じ。

その後は各々以下のような対応を進めていました。

  • @teraken docker-compose剥がし
  • @toritori0318 sqliteのdistinctうんたら〜のとこにindex貼る
  • @わんこ idgen Redis化
  • @toritori0318 MySQLのGROUP BY狙いindex
  • @わんこ visit history Redis化

この辺で 6627 点くらい。

SQLite、explainとかできんのかな?とググったら普通に出来そうだったので、時間かかってそうなクエリ計画見ながらインデックス貼ったりして、普通にチューニング進められました。

中盤

429 Too Meny Error時の挙動が気になるので検証コードを@terakenに依頼。

並行して自分もきちんとアプリ見始める。やっぱflock周りがブロックしてそうだなーというところで「試しにこれ一回消してみよ」とflockしてるとこ消してみたら 普通に通った…!?
この挙動、Redis化の対応と関連してるかどうかまでわかってないのですが、とりあえず問題無さそうなのでこのまま採用。
そしてスコアも 10900 へ。

その後、player_scoreのリストからplayer引くN+1を発見したのでこれはサクッと対応。
この時点でスコア 14433 。

init時にはsqliteにindex貼ってたけど、createTenant時にはindex貼ってなかったのに気づいたのでそれを対応。
この時点でスコア 14914 。

ちなみにこの時点でRedisやMySQLは結構空いていて、ほぼ7割くらいはアプリ負荷になってました。

並行して@わんこがガリガリRedis化やバルクインサートでアプリ改善してるので、この辺りでそろそろアプリのサーバ分散考えないとなーということでインフラ構成考えてたりしていました。

閑話休題。アプリの分散・SQLite協調化についての設計

アプリ過負荷だしSQLite分散問題もあるし、どっちも同時に分散したいなー。さてどうするか。。
実はこれは「double(triple) writeでいけんじゃね?」と当初から考えてました。
そして実際POST APIのリクエスト数も少なそうだし、POST APIだけ全サーバに書き込みして、GETを分散する方法。
これは割と業務実績としてもやっていることだったし1考え方は間違ってないはず…

自分は普段からNginx / OpenRestyでかっこよく課題解決するのを生きがいとしているので 2 、Nginxでshadow proxyできるのを思い出し、これ上手く行ったらかっこいいな!と思って実際に使ったことはなかったんですが挑戦してみることにしてみました。

イメージとしては以下のような感じ(keepaliveとかヘッダーは抜いてます)。

upstream app1 {
  server 127.0.0.1:3000;
}
upstream app2 {
  server 192.168.0.12:3000;
}
upstream app3 {
  server 192.168.0.13:3000;
}

upstream app123 {
  server 192.168.0.11:3000;
  server 192.168.0.12:3000;
  server 192.168.0.13:3000;
}

server {

  # 分散対象とする get api (一部)
  location ~ ^/api/player/competition/(\w+)/ranking {
      proxy_pass http://app123;
  }
  
  # mirror対象とする post api (一部) 
  location ~ ^/api/organizer/competitions/add {
      proxy_pass http://app1;
      post_action @mirror2;
  }

  # mirror
  # 全台に反映したいので mirror3に数珠つなぎ
  location @mirror2 {
      internal;
      proxy_pass http://app2$request_uri;
      post_action @mirror3;
  }
  location @mirror3 {
      internal;
      proxy_pass http://app3$request_uri;
  }

はてさて上手くいくのでしょうか…

終盤

そうこうしているうちに @わんこ がplayerキャッシュやらbulkインサートなど対応し、スコア 30000超え!
そして自分も上記のAPI分散実装出来たつもりだったので「これで上手くいけば結構上がるはず…!」という期待を込めて適用してみた所 スコアはほぼ微増… 😇 なんで?
ログ見ると、POSTはミラーリングされてるっぽいけどGET系があんまり分散されておらず、負荷がapp1に偏ったまま。ここは未だに何故nginxのupstreamでラウンドロビン分散してくれないのかよくわかってないので感想戦で検証したい所。

またRedisやMySQLも別サーバに分散しようとした所、initializeで結構重い処理をしているため30秒に間に合わずコケる事象に。結局最終的にはサーバ分散が殆どできてない状態になってしまいました。

そしてなぜかスコアも上がることしかしてないのにだんだん下がっていき、これ以上やってもリスク高くなりそうとのことで再起動チェックすることに。

再起動チェック

残り30分くらいでサーバを1台ずつ再起動テスト。すると 理由もわからずinitializeがコケる事象 が発生し全員焦るw
結局、なぜか「ログオフを適用するとコケる」ということが発覚し急いで戻し。これなんでinitializeに影響があるのか未だにわかってない…。
そしてさらにサーバ2も再起動テストすると、docker-composeを外した新規作成サービスがdisableになっててさらに再起動テスト失敗 😱
この時点で残り10分を切っていたけど焦らずサービスをenableにしてrebootコマンド実行。問題なくデーモン起動されていることを確認。
ここは本当にヒヤヒヤした…

そして最後にベンチ実行して
30616
でFinish!


感想

とにかく楽しかった!やってる最中でもメンバーと「たのしー!」言いながらやってました。
本選も初出場なので本当に楽しみです。 しっかり準備して良い結果が残せるようにがんばります!

運営の皆様、本当にこの大規模な大会で安定したアプリ・インフラのご提供、また素晴らしい問題を作成していただきまして本当にありがとうございました!


2022/7/26追記

@わんこ 視点のブログも公開されましたのでアプリ改善の詳細はこちらをご覧くださいませ〜 techblog.tver.co.jp


おまけ

コミットログ笑った


  1. サービスの無停止データ移行得意です!(泣

  2. 誇張です