FLYING

/* TODO: 気の利いた説明を書く */

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

f:id:tondol:20200914222643p:plain

スコア 812。bulk insert よりは @kuskus_ainan の DB 設定チューニングが効いた感。

15:49

@hogesako により kataribe の結果が共有される。

/api/chair/low_priced および /api/estate/low_pricedボトルネックに。

16:03

estaterentchairstock, price カラムにインデックスを張る。

16:09

再度 kataribe の結果が共有される。

/api/estate/nazotteボトルネックに。

この頃 @kuskus_ainan と @hogesako により DB を別インスタンスに逃がす形に。

16:34

f:id:tondol:20200914223044p:plain

スコア 1345

16:40

@hogesako の手で nazotte の N+1 が解消される。

再び kataribe。

/api/recommended_estate/:idボトルネックに。

16:55

f:id:tondol:20200914230930p:plain

スコア1361。

17:19

recommended_estate のためのインデックスが追加される。

17:49

この頃 @tondol は必死で chairestatefeatures カラムを 中間テーブルを作って正規化しようとする。

しかし中間テーブル化まではできたものの、それを使って検索するクエリがうまく書けずに難航。

一方、ミドルウェアチームは MySQL 8 化を試行するが、Ruby の Mysql2 ライブラリを再インストールしようとするもエラーに遭遇したりして難航。

(Ruby に慣れた @tondol が手を貸すべきだったが、その余裕がなかった。反省。)

18:17

@hogesako の手でアプリの2台構成化が試行される。

19:11

@hogesako が中間テーブル方式ではなく FULLTEXT INDEX が良いのではと提案するが、実装には移されず。

19:42

f:id:tondol:20200914230314p:plain

スコア 1448

19:53

やっと完成した中間テーブル方式でスコアが伸びなかったため、見切りを付けて FIND_IN_SET を使ったクエリに置き換える。

この頃、@hogesako から chairestate で DB を2台に分ける案が提唱されるが、残り時間的に @tondol が難色を示し実施されず。

20:06

@hogesako の手で estate の緯度経度にインデックスが張られる。

@hosasako がベンチマーク実行時のワークロードを見ており、DB ネックであることを教えてくれた。

そのため、用意していたアプリの2台構成化は実施されず。結局アプリ1台、DB 1台でもう1台は使わないまま終わった。

20:15

なんと、0_Schema.sql に追加したつもりだったインデックス追加の SQL 文が動いていないことが判明!!!!

原因は SQL 中でデータベース名を明示するのを忘れたため。バカバカ!!!!

f:id:tondol:20200915223804p:plain
コミットメッセージから動揺が伝わる

慌てて修正し、ベンチを回す。 *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 でクアアイナの良さげなバーガーを頼んでしっかり食べた。完全にお気持ちだが、やっぱり食べたいものを食べておくと後半もがんばれる気がする。

初歩的なところであるが、httpiejournalctl を使ったエンドポイントのデバッグも比較的スムーズにやれた。

できなかったこと

チームというよりは、@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ではなくなった。でも、それだけではまだ足りない。本音としてはやっぱり本戦に出たい。しっかり修行してまた来年、自信を持って取り組めるようにしたい。

いやあ、奥が深いですね。運営の皆様、各言語実装者の皆様、楽しい大会をありがとうございました

github.com

*1:「くすサポ」とは声優の楠田亜衣奈さんのファンの呼称で、メンバー全員が楠田さんのイベントで知り合ったためこのチーム名になった。

*2:@hogesako が慌ててコミットしているように見えるが、インスタンス上で雑に git config したせいであって、実際に慌てていたのは @tondol です。

*3:ISUCON9での最終スコアランキングは166位。今回は43位。