ぽよメモ

レガシーシステム考古学専攻

ISUCON12予選突破の記録~絶望の4年間~

はじめに

ISUCON12予選にチーム ツナ缶 として参加し、ギリギリ滑り込んで初めて予選を突破したのでその備忘録です。
下記の様な分担で参加していました。

  • pudding(自分):インフラ担当
  • taxio:アプリ担当
  • shanpu:アプリ担当

この記事では過去の参加記録と共に、主にインフラ周りについて書いています。

インフラ担当ではあるのですが普通にGoのAPIサーバは書けるので、インデックスを貼ったり細かいコードの改善は自分も担当しました。

これまでの記録

大会名 結果 備考
ISUCON8 予選落ち taxioと二人で参加
ISUCON9 予選落ち shanpu参戦
ISUCON10 予選落ち taxioとpuddingは社会人になって初参加
ISUCON11 予選落ち サークルの後輩が初参戦で本戦に行き自信喪失*1
ISUCON12 本戦出場 shanpuは社会人になって初参加

初参戦はISUCON8だったようです。リポジトリを見たら「わからん」って書いてありました。わからんかったらしい。

ISUCON8のリポジトリのメモ

今回で5回目の参加ですが、タイトルは3人チームになってからの4年という感じで受け取ってもらえれば。
これまでは上位25位にかすりもしない結果で、上位チームの解説を読んで「これをこの時間で思いつく人たちに勝たないと上位には行けないんだな」と自信をなくしていました。

社会人になって1・2年目にもそれなりに経験値は上がったような感覚はあったものの、結局上位入りは遠く彼方にあり、少し諦めに似たような感覚がずっとありました。競技自体は面白くとても楽しめるので参加しているが、予選突破は半ば諦めているような心情でした。

参加前の準備

方針の決定

2週間ほど前にオンラインで会議をしました。

ISUCON10・ISUCON11ではNewRelicを使ってプロファイリングを進めていましたが、以下の様な課題がありました。

  • 導入に取られる時間が多い
  • 撤去するのもそれなりに大変
  • データが取れる粒度が粗い(NewRelic Infrastructureだと30秒ごととかだった記憶)

ISUCON11ではNewRelic Infrastructureをやめてhtopなど基本的なコマンドに変更しました。実際のところこれで大きな問題は無かったため今回はコマンド類 + netdataにしました。netdataは1秒ごとのデータが取れてインストールも簡単なので便利ですね。各ホストへはsshでポートフォワードして接続することとしました。

アプリ側でも一度基本に立ち返り、NewRelicをやめてpprof + alp + pt-query-digest に戻ることにしました。この段階ではもし無理だったらNewRelicを入れようという話をしていましたが、結局入れることはありませんでした。

改善〜デプロイ〜ベンチを回すサイクルの高速化

ISUCON8〜ISUCON10までは、自分がインフラ担当として全員の変更のデプロイを担うという構成にしていました。しかし、この構成では改善の適用速度が自分によって律速されてしまうため、ベンチを回すコストが高くなってしまっていました。

そこでISUCON11からは一人一台サーバを割り当てて各自が自由にデプロイできるようにすることとし、ansible playbookとMakefileを用意しました。構成はおおよそ以下の様になっています。

.
├── Makefile
├── TARGET
├── ansible.cfg
├── backup
│   ├── isu1
│   │   ├── etc
│   │   └── lib
│   ├── isu2
│   │   ├── etc
│   │   └── lib
│   └── isu3
│       ├── etc
│       └── lib
├── inventories
│   ├── group_vars
│   │   ├── all.yml
│   │   ├── db.yml
│   │   └── nginx.yml
│   ├── host_vars
│   │   ├── isu1.yml
│   │   ├── isu2.yml
│   │   └── isu3.yml
│   └── hosts.yml
├── playbooks
└── roles

Makefilemake deploy すると現在の状態をデプロイできるようにansible-playbookコマンドを叩く単なるラッパーです。

