« shipped Plack::Middleware::Auth::OAuth | メイン | ワンライナーでOOなPerlモジュール使うときに、2回もモジュール名を打ち込んでいられるほど人生は長くない件について »

2012年11月 7日

#isucon2 で連覇させてもらってきました

主催の皆様素晴らしいイベントの提供本当にありがとうございました。

まさかの2連覇ですが、@fujiwaraの恐ろしさを再認識するとともに、@typesterのチート性能を見せつけられた感があります。

まずは個人的な反省点から

  • 去年よりかは大分成長しているつもりだったのに、@fujiwaraとの力関係が何もかわっていなかったことに衝撃
  • @typester(Redis期)がRedis使ってくることはわかっていたのに、競技中に brew install redisとかやってるのはダサすぎ

ということで、isucon2を振り返ります。

事前準備

事前にIRCチャンネルを作っておいてnopate botを呼んでおいたくらい。カヤックから別チームも出ていたので、お互いのチャンネルには入らないという紳士協定。

去年の経験から、revサーバーに直接gitリポジトリを作れれば捗ることは分かっていたので、その辺調べておく予定だったのですが、結局当日は@typesterに任せてしまいました。

11:00-12:00 下準備

  • 配られた資料を見ながら構成情報を転記してIRCに貼るなどする。(Songmu)
  • revサーバーを踏み台にする。authorized_keysに各人の公開鍵書き込み
  • 各サーバーでの.ssh/configの設定 (fujiwara)
  • revサーバーにgitリポジトリ構築 (typester)
  • スキーマとアプリのソース確認 (typester Songmu)
  • redisブランチ立ち上げ(typester)
  • capistranoによるdeploy環境構築(fujiwara)
  • my.cnfのslow_queryの秒数を0.1に変更

下準備ですね。

git周りはtypesterが得意なので任せ。deploy周りは去年使ったシェルスクリプトを流用しようかなーとか思ってたら、@fujiwaraがcapistrano入れてた。俺空気乙。

typesterが速攻redisブランチを切ったのには吹きました。見切り発車で先行実装ってのは去年の@sugyanが頭をよぎりましたが、さすが@typester、格が違いましたね。

アプリを見た感じだと、以下の点には気が付きました。

  • サイドバーの生成部分でjoinしまくっているところが気になる
  • ORDER BY RAND()とかやっててウケる
  • チケットのtableのマスの描画が重そう

11:57:36 <Songmu> これ描画してるのすごく重い気がするけど、消しちゃダメなのかな。
11:58:00 <Songmu> 毎回全件スキャンしてる。
12:00:05 <Songmu> ORDER BY RAND() アル―

とは言え、気になってもすぐに手を付けるんじゃなくて、本当に手を入れるべきか計測してから手を入れるのが鉄則。

12:00-13:30 細かいチューニング

とりあえずベンチ走行。DBに負荷かかってるのを確認しながら細かく調整。

  • ORDER BY RAND() を削除 (Songmu)
  • 静的ファイルをapacheが返すように変更 (fujiwara)
  • URL毎にresponse time計測 (fujiwara)
  • index追加 (fujiwara)
    • CREATE INDEX stock_order_id ON stock(order_id);
    • CREATE INDEX variation_order ON stock (variation_id, order_id);
  • apacheのプロセス数調整 (fujiwara)

サイドバー描画部分はorder_idにindex貼ったら特に問題ないレベルに改善。JOIN3つしてるからボトルネックに見えてキャッシュとかしたくなるけど、完全に撒き餌で、正攻法でindex張れば改善するという。

なんか、普段だったら見つけられるはずのindexの張り漏れをあの場では見つけられないのはなんでなんでしょうね。そして、それをあの場でも高速で発見する@fujiwaraはなんなんでしょうね。

このあたりからしばらくトップ維持。地味な調整でジワジワを他のチームを引き離していく@fujiwaraのチューニング力にビビります。

ただ、どのチームもなかなかブレークスルーを起こせていない状況で、イノベーションが起きれば一気にひっくり返される差でしかない。

13:30-14:00 ボトルネック判明

  • apacheをnginxに差し替えたらスコアが激減する現象に悩まされる (fujiwara)
  • dbサーバーにredis立てたりする(fujiwara)
  • /ticket/以外は大分軽くなったので、/ticketの処理の改善 (Songmu)
  • redisブランチが大体できてくる(typester)

nginxに差し替えたらスコアが下がるのは、nginxが後ろに回しすぎてしまってappが詰まることが原因の模様。

/ticketは残席数のカウントに無駄なCOUNTクエリーが走っていたので、そこのクエリを削減するなどしたらスコアは少し改善。(今回僕が書いたコードはこの辺くらいなのですが、これも結局redisブランチ採用により使わなくなります(涙))

