#isucon に行ってきました (team_karakaniチーム)
こんにちは。ほぼ1年に1回しか日記を書かないkarakaniです。重い腰がようやく上がり日記を書いてます。
#isucon 行ってました。@mutotaiju と @3xv とで善戦してきました。準優勝で入賞でした。
まとめと反省点
- 地道なボトルネックの調査は重要。
- 時間的な制約がある中で新しいことはやらないほうがいい(最近使い始めたGitを使おうとしたけど無駄に時間使った)
- 経験も重要(他のチームのブログ見ててとても参考になりました)
やったこと
次のことをしました。
1. アプリケーション(データベースへのアクセス)のパフォーマンスアップ
2. 静的コンテンツの扱い・リバースプロキシの設定
3. キャッシュ戦略(失敗)
キャッシュ戦略は中途半端、というかできませんでした。
データベースはデチューニングされているのでは? という声がありましたが、私たちのチームではほとんどチューニングはしませんでした。ただ設定ファイルを見る限りデチューニングされているかもしれないというのは薄々感じてました。感じてたレベルですが。
1. アプリケーション(データベースへのアクセス)のパフォーマンスアップ
- 昔使ったプロファイラでクエリ解析してみた
- JOINを使った超遅いクエリがある。EXPLAINの結果も良くないし、頻出クエリのレスポンスに0.3秒は致命的。
- なのでカラムを追加し、JOIN無しでデータを取得できるようにした。
テーブルはarticleとcommentの2種類しかなく、データ量も数千件?程度だったのですが、articleの最新コメントに応じて取得する、というクエリでインデックスが適切に使われていなかったため、articleのカテーブル構造を変更し、 JOIN無しで取得できるようにしました。
この変更は他のチームでも行われてたので常道なのかもしれません。
-- 元のSQL SELECT a.id, a.title FROM comment c INNER JOIN article a ON c.article = a.id GROUP BY a.id ORDER BY MAX(c.created_at) DESC LIMIT 10; -- スキーマの変更 ALTER TABLE article ADD COLUMN last_comment_at timestamp; ALTER TABLE article ADD INDEX (last_comment_at); -- 既存のデータへの変更の反映 -- 記憶を呼び起こして書いてるので間違ってるかも… UPDATE article AS a SET last_comment_at = (SELECT created_at FROM comment AS c WHERE a.id = c.article) -- 変更後のSQL SELECT id, title FROM article ORDER BY last_comment_at DESC LIMIT 10;
最初の数分はクエリの投げ方だけで対応できるのではないかと思ったのですが、アプリケーションの規模を見る限り、コメント、記事の投稿が行われるポイントを把握することは簡単だったのでカラム追加で対応することにしました。
今思えば、別チームでトリガを使うほうがスマートな方法だと思いました。(もっともトリガの使用は実運用では賛否両論になるという話を聞いたことがありますが、それは仕様如何の問題でしょう。)
2. 静的コンテンツの扱い
- リバースプロキシ(以下リバプロ)は単純にリバプロしかしていない
- なので、アプリサーバーまでリクエストが届いている静的コンテンツをフロントのリバプロで返すようにした
- ついでにApacheをNginxに変更しようとしたが、諦めた
難なく静的コンテンツへの切り替えは出来たのですが、httpdからNginxへの置換えを行ったところ、パフォーマンスが落ちるという問題に直面。
直感的にKeep Alive周りで不具合を起こしているのではないかと思ったのですが、"リバプロ-アプリサーバー間"のKeep Aliveが機能していないせい(Nginxをプロキシ動作させる場合にはHTTP/1.0で動作しKeep Aliveが動作しないことを知っていました)と勘違いしたため、フロントエンドのKeep Aliveで主催者の罠が仕掛けられてるせいだとはわかりませんでした。
ちなみに、その後の作業中の調べでわかったのですが、そもそもアプリ側がもともとKeep Aliveに対応していないことがわかっていたので、もっと調べを進めても良かったのではと反省しました。
アプリの起動オプションに "--disable-keepalive" というのがあったので単純に外してみたのですが、上手いこと動作しないっぽいのでアプリ側のKeepAliveはずっと無効のままだったようです。
3. キャッシュ戦略(失敗)
何処にキャッシュ入れよう?
1. データベースでのクエリキャッシュ
2. アプリでのデータベースからのデータの取得時のキャッシュ
3. アプリでのHTML生成後のキャッシュ
4. リバプロでのキャッシュ
- アプリの調べを進めたが、結局何処にもキャッシュ入れられなかった。
1番目のデータベースのでクエリキャッシュは他のチームのブログを見て思い出しました。が、キャッシュの場所としては奥深くすぎるかなと思いあまり考えてませんでした。
2番目のデータベースからのデータ取得時のキャッシュはどのチームも考えてたことだと思います。が、他のチームもここでキャッシュをするだろう&HTML生成コストも減らしたい願望があったので、2番目のキャッシュ場所に加え、3番目のHTML生成場所でのキャッシュも考慮に入れて調査をしました。
4番目のリバプロでのキャッシュは、今回の「POST実施後、1秒以内にコンテンツに反映する」という条件に適合できるかどうか確証がなかったので全く考慮に入れてませんでした。
確かに今回のトップチームの結果を見ると、結果が明らかなようで、キャッシュメカニズムの動作の調査にそんなに時間もかかるものではないため思い込みだけではなく地道に調査をしとくべきだったというのは今回の反省点の一つです。
結局、キャッシュ機能は動作させることができず、導入できませんでした。
DBサーバーが3台目のアプリサーバーに
ところで、「キャッシュ機能を有効にした場合、データベースサーバーってほとんど仕事してないよね」という話題になり、「じゃあ、データベースサーバーもアプリサーバーにしたら早いかもね!」ということでデータベースサーバーもまさかのアプリサーバーとして動作させることにしました。
今回のような構成の場合に実環境でそんな事するのか…という疑問も湧きますが、仮想サーバーの導入目的の一つに資源の有効活用というのがあると思います。実際の仮想サーバー上ではこんな構成にはならないと思いますが、今回の様に資源の有効活用をするというのは大切な事だと思います。なので今回のような構成には目をつむって良いと思います。。
結局のところ、アプリキャッシュはうまいこと行きませんでしたが、一番最初のDBアクセスの部分の対応でDBサーバーの負荷は十分下がっていたので結果として高速化にはつながっていたのだと思います。ここを考慮すると2位になったのはまぐれ的な要素があるのではと自身がなくなります。
他に考えていたこと・試したこと
同時接続数
なのでプロセス数は100に変更し、バックエンドのMySQLサーバーの最大同時接続数も200超にしておきました。メモリ消費はPerlのコード数も大きくないし、データベース上のデータサイズも大きくないので、おそらくそんなに問題ないんじゃないかなと思っていました。
結局、この対応は必要ありませんでした。
負荷状況の計測
- topとvmstat、netstat使って負荷状態を調べたりステータスを確認してました
- 用意されたグラフは実は粒度が低かったのであまり見てませんでした…
- 目の前の「いんふらえんじにあー」チームさんのプロジェクターが羨ましかったです
これから反省会する
とりあえず、 @mutotaiju と @3xv とで反省会としてAWSでもう一度 #isucon アプリの高速化に挑戦しようと思っています。反省会終わったらまた結果を書きます。
最後に
主催していただいたlivedoor技術部会様、参加された方々、ありがとうございました。
楽しかったです! また機会があったら参戦します!
コメント
コメントできません (ログインするとコメントできます)
コメントはまだありません