ISUCON9予選をギリギリ通過しました
2019年9月8日に行われたISUCON9予選にチーム FetchDecodeExecWrite で参加し、10470点でギリギリ通過しました。やったことを書いていきます。
スコア遷移
10:00 競技開始
kcz さんがアリババクラウドと戦っているのを見て応援していた。
マニュアルを熟読したつもりだったが、タイムラインの内容をある程度自由に弄っていいことは最後まで気づかなかった。
2110点
〜10:40 準備
pprof とか MySQL slow query とかの準備をした。
10:53 BcryptCost
pprof の結果 bcrypt が重かったのでとりあえず新規ユーザーの BcryptCost
を10から4(ライブラリで許される最低値)に変更した。しかし、重いのは既存ユーザーのパスワードの確認だったためあまり効果はなかった。
既存ユーザーのログインリクエストを基に平文パスワードを収集して、新たに sha256 などの軽いハッシュ関数の結果を入れることも考えたが、
- 各ベンチマークでユーザーの初回ログイン時に sha256 を保存する方式では、2度ログインするユーザーが少なそうなのであまり効果がない
- initial.sql (初期データ)に過去のハッシュを入れる方式では、収集が面倒な割に100%データが集まるとは限らず、最悪の場合「再試験後に新たなユーザーを使用して確認した結果スコアが大幅に下がり失格」となるかもしれない
ということで見送った。
11:38 getNewCategoryItems で items と categories を JOIN
次のクエリ
SELECT * FROM `items` WHERE `status` IN (?,?) AND category_id IN (?) AND (`created_at` < ? OR (`created_at` <= ? AND `id` < ?)) ORDER BY `created_at` DESC, `id` DESC LIMIT ?
で category_id IN
の対象が共通の parentID
を持つカテゴリ達だったので JOIN した
SELECT ... FROM items INNER JOIN categories ON categories.parent_id=? AND items.category_id = categories.id WHERE items.status IN (?,?) AND (items.created_at < ? OR (items.created_at <= ? AND items.id < ?)) ORDER BY items.created_at DESC, items.id DESC LIMIT ?
1210点 (謎)
11:48 SQLを別インスタンスに移行
応援してた
12:00 Campaign: 1
去年本戦のシェアボタンの悪夢が蘇ってくる仕様。とりあえず変更。その後もころころ適当に変えていた。 1510点
12:41 item.status を比較可能に
ステータスの on_sale
, sold_out
, trading
, stop
, cancel
を "1"
〜 "5"
に置き換えた。このように辞書順に並べればクエリの status IN (?, ?, ?)
とかが大小比較で書ける。
12:?? file descriptor の数を増やす
Nginxのファイル記述子の上限に引っかかっていたらしい。応援していた。
13:?? items が category.parent_id を持つように非正規化
items.cat_parent_id というカラムを用意して、上の JOIN が不要になった。
SELECT ... FROM items WHERE items.cat_parent_id = ? AND items.status <= ? AND (items.created_at, items.id) < (?, ?) ORDER BY items.created_at DESC, items.id DESC LIMIT ?
14:05 postBuy の2つのAPI呼び出しを並列化
応援してた
??:?? アプリケーションサーバーを2台構成にした
応援してた
16:27 getTransactions のリトライ
外部APIに502を返されても一発で諦めず、10回までリトライしたらisucariから500を返すことがなくなったらしい。すごい。
13:??〜16:41 nginx の worker_connections を増やした
地獄だった。campaginを1以上にするとfailするが、ベンチマーカーは「POST /buy: リクエストに失敗しました (item_id: 500**)」というメッセージ以外の情報を表示してくれず、Nginx や golang service、MySQL の error.log などを見ても有用な情報が見つからない。
自分の実装が間違っていたのかとか色々不安になってデバッグしても原因がなかなかわからなかった。
結局 Nginx の worker_connections を1024から4096に増やして解決したらしい。3人×3時間ぐらい無駄にした。つらかった。
Nginx のログに出てこないのは仕方ないが、ベンチマーカーが何も言ってくれないのがつらい。
7570点
16:59 BumpChargeSeconds を変えた(その後戻した)
最後まで bump の恩恵を把握できず終わった。残念。
17:03 カテゴリのデータ全体をメモリに載せた
カテゴリは一切変更されないので初回読み込み時にグローバル変数に保存した。
副作用として一つN+1クエリがなくなった。もう一つのN+1クエリも消したかったが実装工数の都合上最後まで消えなかった。
9230点
17:06 DBコネクション確立を何回でもリトライ
再起動試験時のインスタンスの起動順がわからないので、DBインスタンスが最後になってもいいようにコネクション確立をリトライしまくるようにしたっぽい。
17:21 items.status_le2 カラムの追加
当時一番重いクエリは WHERE items.cat_parent_id = ? AND items.status <= "2" AND ...
というやつだった。
これではインデックスが (cat_parent_id, status)
までしか効かずそれ以降の WHERE 条件や ORDER BY は全件探索になってしまっていた。
そこで status <= "2"
と status_le2 = 1
が同値になるようなカラム status_le2
を追加した。これでクエリ
SELECT ... FROM items WHERE items.cat_parent_id = ? AND items.status_le2 = 1 AND (items.created_at, items.id) < (?, ?) ORDER BY items.created_at DESC, items.id DESC LIMIT ?
に対しインデックス (cat_parent_id, status_le2, created_at DESC, id DESC)
が効いていた(はず)。
10310点
〜17:50 再起動試験とおみくじ
10470点!!!
17:40 アリババクラウドのRAMの設定忘れに気付く
急いでkczさんがアリババクラウドのコンソールから設定した。点数が0点から無限倍になった。やったー
その他
Nginxで静的ファイルを捌いたり負荷分散していたらしい。お疲れ様です。
結果
10470点で24位だった。ギリギリ本戦に進めてよかった。
反省点
仕様をちゃんと読まなかった。実際読んでいるつもりだったがタイムラインの内容に soldOut を含めなくてもいいということにすら気付けなかった。これを含めなければそもそも上の status_le2
は不要だったし、売り上げも大きく違っていたと思う。
今後は、仕様を1文字たりとも読み飛ばさず、怪しいところはリストアップしておくことを心掛けたいと思う。
本戦もがんばるぞい!