ISUCON 3 の予選、土曜の方に参加していました。 まず、とても楽しませていただきました。運営の方おつかれさまでした!
結果の方は、悔しいことに 1 日目 5 位以内には残れなかったのだけれど、 暫定予選通過チームを除いた 2 日通しのランキングで 3 位に残る事はできたので、 一応 (暫定) なんとか本戦には出場できる様子。
で、予選で何をしたかだけれど、問題については 公式の解説記事 を見てもらうとして以下に残します。
尚、使用言語は Ruby でした。スコアは 10813.4。
チーム “白金動物園”
最終的に手を加えた事
middlewares, app
- Ruby 2.1.0dev, redis が入った
httpd
- Apache ではなく nginx をフロントにして unicorn にリバースプロキシするようにした
- unicorn のワーカ数は 50 だったはず
- static_gzip で css,js とかをさばくようにしたけど、同じマシンの上でベンチがはしるのでむしろ gzip きったほうがよかったみたい
app
- ユーザの新規登録という機能が存在しないため、user_id→username の結び付けを記録したファイルを —init のスクリプトで生成、アプリ起動時に読み込んでメモリ上に保持
- memcached もどきに private=0 なメモの総数をキャッシュさせた
- memcached もどきに SELECT * FROM users WHERE id=? をキャッシュさせた
- markdown のレンダリングを redcarpet にした
- html カラムを memos に追加し、POST /memo で INSERT 時、あるいは GET /memo/:id で html カラムが null の場合 (初期データ) で書き込んだ。
- ↑ /memo/:id の時は redis の publish でデータだけ別のプロセスに投げて、そのプロセスに UPDATE を発行させていた。
- GET /, GET /recent/:page でリストの生成を app.rb でやらせた。(view で処理すると遅い)
- あんま効果なかったけど session[token] を sha256 から md5 にしたりした。user_id をそのまま返そうと思ったけど悪魔っぽいからやめた。
MySQL
ALTER TABLE `memos` ADD COLUMN `html` text;
ALTER TABLE `memos` ADD INDEX `idx_1` (`user`,`is_private`,`created_at`);
ALTER TABLE `memos` ADD INDEX `idx_2` (`is_private`, `created_at`, `id`);
SQL
GET /
users へのクエリを消しました。それ以外は変化なし
GET /recent/:page
Before
SELECT * FROM memos WHERE is_private=0 ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET #{page * 100}
SELECT username FROM users WHERE id=?
After
SELECT created_at FROM memos WHERE is_private=0 ORDER BY created_at DESC, id DESC LIMIT 1 OFFSET #{page * 100}
SELECT * FROM memos WHERE is_private=0 AND created_at <= ? ORDER BY created_at DESC, id DESC LIMIT 100
-- users へのクエリは消えた
GET /memo/:id
Before
SELECT id, user, content, is_private, created_at, updated_at FROM memos WHERE id=?
SELECT username FROM users WHERE id=?
SELECT * FROM memos WHERE user=? #{cond} ORDER BY created_at
After
SELECT id, user, content, html, is_private, created_at, updated_at FROM memos WHERE id=?
-- users へのクエリは消えた
SELECT * FROM memos WHERE user=? #{cond} ORDER BY created_at, id
POST /memo
Before
INSERT INTO memos (user, content, is_private, created_at) VALUES (?, ?, ?, ?)
After
INSERT INTO memos (user, content, html, is_private, created_at) VALUES (?, ?, ?, ?, ?)
Gemfile
gem 'rack-mini-profiler', '0.1.31'
gem 'redis'
gem 'redcarpet'
コミットログが 後半になるにつれ徐々に酷くなり焦りを感じているのがわかって おもしろかったですね。ええ。
反省
- memcached が memcached じゃない事にきづかなかった。いろいろキャッシュさせようとして、Dalli がなんか marshal data too short っていってくるなーと思ったら、MySQL だったんですね… varcharとかの長さ不足だとしたらなっとくだわー。次回からは netstat -lp します。
- Web アプリケーションにはビューっていうのがあるんですよ。。。なんでviewに手をいれるのが終了1時間前なの。
- Redis とかのパフォーマンスチューニングの予習忘れてた。やっておこう
- KLabの方と同じように、bin/markdown は普通にプロセス起動してるし重そうだなーと思っていたけど、HTML 構造変わりそうで夕方までさけてた。試すだけなら試せばいいのに… (それやっただけでスコアが跳ね上がった)
その他
- memcached もどきへのキャッシュでうまくいかず、redis に変えたら redis 側がネックになったきがしたので多様しなかった。
- 思えばキャッシュしたところは user_id→username の部分と total のカウントと SELECT * FROM users くらいだ。memcached もどきという事にきづいていろいろキャッシュさせたらさらにスコア上げられたのかも。
- rosylilly が HTTP インタフェースをもったスキーマがハードコードされた go 製のシンプルな DB を書いていたけれど、エラーを潰しきれずあきらめた。
- ↑を書いてる途中で go 組込みライブラリのバグを踏んだらしくパッチができた。
- Ruby 2.1.0dev にある ”f リテラルを使っても問題なさそうなところにひたすらつっこんでいったらスコアが若干落ちた。詳細は追ってない。ベンチツール公開された暁にでも。
- ほんとどうでもいいけどベンチツールが go だってわたし気付いてたよ。
時系列での出来事一覧
どうでもいいけど changelog.vim と ChangeLog で行動ログとるのオススメです。
10:00
- 「じゃー11時開始だし10時集合にしましょうか」…(翌日)…「ごめんなさい10時開始でした!」
- AMI からインスタンスを用意、あらかじめ用意しておいた authorized_keys を投入して作業開始
10:30
- htop とか ruby のビルドに必要な諸々を入れた
- あらかじめrbenv入れてsvn coしてtrunkをビルドするスクリプトがあったので、とりあえずそれで Ruby 2.1.0dev を入れた。終盤投入してみたけどその結果は後述。
- mirakui「今回 private フラグとかセッションがある!」
- webapp ディレクトリをgit管理下にしたりした
10:40
- コード読みながら SHOW CREATE TABLE したり EXPLAIN 叩いたり
- GitHub wiki にまとめてた
11:00
Score: 2510.6
- ほぼ初期のコードで, tcp/80 を nginx に切り替えて unicorn (UNIX socket domain) にリバースプロキシする構成にした状態のスコア
11:15
- total カウントキャッシュできるんじゃないか、とおもってとりあえずmemcached(もどきだったけどね)に入れた
- mirakui がベンチマークの傾向をさぐりつつ、rosylilly が rack-mini-profiler をいれたりしつつ、わたしはmarkdownのキャッシュにとりくんでた
- 「セッションが2リクエストまでしか使いまわされないっぽい、うける」
11:24
- rosylilly「やっぱ Go で DB かきなおせる気がしてきたわ」
11:35
- last_access つかってんのかなーと思いながら放置
11:40
- 「my.cnfがみあたらない」「えー」「locate cnf|grep $cnfしてさがすといいかも、よかったlocateうごいてて」
12:00
- mysqlのquery,xqueryメソッドをモンキーパッチしてstdoutにクエリと時間を表示するように
12:15
- 「user_id から username 引いてくるクエリが多すぎるしなんとかしたい」
- 「新規登録という概念ないし、とりあえずファイルに落としておいてアプリで永遠に保持しとけばいいのでは」
- やりました (CSVもどき吐いて起動直後からずっと保持)
12:19
- 「おい今本番ぶっこわれてんぞ」「えっ」
- 「あれーisucon3 benchでカラムとかindex消えるーー」
- 「あっ —init で初期化後の処理決められるみたい、そこでやらないとだめそう」
12:20
- get /memo/:id のよくわからないループとかなんだろう (SELECT * FROM memos WHERE user=? … ORDER BY created_at)
- あーolder, newerメモのやつか、とりあえず to_aつかったり each_with_index いれて読みやすくした
13:00
- rosylilly「goのDB実装だいたいできてきた、アイアムザヒーロー~」
13:10
- mirakui が Cache-Control をいじるも既に Cache-Control をいじる実装がある事にきづいておらず、ベンチでエラーがでる
14:00
- htmlカラムを追加、新規にweb app経由でポストされたデータについてはPOST時に、初期データについては最初にレンダリング走ったタイミングで redis.publish して別プロセスで mysql に UPDATE をかけるようにした
- バックアップがわりのAMIつくるときにまちがえて reboot が走る事故
- markdown 生成で一時ファイルをやめてパイプをつかうようにした
14:25
score: 5170.3 (fail 1)
14:45
- mirakui が /recent のクエリ改善をする
14:55
score: 5438
15:47
score: 5368.5
16:10
- sorah「うっ5位から漏れてる…」
16:25
- get_user をキャッシュするように
17:00
Score: 8667.5
- redcarpet を投入。KLabの方 と同じように, HTML 構造が変わってダメだと思いこんでやってなかった。やって戻せばいいだけなんだからさっさとやるべきだった。反省
17:10
Score: 8800
- たしか Ruby 2.1 を投入
- sorah「そういえば views 以下まったく見てないなあ」rosy「えっ…」sorah「Webアプリケーションにはviewってのがあったんだよなあ。はー」
17:30
- GET /recent, GET / のリスト生成を app.rb 側でやる (view (erb, erubis) にやらせない)
17:45
- セッションidを MD5 にして毎回乱数生成器を作るのをやめる
17:50
- クエリ改善をする
18:00
Score: 10813.4