ISUCON9 予選の過去問でNew Relicを使う - valid,invalidの続きです。
この記事は1台のVM上でベンチマーカーすべて含むstandalone構成のまま、予選突破スコアを超える12260点を出すまでにやったことのメモ。(ISUCON 9の予選突破スコアは9650点: 参照)過去問のため解説は公式ですでに公開されているので深くは触れません。
以下、(点数) やったこと
。
(1510) 初期状態
MySQL slow query log出力とNew Relicを設定したあとだったかも。
(1810) index追加
top
コマンドでベンチマーカーの実行時の状況を見るとそれなりにmysqld
に負荷がかかっていたのでまずはDBのチューニングから。
MySQL slow query logを出力し、average timeが遅いクエリを見つけてindexをいくつか貼った。
-- 01_schema.sql CREATE INDEX created_at ON items (created_at); CREATE INDEX seller_id ON items (seller_id); CREATE INDEX buyer_id ON items (buyer_id);
このあとslow query logはオフにした。
(2010) shippment/status呼び出しをPOSTからGETへ
New Relicを見たところ、/users/transactions.json
がメチャ重い。特にAPI呼び出し。
マニュアルを読んだらGETのAPIなのになぜかPOSTで呼び出したのが気になったので直して走らせたらなぜかちょっと上がったが誤差か。
(2620) categories
をインメモリにしてfindのN+1地獄を回避
#POST /initialize
時に全部読み込み、定数時間で取れるようHashでメモリに保存しておく。
(3210) items.map
内のusers N+1を回避
categoriesはインメモリ化できたが、itemsに紐づくuserは厳しそう。loopの前にusersをガッと取得してループ内でlookupするようにした。joinでも良かったけど。
(7860) /user/transactions.json
で外部APIの呼び出しは不要なので止める
DBにある値を表示すればそれで事足りた。ここで一気に伸びた。
(9160) indexを複合indexに修正した
WHERE buyer_id = ? ORDER BY created_at
みたいにindex効いてるクエリのsortが大丈夫か?と思ってEXPLAIN見たらExtraにUsing filesort
が出てた。複合インデックスにして潰せるようにした。
-- 01_schema.sql CREATE INDEX created_at ON items (created_at); CREATE INDEX seller_id ON items (seller_id, created_at); CREATE INDEX buyer_id ON items (buyer_id, created_at); CREATE INDEX category_id ON items (category_id, created_at);
ORDER BY created_at
はかなりたくさんあったので結構スコア伸びた。
(7660) campaign=1
にした
リクエストが爆増して全体的に耐えられなくなり、下がった。
APIをスケールアウトしたいがstandalone構成だと難しいので戻した。
{"pass":true,"score":7660,"campaign":1,"language":"ruby","messages":["GET /users/500.json: リクエストに失敗しました(タイムアウトしました)","POST /login: リクエストに失敗しました(タイムアウトしました)","POST /ship_done: リクエストに失敗しました (item_id: 50019)(タイムアウトしました)"]}
(9160) /user/transactions.json
でN+1を解消
もっと伸びるはずが、対して変わらなかった。
(8960) /user/transactions.json
でORクエリを解消
なぜか下がった。
New Relicを見るとこの時点でボトルネックは/buy
だったので誤差程度の修正にしかなっていないと判断。
(9160) /buy
でuserをselect for updateする必要ない
ちょっと上がったけど誤差か。
(9460) /buy
で外部リクエストを並列化
ちょっと伸びた。/buy
がボトルネックでなくなり、/items/:item_id.json
に移ったことがNew Relicで確認できた。
(9660) /items/:item_id.json
でseller, buyerをjoinで一発で取ってくるようにする
ようやく予選突破スコアまでいった 🎉
(9660) /items/:item_id.json
でtransaction_evidence, shippingをjoinで一発で取ってくるようにする
変わらず。
(8760) campaign=1
にした
耐えられないのが/login
だけになった。ログイン用の別サーバを用意するとかできたらもっといけそう。
とりあえず戻す。
{"pass":true,"score":8760,"campaign":1,"language":"ruby","messages":["POST /login: リクエストに失敗しました(タイムアウトしました)"]}
(12260) New Relicをオフにする
require 'newrelic_rpm'
を消す。めっちゃ上がった!
所要時間
環境構築から数えたらたぶん15時間ぐらいかけていた(しかもぶっ通しではなく日をまたいでの細切れ)。なので本番でこのスコアが出せたかというと確実にNO…
学び
- New RelicでボトルネックとなるAPIエンドポイントの移り変わりが確認できて便利
- standalone構成でも地道にチューニング重ねれば予選突破スコアが出た
- 計測系ツールをオフにするのを忘れてはいけない(伸びなくなったタイミングでオフるとちょっと伸びてボーナス感がある)
複数台構成にするとかNginxやMySQLの設定いじるとかのインフラ寄りは苦手なので、アプリケーション中心の改善でもこれだけ伸びる出題だと良いな〜