予選を通過した #isucon 3 に参加してきました。おつかれさまでした! (予選の様子はこちら)
結果から言うと最終計測で fail してしまった。無念。人権がない。うまくいけば 20k くらいは乗れただろうけど fail したのでそんなのは関係がない。
以下、自分のチームが何をしたか等。
チーム 13: 白金動物園
- (Leader) Ops: @mirakui - パンダ
- Dev / Ops: sorah - ?
- Dev: @rosylilly - ハト
問題
- ユーザーは他のユーザーをフォローできる
- 画像を post するとフォロワーの TL にあらわれる
- アイコンは画像アップロードで変更かのう
- postした画像毎に閲覧権限のレベルがある
- JSON API のみ。一応 public/ に html があるけど、html+js でできたシンプルな API クライアントなだけ。
のような画像投稿サービス。リサイズ・クロップする部分がある。
すごい上から目線っぽくなっちゃうけどすごい楽しく良い問題だった。 DBは最初からばっちりクエリ含め爆速だし、キャッシュすれば勝ちとかでもなさそうだし、ビューの生成が重いとおもいきや JSON API しかないのでビューの生成をいかに速くするとか、DOM壊さないように頑張るみたいな事やらなくていい。
結果
fail
workload=4 server=1,2,3,4,5 を指定。
反省
fail した理由は、fail がおこるような状態で終えたのではないです! はい、いや、はい。ダメなものは、ダメ。
fail は今のところリサイズのファイルキャッシュ周りで問題が発生した可能性等を考えてる。
キャッシュが消えた時・DBリセット直後の不安定さを調査しきれていなかったのが反省。workloadを直前に変えてちょっと回してそれで ok みたいな判断をしちゃったのがよくなかったのかも。時間をとるべきだったね。
サーバ構成の概要
(以下、1番目から5番目のサーバをそれぞれ server1 や server5 のように表記)
-
使用実装は Ruby 版
-
server1 のみ MySQL
-
全サーバ nginx + app サーバ起動
-
画像のリサイズは Go で書かれたリサイズサーバに飛ばす。それ以外は Ruby の app (unicorn) へ。
- app.rb も画像リサイズサーバとの結合に必要な変更しかしてない。
-
MySQL のスキーマ・インデックス・クエリはなにも弄っていない
- Go 側の実装で若干の変更があるかも
go でかかれた画像リサイズサーバについては後述するけど、GET /image/:id
とか GET /icon/:id
の URL (当然 size パラメータも) をそのまま扱えるサーバになってました。
nginx + app.rb + リサイズサーバ は全サーバで起動していて、ベンチツールにはサーバ全台指定してベンチツールでまずロードバランスをしてもらった。
画像リサイズサーバのロードバランスについてはこの方法だと考える必要があったけど (どうやって投稿された画像をどのサーバへのリクエストでも見えるようにするかとか)、 それについては app.rb に細工して、あとは nginx レベルでごにょごにょしてがんばった。それも後述。
以降でてくる server_id は hostname 末尾, IP アドレス末尾が連番になっていたのでそれを server id としてつかってます。 isu131 だったら server1、isu132 だったら server 2。
app
app は /image
, /icon
の URL を返す時に、
/image/<server_id>-<entries.image>
や /icon/<server_id>-<user.icon>
の形になるような URL を返すようにした。
理由は画像リサイズサーバのロードバランスのため。
初期の画像については server1-5 全台に rsync していたけれど、新規にアップロードされた画像はアップロードされたサーバにしか存在しない。
UPDATE を事前に叩いとくのも面倒だったし初期画像は entries.image, user.icon カラムに server_id は含んでいないけれど、新規にアップロードされた画像はどこにあるかを示すためにその 2 つのカラムに <server_id>-...
のような形で保存しておくようにした。
なので、user.icon や entries.image の中身は、初期画像は aaa
, 新規に投げられたものは 1-aaa
みたいになった。
あとは画像を保存するときに拡張子を省いて保存するようにしたりとか。
app は画像の URL を返すときに server id がついてなければ users.id, entries.id 等でサーバ台数の余りをとって ((id % 5) + 1)
それを id として返してた。
nginx
nginx は主に app, 画像リサイズサーバにリクエストを投げるためにつかっていた。
app については普通に proxy_pass で localhost:5000 とかに飛ばしていたけど、画像については新規にアップロードされた画像だと 1 サーバにしか存在しないので、path に含んでいる server_id を見て別のサーバのリサイズサーバを直接叩くみたいな事をした。
location ~ ^/(image|icon)/([1-5])- {
rewrite ^/(image|icon)/([1-5])-(.*)$ /$1/$2-$3 break;
proxy_pass http://go$2;
}
location ~ ^/(image|icon)/ {
rewrite ^/(image|icon)/(.*)$ /$1/0-$2 break;
proxy_pass http://127.0.0.1:8088;
}
最初の location directive は server id がついていた場合。 app は初期画像についても返す時にはserver idをそれっぽくつけて API にはかならず server id が乗った URL がかえるようにしたのだけど、ベンチツールがなぜか初期画像の image column そのまま (server id がない) でリクエストを投げてくるらしいという事がわかったのでその対策として, 後者の location directive がある。
後者の location directive は server id として 0- をつけて localhost の画像リサイズサーバになげるようにしていた。 べつにこれ、やらなくてもリサイズサーバ側でうまくとるようにできたのではと思わんでもない (あせってたししかたがない)
画像リサイズサーバ
rosylilly が丁度手元に go で画像リサイズサーバをつくっていた 事もあり、メンバーの残りは知っていたので、何も考えずに採用が内定していた。それをベースにいろいろカスタマイズされた物が投入された。
前述したように GET /image/<server_id>-<entries.image>
あるいは GET /icon/<server_id>-<users.icon>
のようなリクエストがリサイズサーバには飛ぶので、画像を以下のように探す
-
基本的には
/image/
以降の文字列を全部 entries.image カラムを利用して探す。権限を確認するため /image だけはそのようになっている (icon については確か普通にサーバIDを取ったファイルを探してリサイズして返してる) -
ただし、初期画像についてはそのカラムの先頭に server_id が乗ってない (1-aaa のようなフォーマットではない) ので以下の対策をした。
- app.rb はそのような画像に対しては
(id % 5) + 1
のように余りを取って server id として、各サーバに割り当てていた - ただし DB の初期データには server id は image カラムに含まれていないので、画像リサイズサーバは
WHERE image IN (1-aaa,aaa) LIMIT 1
のようにしてサーバ ID がない場合のrowもひっかかるようなクエリを投げていた。
- app.rb はそのような画像に対しては
-
画像は
data/{image,icon}/{entries.image,users.icon}
のようにサーバID抜きで保存されているのでサーバID抜きでファイルを探す。
なので、結果としては
GET /image/初期image_id
->nginx@server1
->GET /image/0-初期image_id
->resizer@server1
->data/image/初期image_id
をリサイズGET /image/2-初期image_id
->nginx@server1
->GET /image/2-初期image_id
->resizer@server2
->data/image/初期image_id
をリサイズGET /image/1-aaa
->nginx@server1
->GET /image/1-aaa
->resizer@server1
->data/image/aaa
をリサイズGET /image/2-bbb
->nginx@server1
->GET /image/2-bbb
->resizer@server2
->data/image/bbb
をリサイズ
のようなフロー。nginx と含めて思ったのは、別に server_id ないリクエストの時に 0- をつけなくてもうまい事リサイズサーバ側でハンドルできたのでは、とか、いろいろあるけど、焦っていたししょうがないと思ってる。
でもこの辺の仕様をちゃんと考えた時に紙かなんかに残しておくべきだったかもしんないですね。結構突発で決めた。
その他
- デプロイは deploy.sh というものを書いてやっていた。全サーバで git pull して bundle install, go build から、supervisord.conf, nginx.conf の更新等をやってくれて便利だった。
- 初期画像は rsync -az で全サーバに配布した。
~/data.orig
に置いていて、ねんのためにもう一個コピーしておいた。 ~/webapp/data
には初期画像を symlink でおいてた。symlink.rb というのを書いて~/data.orig
にあるファイルをひとつずつ symlink していた。
良かった事
メンバーの守備範囲があまり衝突していないのは重要だと思う。 それでも複数人でappを弄る必要性があるなら、ファイル・プロセスを分割する事によってコンフリクトを防いだほうがよさそう。
- 序盤: mirakui=ops(nginxの整備,imagemagickのアップグレード), sorah=ops(デプロイの仕組みや初期画像のrsync), rosylilly=dev
- 中盤以降〜: mirakui=ops(計測・nginx), sorah=dev(app.rbをgo製リサイズサーバに向け改修), rosylilly=dev(go製リサイズサーバの調整)
opsで詰まったらわたし(sorah)が mirakui の画面を覗いて nginx.conf のミスを見つけたりしていた。基本修正とコミットはお任せしていたのでコンフリクトは発生しない。
こまってたこと
-
workloadあげたとき499エラーが出てたのは何だろう、画像のDLがおそすぎて向こうが切ったか、こっちが画像をなかなか返せてなかった?
- 講評聞いて timeline からくる多重リクエストでキャッシュの排他制御がおかしくなってたのではとも思うけど真相はわからず。
やれたなぁと思うこと
- データは毎回微妙に違ったりしてダメかもしれないけど、同じ image についてはキャッシュをつかいまわせるように image id としてファイルの md5 なりをつかえばよかったんじゃないかなー
- キャッシュの不安定さについてもうちょっと調査すべきだったなー
- 直前で workload あげる博打が失敗したので↑含めコードの fix は 17:30 前後でやるべきだったなー
悔しい…
以下、は時系列のログです。
時系列ログ
- 11:18 ログイン
- 11:24 init-db.sh, isucon.dump.sql, webapp を git 管理して github.com の private repo に push
- 11:34 api.md 等を読む。どうやら今回 view なさそう! ワー!
- 11:45 スキーマとか SQL を読んでチューニング必要のなさを実感。
- 11:51 mirakui「imagemagick のビルドならまかせろ」
- 11:59 deploy.sh ができる
- 12:15 rosy,リサイズサーバのカスタマイズ開始
- 12:23 3GB の rsync を開始
- 12:30 終了
- 13:21 画像の分散方法 (GETするときのpath 等も) が決定される
- 13:35 foreman が gemfile に例によって入ってない罠を踏む
- 13:43 rosyほぼ改修完了
- 14:09 nginx.conf が git commit される
- 14:21 app.rb 改修完了
- 14:27 それぞれリサイズサーバや app のバグ取り
- 14:47 nginx の daemon off; にしないと supervisord で上げた時に無限に上げてポート重複なエラーがいっぱいでてしまう罠を踏む
- 15:10 rosyがリサイズサーバがなぜかうまくクロップくれないバグがあったのでなおしていた
- 15:44 ベンチをまわしてひたすら fail をとっていた
- 16:30 cropとresizeの順序が逆
- 16:36 follow_map に id はない罠を踏む
- 17:00 workload をあげるとなぜか 499 がよくでる問題について調査していた。proxy_buffer まわりを弄ってた
- 17:45 release.txt に書く最終 workload をきめるため最後に何度かベンチしていた
- 18:00 終了〜
まとめ
運営・他チームのみなさんおつかれさまでした! fujiwara さん楽しい問題をありがとう!
(また来年?)