TARGETファイルがキモで、デプロイ先のホスト名を書いておくと make deploy する際に ansible-playbook --limit $(cat TARGET) するようになっています。これによって全員が自分の担当するサーバにのみプロビジョニングを実行します。複数台構成になってきたときにも対象ホストをカンマ区切りで増やすだけで対応出来るため、最初から最後まで各自の手元から make deploy するだけでデプロイできるようになっています。

backup以下にはサーバの /etc や /lib 以下の変更したいファイルを最初にバックアップしておき、それを編集してansibleで適用します。インフラ担当である自分が一台で検証した内容を他のメンバーのサーバにも容易に展開できます。また改善が進んで複数台構成になると各サーバの設定は少しずつ異なるものになりますが、最初から別々にしておくことで無理なく表現することが出来ます。

inventories以下には各種変数の定義が、playbooks・rolesにはそれぞれplaybookのyamlやroleのyamlが置いてあります。アプリのデプロイに直接関係があるもの以外に例えば

  • スロークエリログを有効/無効化する
  • netdataをインストールする
  • 指定したファイル/ディレクトリのバックアップを取る
  • ログをローテートする

などのロールを用意していました。

当日の動きとしては以下の様になります。

  1. 担当するホストを決めておく
  2. puddingがssh configを用意してメンバーに展開
  3. 初期バックアップを取ってplaybookを微調整
  4. 後は各自で make deploy

ドキュメントの用意

今まではリポジトリのissueにスニペットやリンクをペッと貼って終わりにしていましたが、毎年やっていて無駄だなと思うようになったので今年からは簡単なドキュメントを用意しました。

isu12q/docs at master · pddg/isu12q · GitHub

  • アプリケーションログの見方
    • journalctlの使い方など
  • スロークエリログの解析の仕方
    • mysqldumpslowの見方
    • pt-query-digestの見方
  • アクセスログ解析の仕方
    • nginxのログフォーマット
    • alpの使い方
  • サーバの状態の観測方法
    • netdataへのアクセスの仕方
    • htopの使い方
  • MySQLの非同期レプリケーションに関する簡単なメモ
    • 結局使わなかった

競技中は細かい説明をすることなく「ドキュメントがあるので見て」と言って済ませていました。もう少し充実させたいですね。

当日やったこと

自分がやったことは細かい改善と構成変更が主なので、直接スコアに響くことは少なめでした。アプリ側の改善は他の二人のブログをご参照頂ければと思います。

10:00〜11:00 初期セットアップ

ssh configを書いてデータをバックアップしてalpのためにログフォーマットを仕込んでスロークエリログを有効化して……などをやっていました。isucon10の予選問題を参考にplaybookを仕込んでいたのでいくつか微調整が必要となり、結局50分ほどここで消費していました。その間taxioにはレギュレーションやコードを見てもらっていたので、完全に手を止めさせていた時間は少なかったと信じています。

11:00〜11:30 作戦会議

アプリについて認識を合わせた後、各自でコードを読んで改善ポイントのあたりをつけました。
このあたりで既にSQLiteからMySQLへの移行はやめておいた方がいいのではという話になっていました。結局負荷の要因がMySQLに移るだけで、かけた時間に対して明確にスコア改善に繋がるか怪しいという話をしていた覚えがあります。

11:30〜13:45 細かい改善・レビュー・バグ取り

自分は主にメインのロジックに関わらない細かい改善を担当し、public.pemの読み込みを一回にしたり*2MySQLの方にインデックスを張ったりしていました。

github.com

13:45〜14:30 docker外し

アプリケーションはdocker-composeで動いていたため、余計なオーバーヘッドかかってる可能性ありそうということで外しにかかりました。結果スコアへの影響は明確にはありませんでしたが……(そりゃそう)*3

remove docker-compose by pddg · Pull Request #26 · pddg/isu12q · GitHub

なぜか isuports package の方をビルドして動かねぇ……とか言っていた無駄な時間があり、本当に無駄でした。普段なら3秒で気付けたはず……思うようにスコアが伸びず焦っていたんでしょうね。

