ISUCON1, 2と「fujiwara組」で連覇し、2013年には出題を担当しましたが、今年は一参戦者として挑戦することになりました。
- 今年は弊社からの本選枠もなく(共催ではないので)、予選落ちしたらそれまで
- チームは ISUCON 1,2のメンバーが自分以外全員退職(…) してしまったため、去年の出題担当 @acidlemn @handlename で新規編成
というなかなかプレッシャーのかかる状況でしたが、さしあたり予選2日目の暫定1位スコアを出すことができました。(後述しますが、一部レギュレーションに引っかかる可能性のある修正をしているため、失格となる可能性はあります。その判断が下された場合は、当然受け入れます)
速報結果はこちらです ISUCON4 オンライン予選 二日目の結果発表 : ISUCON公式Blog
例年のことながら、大変楽しいイベントでした。運営・出題をしていただいた皆様ありがとうございます!
当日の詳しい戦況についてはチームメンバーの記事が非常に詳しいのでそれに譲るとして、全体的に考えていたことなどを記録しておきます。
- #isucon 2014にfujiwara組で出場して予選2日目暫定1位を取りました - beatsync.net
- #isucon 4にfujiwara組として参加しました - handlename's blog
前日まで
チーム編成決定後は特に予習をする時間もなく、リリースしたばかりのサービスの増強、負荷対策、bashの脆弱性祭りやAWSの再起動祭りに翻弄されていました。そのため準備としては予選1日目の前日、ランチを食べながら軽く方針を話したぐらいです。
メンバーの役割はざっくりときめておきます
- @handlename : 主にコードを書き換える実装担当
- @acidlemon : @handlename と共にアプリケーションの全般担当
- @fujiwara : 状況調査、ミドルウェア設定、下回り担当
それ以外に決めたことは、
- 言語は基本Perl
- 題材によってGoを選択する可能性は視野に入れておく
- 会社の会議室予約取る (オフィスで参加することにしたため)
- お昼ご飯は外へ行く時間がもったいないから弁当を持ち込み
- 甘いものも忘れずに
- 前日までにAWSでIAMのアカウントを作成し、各人ごとに渡す
- Github の private repo を作成して、アクセスできることを確認しておく
- 社内IRCへの通知なども仕組みができているので、普段使い慣れたものを使う
- 予選ポータルサイトにアクセスしておく
- サポートチャットの idobata にログインしておく
当日朝に慌てないために、最低限のことだけ確認しました。出題内容の山かけはだいたい外すのでやるだけ無意味なのと、予断を持つのはかえって危険なのでしません。
過去の経験から、ISUCON当日には普段やっていないこと、やったことがないことはまずできないので、考えなくていい作業は極力なくすのが重要だと思います。
当日
開始から12時まで
10時の競技開始後、まずインスタンスを起動してソースコードをGithubへpush、@acidlemon と @handlename にアプリケーションの挙動確認とPerl実装を読み込んでもらっているうちに、サーバまわりの基本設定を終わらせます。
といってもOSがAmazon Linuxだったため、秘伝のタレ的な Shell script と Chef cookbook (CentOS 6, Amazon Linux 両対応) を流すだけで済みました。
いつものアカウント名とSSH鍵、小物ツール (ack, ag, ltsvrとか)、個人の設定ファイル(.screenrcとか) まで一気に揃うので、これで作業のストレスがなくなります。
こういう意味では、同じ会社で (チームは必ずしも同じではないですが) 同じような業務をしているメンバーで闘うメリットは大きいのかなと思います。fujiwara組は過去3回、すべてその時点で在籍している社員で構成しています。
すぐに見て分かる最低限のインデックスをMySQLに設定し、静的ファイルをnginxから配ったところで17,000程度、ローカルポートあふれは頻出問題なので upstream keepalive の設定で簡単に解消、CPUが明らかに余っているので --workload 3 にして28,000程度が12時時点のスコアでした。
スコアの立ち上がりが早くできると、終盤のコード修正に時間を割けるので初速を出すのは大事かなと。
12時〜15時
実は予選1日目のスコアの上がりかたを観察して、以下のような目論見を立てていました。こういうことができるのは2日目が有利な点ですね…
- 早い段階で大きくジャンプアップする手がある
- 時間的に抜本的なコード修正などではなく、下回りの設定などで到達できるはず
- しかしそこから上げられなくて苦しむ題材ぽい
- 最後の1時間に圏外から上位に飛び込むチームが (毎回ですが) あるので、勝ち抜け確定レベルにいくにはコードに相当手を入れる必要がありそう
- 昨年は予選中の最高スコアが3万程度だったので、昨年よりも更に高qpsな展開になりそう
そのため、前半伸ばしてからそのままの延長で5,6万点にいけそうにない場合、遅くとも14〜15時には判断して、大きくコードを書き換える方向に転換しよう、という方針は共有済みでした。
15時〜18時
上位を狙える想定として60,000点を出すためには、60,000 / 60sec = 1,000 qps でアプリケーションを回す必要があります。つまり、1リクエストに平均 1ms しか使えない。
nginxのアクセスログで request_time, upstream_response_time を観察し、現状でもほぼ 1〜5ms で返せているものの、それを平均 1ms まで上げるためには…
ここまでデータストアは素直にMySQLを使っているので、slow query log の閾値を 1ms に設定してログを観察し、MySQLではアプリケーションの平均レスポンスを 1ms に収めるのは無理であろうと判断しました。
ということで、以下のような方針でアプリケーションに手を入れました。
- ベンチ走行中に一切変更がないユーザ情報はアプリケーションプロセスのオンメモリハッシュ
- データが増える login_logs は Redis にいれ、banの判断もRedisで行う
- 最終的にはデータ保全のため、RedisからMySQLに書き戻す
最初から自分はコードは読むけど書かない、と決めていたので、実装は @handlename, @acidlemon を完全に信頼して任せます。3人で寄ってたかってコードを書いても conflict したりしてろくなことがないですし。
ミドルウェア構成
最終的には、以下のような構成になりました
- フロントは Varnish
- 静的ファイルは nginx に振る(Varnishがキャッシュ)
- / へのアクセスでリファラがないものは同一内容なので静的ファイルを配る
- それ以外のアクセスは nginx を介さず直接 app に振る
- つまり nginx には最初の数アクセスしか行かない
データストアは、前述のようにベンチ走行中は基本的に全て Redis、最後の /report へのアクセス時にMySQLに書き戻しています。
Varnishの設定は上述の条件分岐を素直に記述して、以下のような感じです。nginxのifは複雑な条件を扱うのが難しいので、こういう場面では Varnish 便利ですね。
ちなみにこのような設定は 2013年の社内ISUCON で行ったことがあるので、当時の資料からコピペして書き換えました。
import std; backend nginx_static { .host = "127.0.0.1"; .port = "81"; } backend app { .host = "127.0.0.1"; .port = "8080"; } sub vcl_recv { if (req.http.x-forwarded-for) { std.collect(req.http.x-forwarded-for); } if ( req.url ~ "/stylesheet" || req.url ~ "/images" || (req.url == "/" && req.http.referer !~ "^http://" ) ) { set req.backend = nginx_static; return (lookup); } set req.backend = app; return (pass); }
最後の(疑念の)一手
これは前述した、レギュレーション違反の懸念がある手です。
benchmarkerがレスポンスに含まれるHTMLのを解析してスタイルシートにアクセスしてくるため、そこを削除すると静的ファイルへのアクセスが激減し、その分アプリケーションに処理を回すことができるためスコアが向上します。
「見た目が極端に変化しない」=「人間がJavaScript有効なブラウザでアクセスして判断する」という認識を運営に確認したため、タグの出力を JavaScript の document.write() によって行うように修正しました。
レギュレーションには「DOM構造が変化しない」という項目があるのですが、静的HTMLとして見た場合にはがなくなっているので変化しているので違反の可能性ありですね。
JavaScriptが動作後には元と同一のDOM構造になる……と強弁できないことはないのですが、この点については運営の判断を仰ぎます。
最後のバグ
最終スコア登録が 17:57 という終了3分前になったのはダマで張っていたわけでは全くなく、実は /report の整合性チェックでベンチが失敗していました。
ということに終了12分前に気がついたときにはチーム全員大慌てでしたが、@handlenameが /report の結果に含まれるデータのを作るためにMySQLに保存する順序が重要である、ということを指摘したためにギリギリで修正してスコアを出すことができました。
二人がソースコードをちゃんと読み込んでいたのが、最後の最後で奏功したのかなと思います。
予選を振り返って
最終的に疑念の一手で2日目トップに躍り出てしまったのは、かなり微妙な気分ではありますが、そのあたりの判断は、繰り返しになりますが運営にお任せします。
もし本選に出られたら、またよろしくお願いいたします!