この辺でDBのボトルネックは大分改善されて、appがボトルネックになってくる。そして、スコアが改善するにつれて /buy の購入処理でのdead lockの割合が増えてきて、ベンチFailする割合が増えてきた。

ORDER BY RAND()は無いにしても、競合の起こりにくい販売ロジックを考える必要があるなーという話になる。

/ticketのtable部分はキャッシュしないとどうにもならんなーという感じになる。

この辺でスコアは1600ticketsくらい。Redisブランチは一旦pushされたもののまだバグっていて動かない。

この時点でのボトルネックと対策方針は以下2点

  1. /buyの販売の競合を避け、高速にさばけるようにする
  2. /ticketのtable部分をキャッシュする

14:00-15:00 Redisブランチ炸裂

  • redisブランチの調整と完成(typester)
  • Starmanのworkder数調整(fujiwara)
  • お昼ごはん

ところがどっこい、販売処理のボトルネックを劇的に解消する飛び道具が炸裂します。それがRedis。

MySQLの在庫テーブルstockと販売履歴テーブルorder_requestをそれぞれ、Redisのセット型としてリスト型として持つようにして高速に販売処理を捌く実装を@typesterが見事に作り上げます。この短時間であれだけのアプリの改造をやりきったことには舌を巻く他ありません。

これがバグなく動くようになれば、販売処理のボトルネックは一気に解消することになります。この時間帯はサーバ上もredisブランチに切り替えてベンチ走らせながらデバッグ。僕はticketの表の描画部分がバグってたのでその辺typesterに助言したくらいで、わりとこの辺空気。

Redisブランチは無事に完走し、1900ticketsを叩きだす。

スコアより大きいのは、ここで購入処理のボトルネックが一気に解消されたこと。

ただ、スコアは思ったより伸びないねーという感じ。やっぱ/ticketの描画をどうにかしないと厳しいけど、同期処理だとしんどいから非同期処理にしたほうが良いかなーという話が出る。

Starmanのworker数の調整をしてみるも、ベンチマークプロセスの同時並列数が多いので、そこまで減らせないというジレンマ。

そんなこともあり@typesterはnode実装の方を試し始めた。

14:39:36 <typester> ここまで並列度高いとforkモデルは不利なかんじする
14:50:53 <Songmu> このままredisブランチが炸裂してしまうと、僕が空気になるのでつらい
14:51:17 <typester> アプリ非同期にしたくてつらい

15:00-15:40

  • node実装検証(typester)
  • お茶くみ(Songmu)
  • /ticketの部分の処理のプロファイリング(fujiwara)

基本に立ち返りticketの部分の処理をプロファイリング。Devel::KYTProfを入れてボトルネックを検証。やはりテンプレートの処理が重く、他のページの描画は数msなのに、ここは数十ms以上かかっている。確証を得てそこのキャッシュ戦略を考え始める。

僕はコーヒーが来たのでお茶くみなどしておりました。

/buyのタイミングでキャッシュするという実装も少し書いたりはしていたのですが、buyは毎秒数十以上捌くわけだから、そこで毎回キャッシュ作成するのも筋悪だよねーという話になって個人的に手詰まり状態。

この辺でredisブランチ採用する流れになってきたんだけど、僕の手元では動かせなかったのでbrew install redisとかやってた。ダサすぎる。

@typesterはnode実装が手元で動かないとかで諦めて戻ってきた。

若干手詰まり感。このままのスコアでも上位には行けるだろうしもしかしたら優勝できちゃうかもしれないけど、それだと出題者に負けた感が強すぎるので何とかしたいという雰囲気。

15:40-16:15

  • キャッシュ作戦会議
  • 実装

ということで真剣にキャッシュ戦略を考え始める。

購入処理の度にキャッシュしてたら効率悪いけど、1秒以内に反映されていれば良いというレギュレーションだから、それ以内の時間ならキャッシュできるよねってことで、毎秒更新するワーカープロセスを作りましょうという話になる。

一から作るのはちょっと時間がかかるから、既存のアプリを流用して、/ticket/update_cacheというURLにPOSTアクセスが来たら、テーブル部分のキャッシュを更新するようにして、そのURLを叩きまくるスクリプトを別で作るという作戦。

この作戦立案は@fujiaraなんだけど、去年のキャッシュ戦略もそうでしたが、短時間で実現可能な現実的な「力業」を瞬時に立案できるその発想力が凄まじい。その力を得るために、これまでどれだけの修羅場をくぐってきたのか想像もつきません。