14:30〜15:15 MySQL移行を諦めきれず裏でトライ

sqlite3-to-sql が吐くダンプを見て厳しそう〜と思いつつ、数十分程度で終わるなら十分アリだなと思いとりあえず簡単にスクリプトを組んで試していました。参加した方なら分かると思いますが、1.db すら終わることは無かったのでサクッと切り上げて辞めました。

この時点で最後までテナントのDBをMySQLに移すことはせず現状で改善していく方針とすることになりました。

15:15〜16:00 SQLiteへのクエリでトランザクションを使う

SQLiteに詳しいわけではないのですが、調べている限り多数のINSERTではトランザクションを使うと高速化するらしいということがわかっていました。どうせflockを外すためにはトランザクションが必要であったため、半信半疑でトランザクションを入れて一部のflockを外しました。
use transaction to insert into tenant database by pddg · Pull Request #33 · pddg/isu12q · GitHub

このときshanpuが取り組んでいた別の改善(row_num外し)*4と合わせるとスコアが15000を超えそこまでの最高スコアの2倍以上になったので、競技中で一番興奮しました。

16:00〜16:30 サーバ1台をMySQL専用にする

そろそろ自分が専用のサーバ一台を抱えているのは無駄になってきたのでそれをMySQL専用とすることにしました。
use isu3 as database by pddg · Pull Request #39 · pddg/isu12q · GitHub

  • bind-addressを書き換える
  • 残りのサーバの環境変数をいじってそのサーバに向ける
  • ベンチ回して計測

この時点でMySQL専用サーバはメモリ・CPU共に余力があったので改善のウェイトを下げました。

16:30〜17:00 無駄なWHEREを削る

shanpuとtaxioが裏でバンバン改善してくれていたので、自分は相変わらずちょっとした改善を続けていました。
remove tenant id condition by pddg · Pull Request #45 · pddg/isu12q · GitHub

SQLiteの移行を諦めるという選択をとったことで、テナントDBに対するクエリで tenant_id でフィルタしている部分は必要なかったので外しました。テナントDBへの問い合わせなのか、MySQLの方への問い合わせなのかを目grepして確かめていたので、やることに対しておもったより消耗しました。

17:00〜17:15 テナントDBにインデックスを張る

ずっと手を付けずにいた部分にようやく手を付けました。各サーバの ${HOME}/initial_data 以下にある各データベースファイルにクエリを流し込みました。
が、あんまりちゃんと計測せずに適当に残っている player_id と competition_id にだけインデックスを張ったのはあまり良くなかったなと反省しています。

レギュレーションを読んでinitial_dataに手を入れることに問題は無いはず、とメンバーと共に何度も確認しましたが競技終了後に3回レギュレーション読み直したくらい不安になりました。

17:15〜17:30 サーバ1台をnginx専用にする

netdataを眺めているとnginxが5~10%くらいCPUを食っていたので別サーバに分けました。
use isu1 as nginx, isu2 as app, isu3 as db by pddg · Pull Request #51 · pddg/isu12q · GitHub

ここで最終構成が確定し、nginx、アプリ、MySQLにそれぞれ1台ずつ割り当てた3台構成になりました。

この頃はどうやったらテナントDBをシャーディングできたか、みたいな話をしていた覚えがあります。力不足を感じますね。

17:30〜17:45 再起動試験

そろそろやろうということで再起動して試験しました。単に各ホストへsshして sudo reboot し、ベンチが回るか確認しただけです。

チームメンバーには言っていませんでしたがさらっとisucon-env-checkerも叩いていました。

17:45〜17:55 flock外し

トランザクションを使うようにしていたので実際このflockは必要なくなっているはずでした*5
ただ不安だったので1箇所外してはベンチを回し、を繰り返して落ちたらその時点で切り戻すという判断をしました。

不整合で失格などにはならなかったのですが、逆にスコアが下がったので1箇所だけ外して残りはそのままとしました。

Rm flock by pddg · Pull Request #54 · pddg/isu12q · GitHub

