先日、社内 ISUCON(良い感じにスピードアップコンテスト) に参加してきました。 Livedoorで開催されたISUCONのミニ版で、 Webアプリをひたすら高速化するコンテストです。
高速化の対象はNoPaste。 テキストを共有するWebアプリです。 テキストの投稿・投稿の閲覧・投稿にスターをつける の3つの動作ができる簡単なアプリです。
新卒 vs 先輩ということで、それぞれ4チームが参戦。 チームは二人一組で僕は @Maco_Tasu くんと一緒でした。 @Maco_Tasuくんのブログも参照。
Recent Posts 生成クエリの高速化
高速化前のアプリのベンチマークの結果、スコアは77(≒1分あたりの捌いたリクエスト数)。 何も考えずにデータベースの全行を舐めるクエリを書いていたので、まあ、妥当なスコアですね。
重いのはサイドバーに表示される Recent Posts。 Recent Posts は表示回数が多く、 複数の行、複数のテーブルへのアクセスが発生するため、 きっとここがボトルネックになるだろうと予測してました。 そこで最初にこの部分を解決することにしました。
- とりあえずインデックスを張る
- クエリを修正してアクセスする行を最小化
- スターのカウントした結果をテーブルに格納
- オリジナルのデータベース構成では、スターした回数だけ行が増えてました
- 必要なのは投稿ごとのスター数なので、独立したテーブルに
- この時点で早くも重大なバグを組み込んでしまったことに、この時はまだ気がついていなかった・・・
nginxによる静的ファイル配信
僕がクエリをいじっている間、@Maco_Tasuくんには サーバの設定をお願いしました。
ログの様子を眺めてみると、cssとかjsとかの静的ファイルが結構な量ありました。 最初のスクリプトでは静的ファイルの配信もアプリでやってたので、 これをnginxを使って配信するように変更。 その他のリクエストはリバースプロキシを設定してアプリに投げます。
Starlet と Server::Starter
リバースプロキシを設定する際にアプリの起動スクリプトを編集する必要があったので、 ついでに起動時の設定を色々変更。 PSGI実行のStarletというのが速いと聞いてこれを採用。 Starlet使い方調べてたら、Server::Starterを使った例が出てきたので一緒にインストール。 ワーカーの数の数は適当に10個にしました。
ここで2回目のベンチマークを実行。 スコア1300程度で、その時点のトップ!
SSIを使ったサイドバーの埋め込み
お昼を挟んで、さらなる高速化を目指します。
topコマンドを眺めているとPerlで作ったアプリの負荷が大きい。 ほとんどテンプレートエンジンを呼び出しているだけの単純なコードなので、 ここを高速化するのは面倒くさい。 そこで、前段のnginxでキャッシュする作戦を採用することにしました。
もっともキャッシュが有効なのはサイドバーだろうと予想。 クエリの最適化をしたとは言え、サイドバーには100件程度の投稿が表示されるので、 クエリ実行にもレンダリングにも時間がかかるはず。 さらにすべてのページでサイドバーは共有できるので、大幅な高速化が期待できるはずです。
過去のISUCONの記事にSSI(Server Side Include)を使った例があったのを思い出し、 これを使ってサイドバーのみキャッシュ、nginx内でサイドバーを埋め込むように。
僕が SSIのタグ埋め込み、 @Maco_Tasu くんにnginxのキャッシュ設定を行ってもらうという役割分担で作業を再開しました。
サイドバーのキャッシュ
僕の作業はテンプレートを書き換えるだけだったのですぐ終わったんですが、 nginxのキャッシュがうまくいかない。 設定変えてnginxの再起動を何度も繰り返して、ここで2〜3時間時間を浪費してしまいました。
数時間悩んだ挙句、Set-Cookieがレスポンスヘッダーに入っているとキャッシュされないことが判明。
考えてみれば当たり前だ・・・人ごとに違うページが表示されるからサーバーでキャッシュされたら困る。
アプリ側でサイドバーだけクッキー返さないのが正攻法かなとは思ったのですが、
実際どうやるのかが時間内に調べられなかったので、
proxy_ignore_headers set-cookie Cache-Control Expires;
をサイドバーのURLに指定し、
ヘッダーを無視するようにしました。
この時のベンチでスコア1700!
キャシュする時間は長いほどいいけど、長くするとサイドバーとスターの数etcが食い違い、テストにFAIL。 そこで、Cache Purgeをnginxにインストールして、Perl側からキャッシュ削除。 削除するキャッシュのキーを指定する方法でしばらく悩んで、結局決めうちというひどい設定 (本当はURLなどを決定するはずだけどなぜかうまくいかなかった)。 なにはともあれ、これでFAILはなくなるはず!
・・・と思ったけど、なんだか時々FAILする。 スコアは確実に上昇して2300前後をとれるようになったけど、本番でテストFAILしたら一環の終わり。 キャッシュの寿命の設定だと思って、終了直前までキャッシュの長さの調整してました。
結果
テストFAILした!! No Score!!
敗因
敗因はただ一つ。** データベースの初期化スクリプトが間違ってた!! **
スターの個数を数えてテーブルに挿入するSQL文をベンチ開始前に走らせたんだけど、 「テーブルにすでに値が存在したときのことを考慮してなかった」 「スターが0個のときを考慮していなかった」 という致命的なバグがあり、 データベースが不完全な状態でした。
うわあああああああ!!!! 完全に僕のミスじゃいですかあああああああああ!!!
Cache Purgeインストール後FAILしていたのもおそらく これが原因です。
FAILしてなければ、事前のベンチでは先輩方と遜色がない程度のスコアが出てただけに、悔しい終わり方になってしまいました。 さらに新卒組は全チームテストFAILという残念な結果。
速いことよりも正しく動作することのほうが大事、 ということを身を以て体験できた一日でした。