半空洞男女関係

思ったこととかプログラミングしてるときのメモとか色々かいてます。メールはidそのままgmail

ISUCON8 予選で敗退した(くやしい)

ISUCON8に id:EBAGmasaid:kwzr と私でかっぱぺんぎん㌠というチーム名で参加しました。当日はkwzrの予定が悪くなってしまって2人での参戦になったのですが、そのままやっていくことにしました。

最終的には9000点くらいで終わってしまいましたが、ざっとやってみた戦略などを紹介したいと思います。

午前中: プロファイリングと手元環境整備

プロファイリング

プロファイリングはもともと alppt-query-digest をメインに使う予定でした。Ruby実装で挑むことにしたので、 rack-lineprof なども使ったり。id:EBAGmasa が事前に alppt-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を取ってきて使うようにしました。ついでに reservationsreserved_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」とのことだったので、次のような仕組みでやってみることにしました。

  1. RedisのSETをつかって、予約可能な座席を表現する。キーは "event_id:rank" といった感じ。
  2. 「event_id: 3の、S席ください」とリクエストが飛んでくる。
  3. RedisからPOPするとランダムで値が取れるので、適当に返す。取得に失敗してnilが帰ってきたら、エラー処理。
  4. 同じ席を予約する処理をすることはないので、トランザクションは特にかけずに、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からの載せ替えに手間取ってしまったのがもったいなかった
    • MySQLの設定についてはデバッグがやりにくい。
    • MySQLがどこを読みに行っているのか?などをぱっと見れなかったのが良くなかった。(普通に mysql --help | grep my.cnf とかで見れたっぽい)
  • alppt-query-digest を取ったところまでは良かった
    • もう少し良く無さそうなところを考える時間をじっくり取るべき。
    • Redis使うのはおもしろいけど、早すぎた
      • 優先順位を考えよう
      • ベンチマーカーのエラーに着目したほうがよい
  • rack-lineprof をきちんと使っていくと、安心感を持って進められそう
  • 手元ですべて動かすと楽
    • 次回は、docker-compose.yml とかでそれっぽい環境作っておきたい
  • deployはもう少しなんとかしたい
    • 今回は2人だったけど、3人いたら、1人はそのへんの自動化をやっていくと良い
  • 普段やらないことは、ISUCONだとやっぱりできない

色々後悔は残ったけど、楽しいISUCONでした!運営の皆さんありがとうございました、本線進む方がんばってください。