最終スコアは 23158 でした。🙏🙏🙏🙏 祈り 🙏🙏🙏🙏 を捧げてゲーム終了としました。

1時間前のスコアと比較すると10位以内に入っていて、今までの中では最も予選突破できる可能性が高そうということで、嬉しくなってスクショを撮りました。

競技終了間際のダッシュボード

感想戦でプロファイリングツール外したらスコア爆伸びした報告がよく見られたので、再起動試験で大量にふるい落とされない限りこれは厳しいな……と思っていました。翌日予選突破とわかり、リアルにちょっと飛び跳ねました*6

現実を受け入れられない僕

アプリ担当のやったこと

このままではインデックス張ってトランザクション使って複数台分散しただけで本戦行けたように見えてしまいそうだったので、アプリ担当がやってくれたこともメモ程度に書いておきます*7

振り返り

良かったところ

  • 一人一台を割り当て全員がデプロイできるようにすることで、インフラ担当が他のことをやっている間にもアプリの改善を進めてもらうことができた
  • スロークエリログやアクセスログの解析ドキュメントをまとめていたので、当日はそれを参照すれば十分ボトルネックの解析ができた
  • MySQLのインデックスについてちゃんとカバリングインデックスを意識することができた
  • ちゃんとスコアが上がったことで自分たちが積み重ねてきたものに対して少し自信が付いた

改善したいところ

  • デプロイ→ベンチ→結果の収集の流れの体験が良くなかった
    • スロークエリログの解析結果やpprofの結果を自動で回収させる仕組みがあるともっと良かった
  • 事前に用意したansible playbookの細かいバグで詰まった
    • ログローテーションの設定が壊れていたり、typoがあったり……すぐ分かるミスを事前の検証で洗い出せていなかった
  • アプリ側のロジックと合わせたDBの最適化まで手が回らなかった
    • 例えばvisit_historyはインデックス張るだけでなく、そもそもこんなにデータいらないんじゃないかとかそういう方面に気を回す余裕が無かった

本戦に向けて

nonyleneくんの記事を読んでこれが本物のインフラ担当ってやつか……ってなりました。

nonylene.hatenablog.jp

普通にこれを真似るのもよさそうではありますが、それは来年にとっておこうかなと思っています。既に慣れたこの仕組みをブラッシュアップした方が本戦で迷わず済みそうな気がするので。
過去の本戦問題もまともに解いたことがないので、初の本戦を全力で楽しんであわよくば100万円もらいたいの気持ちで行きます。

おわりに

特に今回ISUCON初参加した人の中には「一生予選突破できないのでは」と感じている人がもしかしたらいらっしゃるかもしれません。毎回本戦出場みたいな激つよチームばかりが勝っているのではなく、4年同じチームで挑戦しつづけようやく突破できたみたいなチームもあることを知ってもらい、次への活力としてもらえると嬉しいです。


*1:この後輩たちは今年も本戦出場並のスコアをたたき出しており、若者こわい

*2:最終的にtaxioが中身をまるごとコードに埋め込んでくれました

*3:このPR見るとわかりますが普通に環境変数が間違っています。デフォルト値でうまく動いていたので気付いていなかったというオチでした。後で気付いて直しています。

*4:taxioの環境でflakyな整合性エラーによる失格が発生しており、一度revertされてしまいましたが最終的には取り込まれました。なおこのflakyなエラーはtaxioが initial_data を手でいじってしまっていたことによるものでした。即座に他のサーバから初期データのダンプを取ってきて上書きする、という判断を取れずコードが悪いのかずるずる考えてしまったのは大変良くなかったです。反省。

*5:flockではなくmutexにするアイデアも最初に出たのですが、mutexのmapを触る操作でグローバルなロックを取らないとrace conditionでダメなのでは、という話になって実施しませんでした。他のチームを見ていると導入したところもあったようなので、うまくやればできたんでしょうか?

*6:挙動不審だったので喫茶店の店員さんにビビられました

*7:二人はブログ書いてくれないらしいので……