ISUCON10 予選に参加し敗退しました!
TL;DR
惜しいところまで行けたのも実力。それでも足りなかったのも実力。
はじめに
@hogesako @kuskus_ainan と共にチーム名「くすサポISUCON部」で ISUCON10 予選に参加し、最終スコア1965点で敗退した。 *1
再起動試験を考慮しない最終スコア (あくまでも参考値!) では、全468チーム中43位だった。
メンバーごとの作業割り振りはこんな感じ。
name | role |
---|---|
tondol | アプリケーションの改善 |
kuskus_ainan | ミドルウェアのチューニング |
hogesako | 上記2人の中間あたり |
まずは時系列でやったことを振り返る。
作業ログ
12:20
競技開始と同時にチーム全員で当日マニュアルを読み合わせ。
CSV 入稿のエンドポイントがあること、BOT アクセスを弾いても良いこと、スコアが購入リクエストの成功数と資料請求リクエストの成功数の合計で決まることなどを発見。
12:46
踏み台サーバーという (ISUCON においては) 新概念に若干戸惑ったが、SSH の設定をひと通り済ませる。LocalForward
は便利。
webapp/
以下を Git リポジトリ化、予め作成しておいたリポジトリに push。
12:58
/initialize
走行後の DB 内容などチェックする。2テーブルしかないシンプルな構成で驚く。
なぞって検索のクエリで見たこともない関数が使われておりビビる。ある程度知っているらしい @hogesako に引き継ぐ。
このあたりで Ruby への切り替えもしたはず。
13:34
nginx で雑に if を使って BOT の User-Agent を弾く設定を入れる。
log_format
も変更したつもりだったがポカミスでこの時点では動いていない。
14:03
kataribe のために nginx の log_format
を今度こそ変更。
CSV 入稿系のエンドポイントの bulk insert 化に取り組む。
14:27
bulk insert が完成。
httpie でコマンドラインから post エンドポイントの動作確認が簡単にできて便利。
14:54
スコア 812。bulk insert よりは @kuskus_ainan の DB 設定チューニングが効いた感。
15:49
@hogesako により kataribe の結果が共有される。
/api/chair/low_priced
および /api/estate/low_priced
がボトルネックに。
16:03
estate
の rent
、chair
の stock
, price
カラムにインデックスを張る。
16:09
再度 kataribe の結果が共有される。
/api/estate/nazotte
がボトルネックに。
この頃 @kuskus_ainan と @hogesako により DB を別インスタンスに逃がす形に。
16:34
スコア 1345。
16:40
@hogesako の手で nazotte の N+1 が解消される。
再び kataribe。
/api/recommended_estate/:id
がボトルネックに。
16:55
スコア1361。
17:19
recommended_estate
のためのインデックスが追加される。
17:49
この頃 @tondol は必死で chair
と estate
の features
カラムを 中間テーブルを作って正規化しようとする。
しかし中間テーブル化まではできたものの、それを使って検索するクエリがうまく書けずに難航。
一方、ミドルウェアチームは MySQL 8 化を試行するが、Ruby の Mysql2 ライブラリを再インストールしようとするもエラーに遭遇したりして難航。
(Ruby に慣れた @tondol が手を貸すべきだったが、その余裕がなかった。反省。)
18:17
@hogesako の手でアプリの2台構成化が試行される。
19:11
@hogesako が中間テーブル方式ではなく FULLTEXT INDEX が良いのではと提案するが、実装には移されず。
19:42
スコア 1448。
19:53
やっと完成した中間テーブル方式でスコアが伸びなかったため、見切りを付けて FIND_IN_SET
を使ったクエリに置き換える。
この頃、@hogesako から chair
と estate
で DB を2台に分ける案が提唱されるが、残り時間的に @tondol が難色を示し実施されず。
20:06
@hogesako の手で estate
の緯度経度にインデックスが張られる。
@hosasako がベンチマーク実行時のワークロードを見ており、DB ネックであることを教えてくれた。
そのため、用意していたアプリの2台構成化は実施されず。結局アプリ1台、DB 1台でもう1台は使わないまま終わった。
20:15
なんと、0_Schema.sql
に追加したつもりだったインデックス追加の SQL 文が動いていないことが判明!!!!
原因は SQL 中でデータベース名を明示するのを忘れたため。バカバカ!!!!
慌てて修正し、ベンチを回す。 *2
ここで伸び悩んでいたスコアが2000を超える!!!!
ハイタッチが発生するレベルの盛り上がり。間違いなく予選のピークであった。
その後、ベストスコア2100を記録する。
盛り上がりすぎて特にスクショなどが残っていない悲しみ (´・ω・`)
20:28
@hogesako が recommended_estate
用のインデックスを改善する。具体的には、door_width
, door_height
の個別インデックスから複合インデックスにした。
以降、残り30分なのでアプリのソースコードはフリーズ。
20:30 以降
再起動試験を行い、問題が発生しないことを確認。
スロークエリログをオフにしたあと、何回かスコアガチャを行うが過去ベストを超えられず。
残り10分となり最終スコア1965で終了。
感想
問題について
アプリケーションの実装は (Ruby しか見ていないが) とても素直な作りで、よく見ればいくつもの問題があるものの、パッと見で明らかなボトルネックが見つからないほどだった。
そのため、まず計測してボトルネックを明らかにしてそれを改善する。さらに計測すると次のボトルネックが明らかになる。ということを繰り返すような流れに自然となり、大変良い問題だったと感じている (予選敗退の身で何様だよって感じだが) 。
スコアについて
これまで同じチームで過去3年ほど ISUCON 予選に取り組んでいる。
ISUCON8では最終スコア0、ISUCON9ではある程度のスコアは出たものの本戦出場にはまったく届かないレベルであった。 *3
それが、今回はもうあと1手チューニングができれば本戦に出場できるかもしれないというところまで辿り着いた。
今回は初めて、予選の前にチームメンバーで集まって過去の問題を解くということもやってみたのだが、明らかに事前の準備が本番に生きてくる実感があった。
やればやるだけしっかりと身に付いて結果が伴っているという実感が競技中もあったし、だからこそ一歩届かなかったことが大変悔しくもあった。
うまくいったこと
最初にしっかり当日マニュアルを読んだこと。これにより、BOT のルールなどについては最初の段階で把握することができた。
事前の練習の成果もあり、役割分担がうまくいった。それぞれの得意分野を活かすことができたと感じる。
オンライン開催ではあったが、やはり全員ひとつの場所に集まって会話しながらできたのも良かった。作業に詰まったときに外部ディスプレイを使ってペアプロ的にアドバイスすることができた。
最初の SSH 接続から Git 管理化、kataribe と pt-query-digest を使う流れも比較的スムーズだった。特に、ログの解析については @tondol はほとんど関わらず、チームメンバーに任せることができた。
ワークロードの状況からインスタンスの使い方を検討し、実際に DB を別インスタンスに逃がす部分も完全にチームメンバーに任せられた。気づいたら env.sh を編集すればいいだけの状況になっていた。ありがたい。
お昼も UberEats でクアアイナの良さげなバーガーを頼んでしっかり食べた。完全にお気持ちだが、やっぱり食べたいものを食べておくと後半もがんばれる気がする。
初歩的なところであるが、httpie
や journalctl
を使ったエンドポイントのデバッグも比較的スムーズにやれた。
できなかったこと
チームというよりは、@tondol の個人的な反省が多めになるが。
まずは、検索機能のエンドポイントを良い感じに仕上げられなかったこと。中間テーブル化なども試したが、最終的に複雑なクエリになってしまい、あまりスピードアップに繋がらなかった。
以下は中間テーブルを使った最終的なクエリ例である。
SELECT estate.* FROM estate JOIN estates_features AS ef ON estate.id = ef.estate_id WHERE ef.feature_id IN (1,2) GROUP BY estate.id HAVING COUNT(estate.id) = 2 ORDER BY popularity DESC, id ASC LIMIT 20 OFFSET 0
この複雑さを見ると、features
の取り扱いをがんばるよりは、 popularity
のインデックスをどうにかする方が良かったのではないかとも感じる。
ISUCON 公式の discord では generated columns を使った解法も紹介されていたが、残念ながらその知識はなかったので、popularity の符号を反転したソート用のカラムを作るなどのやり方ができたのではないか。
kataribe やスロークエリーの集計なども若干オペレーション上のオーバーヘッドが高かったように感じる。他チームのように簡単にコマンド一発で計測ができるような仕組みを事前に準備しておけるとよかった。
また、@tondol は事前の練習でアプリケーションのプロファイリングなども試し、やり方を把握していたが、今回の本戦中はついぞ試す機会がなかった。
ネックが DB 側に多く、あまりアプリ側のプロファイルの必要性に駆られなかったというのはあるが、最初の段階でサクッと入れておけると、その後チューニングするべき場所を選ぶ判断が早くできたのではないかとも感じる。
張ったつもりだったインデックスが実際に効いていないことに終盤まで気づけなかったことも大きかった。
これが早い段階で分かっていれば、修正してから再度ログ計測を行い、余裕を持ってさらなるボトルネックの改善に進むことができたはずである。
気づけないよりは100億倍マシではあったが、それでもやはりもったいなかった!!!!
あとは更新されることが少ないクエリや get 系のエンドポイントについて、キャッシュすることを検討する頭がもうちょっとだけでもあると良かった。完全に他回答者の感想を見て「たしかにな〜」となった部分。
まとめ
これまでは、そもそも本戦に出るという可能性が見えていなかったし、競技後もただただ実力不足だという事実を見せつけられるのみだった。しかし、今年は違った。
今回、少なくとも0ではない最終スコアを残すことができたし、悔しいという気持ちにもなることができた。準備したことが身に付いている、スコアに繋がっていると思える瞬間もあった。スコアが跳ねた瞬間はめちゃくちゃ楽しいなと素直に思った。
個人的に好きな <ラブライブ!サンシャイン!!> という作品には、そのストーリーの根底に『0を1にしたい』『0から1へ』というテーマがあるが、まさにそのことを思い起こしながら取り組んだ8時間だった。
今回のISUCONで、我々の結果も0ではなくなった。でも、それだけではまだ足りない。本音としてはやっぱり本戦に出たい。しっかり修行してまた来年、自信を持って取り組めるようにしたい。
いやあ、奥が深いですね。運営の皆様、各言語実装者の皆様、楽しい大会をありがとうございました。
温かみのあるメソッドを書いたが使わなかったやつを供養します pic.twitter.com/REtug9CtEc
— とんどる (@tondol) September 12, 2020