この実装は分担的に僕がやるところだったんだけど、僕の手元でredisブランチが動かないという大失態でわたわたしていたので、結局@typesterに実装をお願いすることに。情けない限りである。

KossyとかXslateは僕のほうが慣れていたので、僕が後ろから見ながらペアプロ的に実装。程なく実装は完了。

テンプレのキャッシュはRedisで十分高速なので、memcachedは使わずにRedisに格納するようにした。Redis大活躍。

16:15-16:40 特別賞僅差

キャッシュの実装が完了して実際に動かしてみる。多少バグはあったものの、スコアは5500ticket前後を叩きだし、暫定トップに返り咲く。

この少し前に一つ頭の抜けだしたスコアを山形組が叩きだし、特別賞を僅差で持っていかれたのが残念である。

16:24:47 <Songmu> 山形組キテる
16:28:22 <Songmu> 特別賞持っていかれた
16:28:44 <typester> もうちょいここ効率化する
16:28:51 <typester> 更新しなくても良いのもループ回してるから
16:37:14 <Songmu> 山形組安定しない。
16:37:21 <Songmu> 安定させれば勝つる
16:39:30 <Songmu> トップ奪還
16:40:22 <typester> おしいなー
16:40:23 <typester> おしい!
16:40:30 <typester> 特別賞的な意味で

ちなみに、@fujiwaraがこの辺でapacheをnginxに差し替えていたようです。appが高速化したのでnginxに差し替えても詰まることがなくなったということでした。

16:40-17:20 キャッシュの見直しとプロセス分割

一応一位に返り咲いたものの、Failすることも多く安定しない。原因としては5ページ分のキャッシュを作る処理がappの負荷状況によっては1秒以上かかってしまい、そこでコンテンツの不整合によりベンチで撥ねられるというもの。

revでもplack立てたり色々試行錯誤したのですが、結局、dbにキャシュ更新専用のplackを立てることに。かつ、ひとつの処理で5ページ分のキャッシュを更新するのではなく、処理のプロセスを分けることに。

@fujiwaraがdbでplackを動かす準備をしている間に、プロセス分割の処理の実装は僕が担当。

この間、@typesterはレギュレーションのCSVのデータ永続化周りが気になるので、redisが突然再起動かかっても大丈夫かどうかの確認や調査をしてもらっていました。

17:20-17:40 アプリの凍結と再起動確認

作戦は見事に成功。ベンチも安定し、もうこれ以上はいじってもバグるだけだろうとうことでここで凍結を決定。あとは再起動してからちゃんとベンチが通るかを確認するだけとしました。

全台再起動してからのベンチも成功し後は終了を待つだけというかなり余裕を持ったフィニッシュとなりました。

山形組との一騎討ちの様相を呈していたのですが、山形組が5000ticket前後なのに対して、5500ticket前後のスコアで安定していたので、多分勝てるんじゃないかと思っていました。

17:40:50 <typester> redisの布教ができそうでなによりでございます
17:41:28 <Songmu> 完走すれば勝てる
17:41:32 <Songmu> 去年と同じや!
17:41:53 <Songmu> buyのボトルネックをredisでふっ飛ばせたのは大きい。
17:41:57 <Songmu> 飛び道具だった
17:42:01 <typester> ほほほ
17:42:07 <Songmu> typester++
17:42:10 <Songmu> まだわからないけど。
17:42:21 <typester> まぁしってても、仕事で使ってる人じゃないとたぶんうまく使えないですよね
17:42:27 <Songmu> そっすね
17:50:06 <Songmu> ローカルにRedis立てて置かなかったのが反省点。
17:50:44 <typester> w
17:50:53 <typester> そういえばredisブランチのローカルのうごか仕方を
17:50:57 <typester> 共有してなかったw
17:52:45 <fujiwara> このままこけなければ勝てる

二連覇

ということで去年に引き続き優勝させてもらうことができました。@fujiwaraの相変わらずのボトルネック発掘力と作戦立案力もさることながら、@typesterがあれだけの改造をしかもプロダクションレベルで通用するレベルで成し遂げてしまったことには驚きを禁じえません。

ホント間近で良い物を見せてもらいました。僕自身は不甲斐なくて悔しい部分もあります。

懇親会で@typesterに

「Songmuさんがいなかったら、あそこまで迷わずにredisブランチを立ちあげられなかった」

ということを言われて少しは救われた気になりました。

去年同様うれしさはありますが、去年以上に悔しさの大きい結果ではありました。引続き頑張って、@fujiwaraや@typesterとの差を埋めていきたい。

最終的なソースは以下にあるので、興味のある方は御覧ください。

https://github.com/kayac/isucon2

投稿者 Songmu : 2012年11月 7日 00:33