結構時間経っちゃいましたが、@tatsuyaoiwに誘われて@takady7との楽天同期トリオでISUCON初参加してきました。非常に残念ながら予選落ちで非常に悔しい思いをしたので、来年こそはという気持ちも込めて今回の参加前後で勉強になったことをまとめました。

事前に勉強したこと

参考資料を読み漁る

とりあえず3人でslackグループを作って色々参考になるリンクを送り合ってたんですが、特に下記のリンクはしっかり読みました。

ISUCON4の予選問題

実際に予選問題のイメージを作って、一人ISUCONしてみました。

ISUCON4 予選問題の解説と講評 & AMIの公開

AMIも公開されていたんですが、サーバー代ケチってこちらのVagrantfileからローカルに環境作りました。

matsuu/vagrant-isucon

流れとしては、

  1. stats系を見てCPU・メモリ・IOなどがボトルネックになってないか調べる
  2. nginxのログを集計して時間かかってるページを調べる
  3. アプリのプロファイルを取って時間のかかる処理を調べる
  4. MySQLの時間かかってるクエリを調べる
  5. 対策して繰り返す

という感じなので、この辺のツール類をまずは色々試してました。

stats系

普段はtopとfreeぐらいしか見ないので、IOも含めてdstatを試してみました。なんかUbuntuやとどうもメモリのカラムのusedとかfreeとかがラベルと実際が合ってない感じがしたので、メモリ以外をdstatで見つつメモリはwatch free -mでいくことにしました。

nginxのログ計測

kataribeが結構使いやすかったので、アクセスログの集計はこちらを使うことにしました。ほとんどSort By Totalのパートしか見てないかも。

アプリのプロファイル

言語はメンバー間で一番慣れてる度合いの高いrubyを使いました。rubyのプロファイラをいくつか試して、一番使いやすかったstackprofをメインで使うことにしました。

newrelicも導入しようかと思ってたんですが、なんかアプリプロファイリングの粒度が荒かったのとクエリ系は有料版じゃないとだめっぽかったので諦めました。がんがんベンチ回すこと考えたら30分単位で集計しても微妙かなっていうのもありました。

MySQLのクエリログ

これもいくつか試したんですが、うまく動かせなかったりして結局my.confのlong_query_timeを0にして標準のmysqldumpslowを使いました。そのまま叩くといっぱい出てきすぎるのでlessに流して上の方だけ見る感じでした。

redis実装の練習

redisを使った参考解答があったので、写経しながらベンチの上がり具合を見たりしました。

ISUCON4 予選 参考解答(Redis版)

これがrubyのredis実装のカンペになって結構当日の時間短縮になった気がします。

SPONSERD LINK

当日

1日目に参加しました。序盤のnginxやmysqlの設定周りはあとの二人に任せて、ぼくはアプリをメインで見るというざっくり分担でした。途中からは各々ボトルネックを探して気づいたやつを改修していくみたいな。

@tatsuyaoiwの自宅で、でかいダイニングテーブルに3人横並びで作業できたのはコミュニケーション取りやすくてよかったです。たつやありがとう。

試したこと

ざっくり当日やったことを書いていきます。

  • とりあえずベンチ流す
    → スコア200ぐらい
  • unicornのworker増やす
    -> デフォルトの1から8に増やして2,000ぐらい
  • あしあとredis化
    -> footprintsテーブルのみredisにしてスコア7,000ぐらい。3,000点一番乗りは逃しましたが、この時点で確か3位だったので、「これいけんちゃう!?」って一気にテンション上がる。
  • entryのクエリ調整
    -> この段階のslow queryでダントツトップだったので、entryをLIMIT 1000で取ってきてeachで回しながら条件を満たすものが10件溜まったらbreakっていう処理を、最初からwhereで条件書いてLIMIT 10にした。この辺から作業の並行度が上がって最終的にどれがどれぐらい貢献したか覚えてない。
  • mysqlソケット化
    -> これは他のメンバーがやってくれた。ネットワーク介さずに直接socketファイルでやりとりするっていうやつ。
  • session store redis化 -> これも自分でやったわけじゃないけど、session storeをcookieからredisに載せ替える修正。

これでベストスコアは10,700点台でした。予選通過スコアは14,000ぐらいだったので全然届かず・・・

試しきれなかったこと

終盤で実装ようとしたけどバグ取りきれなかったのが2つあって、どっちかでも成功してたら・・・っていう後悔があります。完全に実装力不足でした。

  • relationのredis化
    -> 前述のentryクエリを改善した段階でslow queryの3位ぐらいにrelationまわりのやつがいたので、他にredisにできるとしたらここかなーという感じで着手するもなかなかベンチ通らず。苦労してる間にどんどん他のチームがハイスコアを叩き出してたので、これやりきっても4位以内の2万点とかいかなさそうやなーと思って諦める。
  • トップページのキャッシュ化
    -> ラスト1時間ぐらいでもトップページがダントツで時間食ってたので、レギュレーションの「負荷走行中、更新を伴うHTTPリクエストに対してレスポンスを返してから1秒以内に関連するURI GETのレスポンスデータに反映されていること」というのを見て、ページまるごとredisにキャッシュして1秒以内に別スレッドでキャッシュ更新したらいいんちゃう?っていうのをトライしました。いい感じに実装できたように見えたんですが、ページの一部が正しく表示できないバグを最後まで取りきれずにタイムアップ。

悔しい。

終了後の全体チャットで勉強になったこと

参加者全員入ってるチャットの感想戦を見てて、その発想があったか!みたいなのとかそもそも知らなかったこととか色々あったので、来年はこの辺レベルアップしてがんばりたいと思います。

  • usersを起動時にグローバル変数に入れる or redisに入れる
    -> usersは更新がないので最初からグローバル変数に入れちゃえばリクエスト時にDB叩く必要がない。確かにN+1も含めてget_userめっちゃ呼ばれてたし効果ありそう。
  • AppArmor
    -> 一番最初にmy.confとnginx.confをリポジトリにシムリンク貼る?っていう話してたんですが、微妙に挙動変わったりしたらめんどそうなのでやりませんでした。結果的にこの罠踏まずに済んだんですが、そもそも初めて聞いた。
  • apparomor と iptables は初手で apt-get purge
    -> 賢い。こういう秘伝のタレも必要ですね。
  • MySQL5.6のinnodb_buffer_pool_dump_at_shutdowninnodb_buffer_pool_load_at_startup
    -> こんなオプションあったんですねー。全然キャッチアップできてない。
  • comments_of_friendsはSELECT c.* comments c JOIN relations r ON c.user_id = r.another WHERE r.one = ? ORDER BY c.id DESC limit 10とかで割と早く返ってきたので、あとは、 entry_id をリストに詰めて WHERE INで取った。
    -> 確かここが最後にslow queryでトップやったやつかな・・・JOINの仕組みとかパフォーマンスの知識があやふややったので他のメンバーに任せてあんまり自分では深追いしなかったんですが、usersを飛ばしちゃうだけでもだいぶ効果あったみたいですね。

というわけで、この感想戦チャットも個人的にめっちゃ勉強になりました。むしろこれを残したいがためにこの記事書いたようなもんです。

まとめ

今回の予選のコードやベンチマーカーも公開されてるのでまたしっかり復習しつつ、こういうスピードアップが必要になるぐらいユーザーが集まるサービスをまずは作りたいと思います。

ISUCON5 予選問題 参照実装ならびにベンチマーク等の公開