壁を感じたISUCON12本戦
TL; DR
- 実力不足を痛感した
- なんとなく計測した気になってはいけない
- 予め用意したものはとても役に立った
- 更にブラッシュアップしていきたい
はじめに
この記事はチームツナ缶 としてISUCON12本戦の記録です。
予選同様、下記の様な分担で参加していました。
もし興味があれば、ぜひ予選のときの記事も読んでみてください。
参加前の準備
ISUCON12予選ではデプロイの自動化などはうまく回っていましたが、反面計測周りに関しての体験が良くありませんでした。具体的には以下の様なフローになっていました。
- アプリやインフラへの変更をデプロイ(自動化されておりコマンド一つ)
- 全てのログをローテート(自動化されておりコマンド一つ)
- ベンチを回す
- 手元でpprofのコマンドを用意してポートフォワード越しに実行(もちろん実行者しか見れない)
- 終わったらサーバに入ってIssueに貼っておいた解析コマンドを実行してコピー
- まとめてIssueなりPRなりSlackで報告
問題は3つあります。
- 手作業が多い
- 実行者しかpprofの結果をみれない
- 結果が一箇所にまとまっておらず、毎回どこにあるか探すことになる
これらを一挙に解決すべく、Ansibleでベンチ前後の処理を自動化しました。make bench
すると以下の様な処理を実行するようになっています。
- スロークエリログを有効化
- 全てのログをローテート
- ユーザのインタラクションを待つ(ベンチリクエスト待ち)
- Enterが押されたらアプリケーションホスト上でpprofでプロファイリング
- ユーザのインタラクションを待つ(ベンチ終了まで待ち)
- Ansibleのinventoryに従いDBグループならpt-query-digestの結果を、nginxグループならalpの結果を取得し、レポートのMarkdownを生成しローカルにダウンロード
- pprofのダンプファイルをローカルにダウンロード
- ダウンロードしたレポートを一つのMarkdownにマージし、ローカルホストからghコマンドでIssueに投稿
これにより以下の様なレポートがIssueに投稿されます。本文が長すぎて投稿出来なかった場合でも、ローカルに同じ内容のMarkdownのファイルが残るのでそれを見ることが出来ます。
https://github.com/pddg/isu12f/issues/1#issuecomment-1229147907
結果としてベンチ前後の処理が大幅に楽になりました。
(pushしていなかったりしてcommitがない場合もあるので必ずでは有りませんが)現在のHEADを指すコミットハッシュをレポートに入れているのでどの時点の結果なのかよくわかるというのも良い点でした。
またpprofの実行結果を全ホストからダウンロードする方法も1コマンドで用意し、他のメンバーの実行したpprofでも簡単に見ることができるようになった点も大きかったです。
今回の出題内容
クッキーの代わりに椅子を生産するクッキークリッカーみたいなゲームに関する問題でした。
ユーザは椅子生産を効率化するアイテムでより大量の椅子を生産し、ログインボーナスを獲得してガチャを回してより強いアイテムを……みたいなサイクルを回すゲームになっていました。
アプリケーションの実態としては単なるWebアプリですが、フロントエンドのアプリケーションが運営が用意したCloudFrontから配信されていたのが少し驚かされる点でした。過去問再現が難しそう……
また、サーバが5台用意されていたところもこれまでとは違う点でした。幸いAnsibleではinventoryに2つホストを足すだけで良かったので、対応は難しくありませんでした。
やったこと
自分はインフラ担当なので主にその立場でやったことになります。
2022/8/28 20:14 追記
一時的にコードを非公開にしました。実行中のインスタンスが停止次第、再度公開する予定です。
10:00~11:00 初期セットアップ
nginx・MySQL・Go製のアプリケーションとほぼいつも通りの構成であり、迷うところはありませんでした。
alpやpt-query-digest、netdataなどを入れて、nginxやMySQLの設定をバックアップ、nginxのログをLTSVにしたりMySQLのbinlogを無効にしたりしていました。
あとはmitigations=offを入れたりしていました。ちなみに/proc/cpuinfoを見たらAMD EPYC 7763だったのでちょっとびっくりしました*1。
初期スコアは600くらいでした。
11:00~12:00 作戦会議とか
アプリの仕様の共有をして、取得されたalpの結果やスロークエリログからとりあえず方針を決めました。 まずサーバが5台あり、1人1台割り当てても2台余ることから、初手でDBを別ホストに分けました。 後はインデックス貼ったりN+1潰したりをやっていこうという方針で行きました。
- DBサーバの分割
- generateIDという関数が単一のテーブルからあらゆるオブジェクトのIDを採番している部分を適当に
time.Now().UnixNano()
に変える- Fix generate ID by pddg · Pull Request #15 · pddg/isu12f · GitHub
- 結果として後からUnixNanoでは重複が出てだめだったので乱数に切り替えています。
このあたりは何かやるだけスコアが上がるのでとても楽しかったです。
12:00~13:00 obtainItemの闇に触れる
obtainItemという関数があるのですが、こいつはobtainという名前に反して内部では状態の更新をしており返り値はどこからも利用されていませんでした。
ヤバすぎる関数なのでなんとかしたかったのですが、これの調査をしているだけでかなり時間を持って行かれたのが痛かったです。同時期の他の変更とのコンフリクトを避けるため、ひとまず内部の処理を関数に分けるリファクタを加えました。
13:00~14:00 admin関連のテーブルを別DBに分離しようとしてベンチが落ちる
DBの負荷が相変わらずなので、一部のデータだけでも分離できないかと考え、ひとまず他から参照されて無さそうなadmin_usersとadmin_sessionsのみ別DBに分離しようとしました。
use isu4 as Admin DB host by pddg · Pull Request #19 · pddg/isu12f · GitHub
が、ベンチがコケるので諦めました。この時点で20000点くらいは出ておりこのままいければそれなりに順調だった(と思う)のですが、このあたりから何をしてもスコアが伸びなくなり、焦り始めます。
14:00~15:00 generateIDで重複する
単に現在の時間をナノ秒単位で返していただけの実装がduplicate entryと言われるようになってしまったので乱数に変えられないか調べていました。
ORDER BY id ASC
しているテーブルは自分たちで採番していなかったので乱数でイケると判断してrand.Int63()
に変えました。
- generateIDを乱数化 by pddg · Pull Request #23 · pddg/isu12f · GitHub
- 間違っている。焦っていて冷静になれていない……
- fix generate random id by taxio · Pull Request #25 · pddg/isu12f · GitHub
- これが正しい。
このあたりでRedis欲しいかもと言われたので、余っていた4台目にRedisを入れ設定し、環境変数にホストやポートなどを入れました。
Introduce redis by pddg · Pull Request #24 · pddg/isu12f · GitHub
15:00~16:00 alpの結果がおかしい
行き詰まってきてしまい、色々見直そうということでプロファイリング結果を見ていたのですが、叩かれているはずのエンドポイントがalpで観測出来ていませんでした。
明らかに今回最大のミスで、alpのオプションを初期のログから目grepで組み立てて運用していたのが良くないポイントでした。
- fix alp option by pddg · Pull Request #28 · pddg/isu12f · GitHub
- fix by pddg · Pull Request #31 · pddg/isu12f · GitHub
明らかにこれによって解像度は上がりました(が、結局スコアに繋がる改善ができず……)。
16:00~17:00 泥沼にはまる
- user_cardsテーブルへのinsertがとにかく遅いので、UNIQUE制約に入っている
deleted_at
という全く参照されていないカラムを外そうとしたら初期データにはあるっぽくてエラーで落ちた。 - 苦し紛れにカバリングインデックスになるようにするがスコア改善はない
- masterデータのオンメモリキャッシュを作るも、全ての場所を入れ替える前に時間が尽きそうで途中で諦める
スコアがこの時点で14時辺りから変わっておらず、打開策を見つけられなくなっていました。
17:00~18:00 とにかく再起動試験を通す
スコアは残したいので再起動周りに手を入れ始めました。 DBが起きてくるまでアプリ側は無限ループしたり、pprof切ってログ切ってnetdataをアンインストールして……みたいな感じで計測系をひたすら外していました。
再起動して何回かベンチ回してコケないことを確認して、最高スコアの22000あたり(うろ覚えだった)が出たところで終了にしました。
結果
チームツナ缶はスコアを残すことに成功し、最終スコアは21710点でした。得点のあるチームの中では上から15番目(精一杯好意的な解釈)でした*2。
失格するチームも多い中得点を残せたのは、flakyなfailを潰すために何度もベンチを回したり*3、丁寧に再起動周りをケアして、自分で再起動回してベンチがfailしないことを確認したおかげかなと思います。
振り返り
alpのオプションミス
明らかにこれは痛かったです。正規表現が間違っており、分離されるべきパスが全て集約されてしまったことで誤ったエンドポイントが遅いように見えていました。
アプリを見る前にログを目grepしてオプションを組み立てていたのですが、アプリのコードがそこにあるのだからそれを見て組み立てるべきでした。
シャーディングの観点の欠如
今回はユーザ同士のインタラクションがなく、完全にそれぞれ独立しているためシャーディングをすれば余っているサーバを活用できました*4。IDの採番のロジックも握っているのでどうにでもなったはずです。
が、そもそもこの発想に至っていませんでした。予選がSQLiteで複数サーバにシャーディングできなくて困った~~って話をやったばかりなのに、一体何を学んでいたんでしょうか……?
アプリケーションログの軽視
あまり良い手立てを思い浮かんでいなかったのでアプリのログは愚直にjournalctlで見ていました。
が、他のメンバーはあまり慣れておらず効率的なログの閲覧ができていませんでした。これによりデッドロックに気付くのが遅れたり、作業が遅くなったりしており、大いに改善の余地があります。
nonyleneくんがやっていたように、ログとトレーシングの基盤を用意するのがいいのかな……と考えていますが、来年の僕に期待します。 nonylene.hatenablog.jp
デッドロックの解消の知見が乏しい
/login
という重いエンドポイントがありました。ここではログイン処理(パスワードチェックとかsession周りとか)に加えてログインボーナスの処理などをやっており、非常に複雑なエンドポイントになっていました。
alpで見ると全アクセス中このエンドポイントは500のケースがかなり多く、ベンチの結果にも出ていましたがそこまで気にしていませんでした。
が、実はデッドロックを引き起こしておりこれを解消しなければそもそも遊んでくれるユーザが入って来れないという状況になっていたようです。
終盤はtaxioがずっとこれを見ていてくれたのですが、何がデッドロックしているのか、どうすれば解消できるのかが掴めず最後までどうにもなりませんでした。ここがスコアを伸ばすポイントだったのに、みんな目先のN+1やスロークエリにとらわれてしまっていました。
また、自分がDBのデッドロックに詳しくなく、あまり手が出せなかったのも良くなかったです。とはいえギャップロックとかネクストキーロックくらいの知見はあるのだから、分離レベルをREAD COMMITTEDにしてみるとか*5それくらいは出せたはずでしたが、競技終了後に思い出しました。
来年に向けて
他のチームのスコアを見ると、自分たちのチームと見えている世界が違いすぎ、壁を感じました。ただし「また新しい知識を吸収して来年こそは本戦でも爪痕残すぞ」という気持ちになってきたのは良かったと思います。
幸い数日間、感想戦としてポータルやインスタンスを提供して頂けるとのことなので、冷静になった頭で色々ためしてみたいなと思います。
運営の皆さん、ありがとうございました!優勝のNaruseJunチームはおめでとうございます!
来年また対戦よろしくお願いします。
Mozilla Foundationへの寄付を始めた
TL; DR
- WebやOSSの発展に対して継続的な支援がしたくなった
- 自分が一番世話になっているのはブラウザなのではないかと気付いた
- FireFoxには生き残って欲しいのでMozilla Foundationへ毎月一定額を寄付することにした
はじめに
これは後から謎の請求を見て自分が混乱しないように残しているポエムです。寄付というものは性質上強制されるものでも奨励される物でも無いと思っているので、そういう書き方はしないように心がけています。Mozillaの回し者ではないです。
意図せずそういう表現になっていたらこっそり教えて下さい。
OSSと持続可能性
社会人になり、ソフトウェアエンジニアとしてのキャリアをスタートさせて2年と少し経ちました。世の中は大きく変わりましたが、自分の仕事は相も変わらずよくわからないソフトウェアを書いて、メンテナンスして、バグを出したり直したりしています。
さて、自分は学生の頃からOSSが好きで、自分で公開したりissueを投稿したりコントリビューションしたりしてきました。例えば、修士のときにコントリビューションしたtextlintのLaTeXプラグインを今でも時々メンテナンスしていたりします*1。
この程度の規模だとたまに来るIssueに対応したり時折依存関係の更新をしたりする程度で済んでいるため、それほど大きな負担があるわけではありません。積極的に機能開発をしているわけでも、大量のユーザがいるわけでもない、極々一般的なオープンソースソフトウェアです。
一方で、世の中には多数のユーザを抱え非常にクリティカルな箇所で使われるにもかかわらず、無償ないし寄付に頼ってメンテナンスが行われているソフトウェアというものが多数あります。 2021年の冬にはLog4j2の脆弱性が話題となり、無償でメンテナンスをしているメンテナーと、そのソフトウェアに強く依存する大量の営利企業の構図が比較的大きく取り扱われ、様々な議論を呼んでいました。
最近ではGitHub Sponsorsなどもありソフトウェアのメンテナを金銭的に支援する方法は増えてきたように思います*2。これによって本当にOSSというものが持続可能性を手に入れることが出来るのかは分かりませんが、少なくとも個人レベルで何らかの支援が出来る/受けられるようになってきたことはありがたい限りだなと思っています。
何を支援したらいいのか問題
現代では様々なところでOSSが利用されており、日々たくさんのソフトウェアの世話になっています。さて、自分が何かOSSを応援したい場合、どうすべきなのでしょうか?
- コントリビューションする→これももちろんできるならやりたい
- 全部にたくさん寄付→破産
- 全部にちょっとずつ寄付→手数料にも満たなさそう
- 特定のものに絞る→これが難しい
自分が世話になっている自覚のあるOSSが多岐にわたりすぎて、全部に寄付すると破産するし一つに選ぶのも難しいしかといってメンテ出来るほど詳しくもない……ということで、近年は大変そうだな〜と思ったところにまとめて寄付するようにしていました。例えば昨年はLog4j2のメンテナに少額ですが寄付させて頂きました。
自分が一番世話になっているのはブラウザではないか
今年に入ってからもう少し継続的な支援がしたいという気持ちが強くなってきたため、やっと寄付対象をまともに考え始めました。
- The Linux Foundation
- Linuxでメシ食わせてもらっていると言っても過言ではない
- Free Software Foundation
- GNUにはいつも世話になっているため
- Let's Encrypt
- Debian Project・Canonical
- Ubuntuには仕事でもプライベートでも世話になっているため
- Mozilla Foundation
- 就職してからプライベートではFireFoxをメインブラウザにしているため
会社でWebブラウザセキュリティという本の輪読会に参加し、Webブラウザの担う役割の大きさに気付かされたのもあり、ここしばらく自分が最も直接的に恩恵を受けているのはブラウザなのではという思いが強くなってきました。macOSでもWindowsでもLinuxでもFireFoxを使っていますし、自分の重要な情報はプライベートでも仕事でもありとあらゆるものがブラウザを経由しているように思えます。
Webブラウザセキュリティ ― Webアプリケーションの安全性を支える仕組みを整理するwww.lambdanote.com
現代のWebブラウザが当然考慮しているであろう様々な複雑な事情・ユーザの利便性・プライバシー保護を考えると頭が上がりません。また特定のベンダーによって独占される市場は大抵ろくなことになりません。Internet Explorerが退場し、EdgeがChromiumの仲間になった今、一人のユーザとして支持するべきはMozillaなのではないかという思いが強くなってきました。
寄付の仕方
以下のリンクから可能です。PayPal、もしくはクレジットカードが選択できます。
自分はとりあえず560円/月から始めることにしました。いつも最新のWebブラウザを使えるというサービスのサブスクリプションを契約しているような感じで良いかなと。
収入が増えたらもう少し額を増やすか、寄付対象を増やそうと思っています。
おわりに
めちゃくちゃ少額ですが、少しでも何かの助けになると嬉しいです。
ISUCON12予選突破の記録~絶望の4年間~
- はじめに
- これまでの記録
- 参加前の準備
- 当日やったこと
- 10:00〜11:00 初期セットアップ
- 11:00〜11:30 作戦会議
- 11:30〜13:45 細かい改善・レビュー・バグ取り
- 13:45〜14:30 docker外し
- 14:30〜15:15 MySQL移行を諦めきれず裏でトライ
- 15:15〜16:00 SQLiteへのクエリでトランザクションを使う
- 16:00〜16:30 サーバ1台をMySQL専用にする
- 16:30〜17:00 無駄なWHEREを削る
- 17:00〜17:15 テナントDBにインデックスを張る
- 17:15〜17:30 サーバ1台をnginx専用にする
- 17:30〜17:45 再起動試験
- 17:45〜17:55 flock外し
- アプリ担当のやったこと
- 振り返り
- 本戦に向けて
- おわりに
はじめに
ISUCON12予選にチーム ツナ缶 として参加し、ギリギリ滑り込んで初めて予選を突破したのでその備忘録です。
下記の様な分担で参加していました。
この記事では過去の参加記録と共に、主にインフラ周りについて書いています。
インフラ担当ではあるのですが普通にGoのAPIサーバは書けるので、インデックスを貼ったり細かいコードの改善は自分も担当しました。
これまでの記録
大会名 | 結果 | 備考 |
---|---|---|
ISUCON8 | 予選落ち | taxioと二人で参加 |
ISUCON9 | 予選落ち | shanpu参戦 |
ISUCON10 | 予選落ち | taxioとpuddingは社会人になって初参加 |
ISUCON11 | 予選落ち | サークルの後輩が初参戦で本戦に行き自信喪失*1 |
ISUCON12 | 本戦出場 | shanpuは社会人になって初参加 |
初参戦は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
Makefileは make 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をインストールする
- 指定したファイル/ディレクトリのバックアップを取る
- ログをローテートする
などのロールを用意していました。
当日の動きとしては以下の様になります。
- 担当するホストを決めておく
- puddingがssh configを用意してメンバーに展開
- 初期バックアップを取ってplaybookを微調整
- 後は各自で
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の読み込みを一回にしたり*2、MySQLの方にインデックスを張ったりしていました。
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。
- テナントIDをランダムに払い出すようにした
- N+1の解消
- 各テナントへの請求金額の計算で終わってない大会は請求金額を計算しない仕様なので早期に0で返す
- 請求金額の計算周り修正 by chez-shanpu · Pull Request #20 · pddg/isu12q · GitHub
- 請求金額は一度決まったら変わらないからキャッシュしとけばよかった
- playerの追加をバルクインサートにする
- 不要になったソートの削除
- 選手のスコアを逐一記録してたけど、最新一件があればいいので、上書きしていくようにした。これで最新一件取るためのORDER BY外せて処理時間短縮。
振り返り
良かったところ
- 一人一台を割り当て全員がデプロイできるようにすることで、インフラ担当が他のことをやっている間にもアプリの改善を進めてもらうことができた
- スロークエリログやアクセスログの解析ドキュメントをまとめていたので、当日はそれを参照すれば十分ボトルネックの解析ができた
- MySQLのインデックスについてちゃんとカバリングインデックスを意識することができた
- ちゃんとスコアが上がったことで自分たちが積み重ねてきたものに対して少し自信が付いた
改善したいところ
- デプロイ→ベンチ→結果の収集の流れの体験が良くなかった
- スロークエリログの解析結果やpprofの結果を自動で回収させる仕組みがあるともっと良かった
- 事前に用意したansible playbookの細かいバグで詰まった
- ログローテーションの設定が壊れていたり、typoがあったり……すぐ分かるミスを事前の検証で洗い出せていなかった
- アプリ側のロジックと合わせたDBの最適化まで手が回らなかった
- 例えばvisit_historyはインデックス張るだけでなく、そもそもこんなにデータいらないんじゃないかとかそういう方面に気を回す余裕が無かった
本戦に向けて
nonyleneくんの記事を読んでこれが本物のインフラ担当ってやつか……ってなりました。
普通にこれを真似るのもよさそうではありますが、それは来年にとっておこうかなと思っています。既に慣れたこの仕組みをブラッシュアップした方が本戦で迷わず済みそうな気がするので。
過去の本戦問題もまともに解いたことがないので、初の本戦を全力で楽しんであわよくば100万円もらいたいの気持ちで行きます。
おわりに
特に今回ISUCON初参加した人の中には「一生予選突破できないのでは」と感じている人がもしかしたらいらっしゃるかもしれません。毎回本戦出場みたいな激つよチームばかりが勝っているのではなく、4年同じチームで挑戦しつづけようやく突破できたみたいなチームもあることを知ってもらい、次への活力としてもらえると嬉しいです。
*1:この後輩たちは今年も本戦出場並のスコアをたたき出しており、若者こわい ISUCON12では、あくあたん工房の学生チームとして「Aquaharaimori」が参加していました!
本戦出場出来るスコアを残せていましたが、追試の挙動チェックで惜しくも失格となってしまいました...😢
来年は本戦出場してくれることを期待しています💪
お疲れ様でした!
*2:最終的にtaxioが中身をまるごとコードに埋め込んでくれました
*3:このPR見るとわかりますが普通に環境変数が間違っています。デフォルト値でうまく動いていたので気付いていなかったというオチでした。後で気付いて直しています。
*4:taxioの環境でflakyな整合性エラーによる失格が発生しており、一度revertされてしまいましたが最終的には取り込まれました。なおこのflakyなエラーはtaxioが initial_data を手でいじってしまっていたことによるものでした。即座に他のサーバから初期データのダンプを取ってきて上書きする、という判断を取れずコードが悪いのかずるずる考えてしまったのは大変良くなかったです。反省。
*5:flockではなくmutexにするアイデアも最初に出たのですが、mutexのmapを触る操作でグローバルなロックを取らないとrace conditionでダメなのでは、という話になって実施しませんでした。他のチームを見ていると導入したところもあったようなので、うまくやればできたんでしょうか?
*7:二人はブログ書いてくれないらしいので……