valid,invalid

関心を持てる事柄について

ISUCON9 予選の過去問で予選突破スコアを出すまで練習

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の設定いじるとかのインフラ寄りは苦手なので、アプリケーション中心の改善でもこれだけ伸びる出題だと良いな〜