diary.sorah.jp

#isucon 3 予選まとめ

ISUCON 3 の予選、土曜の方に参加していました。 まず、とても楽しませていただきました。運営の方おつかれさまでした!

結果の方は、悔しいことに 1 日目 5 位以内には残れなかったのだけれど、 暫定予選通過チームを除いた 2 日通しのランキングで 3 位に残る事はできたので、 一応 (暫定) なんとか本戦には出場できる様子。

で、予選で何をしたかだけれど、問題については 公式の解説記事 を見てもらうとして以下に残します。

尚、使用言語は Ruby でした。スコアは 10813.4。

チーム “白金動物園”

  • mirakui, rosylilly, sorah
  • パンダ、ハト、(>ヮ<)/ ?
  • 白金台方面から参加
  • mirakuiと私がopsもしていた (記憶)

最終的に手を加えた事

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

Backfilled at , Published at