ISUCON8に id:EBAGmasa と id:kwzr と私でかっぱぺんぎん㌠というチーム名で参加しました。当日はkwzrの予定が悪くなってしまって2人での参戦になったのですが、そのままやっていくことにしました。
最終的には9000点くらいで終わってしまいましたが、ざっとやってみた戦略などを紹介したいと思います。
午前中: プロファイリングと手元環境整備
プロファイリング
プロファイリングはもともと alp
と pt-query-digest
をメインに使う予定でした。Ruby実装で挑むことにしたので、 rack-lineprof
なども使ったり。id:EBAGmasa が事前に alp
と pt-query-digest
の結果をGitHubにアップロードするスクリプトを書いてくれていたのですが、 pt-query-digest
の結果が大きすぎてアップロードに失敗するなど事件がありました。pt-query-digest
は大きなクエリは省略してくれるオプションとかあればいいんですけどね…。
当日のサーバーはh2oでalpにすぐにかけられないことがわかったため、それをnginxに置き換えるのと、MySQLのSlow Query Logがうまく出ないので苦戦して午前中をプロファイリングや実装の確認で使ってしまいました。
my.cnf 読まれない問題
当日はMySQLの /etc/my.cnf
を /home/isucon/torb/etc/my.cnf
あたりに保存し、/etc/my.cnf
にシンボリックリンクを貼ろうと思っていたのですが(大体の設定ファイルをそのように実装しようとしていた)、シンボリックリンクだと設定ファイルを読み込んでくれないようで、権限周りも確認したのですが結局は /etc/my.cnf
に直置きすることとなりました。
環境整備
今回は始めてちゃんと手元で動作させながら確認しました。結構今まではカンでコードを書いてデプロイしてることが多かった気がするのですが、これだとなかなかサイクル回せないなということで、きちんと手元に環境を作っておきました。これは結構良かったなと思っています。DBが軽かったり、初期化スクリプトの init.sh
がきちんと置いてあったのがありがたかったです。
10:30 gzip化
とりあえずこれくらいはってことでgzip化とかを id:EBAGmasa がしてくれました。static fileをgzかけて置いておくだけです。
方針をたてる
3台あるけど、まず最初は今動いている1つのサーバで高速化をはかって、その上である程度まで高速化できたら3台構成にしようという作戦にしました。それまではアプリケーションの改善をメインにやっていこうということです。
12:00 reservationsのuser_idにindexを貼る
クエリの中でユーザの予約状況などを取ってくるところがあったと思うのですが、そのクエリをEXPLAINしてみると using where; using filesort
あたりになっていたのでindexをはってやりました。これで数千点上がったような
13:00 sheets剥がし
あるランクの座席の総数や、値段は基本的にどれも同じという前提があったので、それをコードの中に埋め込みなおすのを id:EBAGmasa がやってくれました。1800点くらい。
14:00 3台構成化 / get_eventのN+1 queryをなおす
この辺から時間もなくなってきたので、3台構成にしようということで id:EBGAmasa が3台構成にする作業をしてくれます。
get_event
の中で座席分だけSELECTしているのはヤバいなということで、先にreservationsを取ってきて使うようにしました。ついでに reservations
の reserved_at
にもindexを貼っています。
本当はここでもう少し、 get_event
に着目すべきでした。N+1 queryは解消したものの、結局sheetsのループの中で毎回reservationsにfindをかけており、それなりにCPU時間を食っていました。実は id:EBGAmasa が最初に rack-lineprof
を入れてくれていたのですが、この後くらいまで使わなかった。ちゃんとプロファイリングしていればこれに気づけたので、これが結構もったいないことをしたなあと思っています。
15:00 Redis化
コードを全体的に見ていて、トランザクション処理をかけているのが気になりました。pt-query-digest
でもCOMMITに時間をかけていたし(でもこれ、処理がCOMMITのタイミングで行われるからだけなんじゃないか…)。今回のメインの処理は予約をするところかなと考えてしまったので、そこをどういうふうに処理していくかを考えました。
マニュアルには「1秒以内に反映されていれば、情報が遅延していてもOK」とのことだったので、次のような仕組みでやってみることにしました。
- RedisのSETをつかって、予約可能な座席を表現する。キーは
"event_id:rank"
といった感じ。 - 「event_id: 3の、S席ください」とリクエストが飛んでくる。
- RedisからPOPするとランダムで値が取れるので、適当に返す。取得に失敗してnilが帰ってきたら、エラー処理。
- 同じ席を予約する処理をすることはないので、トランザクションは特にかけずに、INSERTする。
キャンセルは同様に、DBからSELECTして正しい予約ができるかを調査して、RedisのSETに戻してやるだけです。
でもよくよく考えたら、innodbは行ロックだったと思うし、これってそんなに意味はなかったかなあ…。と思ったり。でも激しく予約リクエストが来たときは多分効きますよね。
これで、commitとか、rollbackとかは全部消しました。
ここで FOR UPDATE
についてきちんと調べておけば、FOR UPDATE全部消せたんじゃないかなあ…。(それよりも get_event
の重さのほうがヤバかったわけだけど。)
16:00 MySQL / Puma メモリチューニング
このあたりで id:EBAGmasaがリソースを使い切れるように、ワーカー数やメモリの割当を調整してくれた。 puma
が結構メモリを食ってしまうのに苦戦していた様子。この辺で4000点とかだったかな?
16:40 get_event 再燃
ここで改めて rack-lineprof
を見るとやっぱり get_event
ヤバいぞ…。ということで get_event
に手をいれることにしました。なるべくJOINしてDBへの問い合わせやループを削除して頑張っていく。Hashの引数にHash使ってるコード、マジで頭がヤバくなる。
17:20 再起動テスト
id: EBAGmasa が再起動テストしたりしてくれてました。
17:30 get_eventの修正入れて、デバッグ作業
ああもうこのままだと絶対勝てんぞ!ということで、出来る限り頑張ってやってみることにしました。get_eventの修正をこの時間から投入して、デバッグ作業しながら進めていき、結果9200点くらいで着地です。
実は最後にslow query logをOFFにするのを忘れていたんですよね、おつらい。
感想
運営について
今年のダッシュボードは本当によくできていて驚きました。ベンチマーカーも快適だったし(1チームに対して1ベンチマーカーいたとのこと。ネットワーク割当もすごい。)、DBのバックアップも取りやすくなっていて快適でした。前段サーバーがh2oだったりとか、mariadb使われていたりというのはちょっと驚きました。
振り返って
- 今回は、アプリケーションの無駄を素直に取り除けば良かった問題だった。
- slow query logやh2oからの載せ替えに手間取ってしまったのがもったいなかった
alp
やpt-query-digest
を取ったところまでは良かった- もう少し良く無さそうなところを考える時間をじっくり取るべき。
- Redis使うのはおもしろいけど、早すぎた
- 優先順位を考えよう
- ベンチマーカーのエラーに着目したほうがよい
rack-lineprof
をきちんと使っていくと、安心感を持って進められそう- 手元ですべて動かすと楽
- 次回は、
docker-compose.yml
とかでそれっぽい環境作っておきたい
- 次回は、
- deployはもう少しなんとかしたい
- 今回は2人だったけど、3人いたら、1人はそのへんの自動化をやっていくと良い
- 普段やらないことは、ISUCONだとやっぱりできない
色々後悔は残ったけど、楽しいISUCONでした!運営の皆さんありがとうございました、本線進む方がんばってください。