ISUCON7の予選をはてなメンバーで通過してきました
ISUCONは過去3回優勝させてもらっているわけですが、はてなのメンバーだけで優勝したい気持ちがあります。前回はてなのメンバーだけで出たISUCON4の時は本戦は出られたものの惨敗。今回はそのリベンジも兼ねて挑みました。
チーム名は、id:Songmu(ソンムー)・id:motemen(モテメン)・id:masayoshi(マサヨシ)の3人で出たので、「ソン・モテメン・マサヨシ」。役割分担は、僕が一応リーダーで、motemenがアプリケーションメイン、masayoshiがインフラメイン。
予選は、リモート体制で、はてなの東京オフィスと京都オフィスの会議室を繋いで参加しました。僕だけが東京オフィス。
予選参加は土曜参加にした。土曜参加のほうが、翌日の日曜日休めるし、その日はISUCONのことを気にしないで過ごせるので例年そうしている。また、日曜のほうが強豪が集まる気配がしたので、土曜の方が3位以内通過を決めてしまうのも楽かもしれないという打算もあった。
言語はGoで行くことに事前に決めていた。Perlも捨てがたかったのだが、問題の特性上Perlだと辛いこともあるだろうということでGoにした。ただ、Perlの方が明らかにスラスラ書けるし、GoでそこまでゴリゴリWebアプリケーションの表側を書いたこともないので不安もあった。
競技開始直後
事前のレギュレーションに「複数台」という記述があって、こりゃ、予選で複数台来そうだなーと思ってた。しかし、予選で複数台構成でやるのは出題チームの負担がヤバイので本気なのかな、とは思ってた。しかし、予選開始時間が遅れたこともあって「ああこりゃ絶対複数台構成だわ」ってなった。果たして3台構成であった。
当日マニュアルを読むと、帯域について丁寧に説明があり、そのうえ静的ファイルの304についての得点も事細かに書いてあったので「こりゃ帯域ネックになる要素があるな」「304を適切に返させたいんだろうな」みたいなところは読み解けた。
それで、ベンチマーク画面を見ると、複数台に対してリクエスト飛ばせるようになってたので「ああ、これは、1台だけで配信しようとすると最終的に帯域足りなくなるやつで、最後は2台か3台で配信できるようにしないとダメだろうな」と予想。
複数台で静的ファイルを配信させつつ、適切に304を返したいとなると「ああ、これは『ハイパフォーマンスWebサイト』で読んだやつだ」ってピンときた。つまり、複数台でそれぞれちゃんと静的ファイルの更新日時を揃えておく必要があるし、ETagの生成ルールも合わせておく必要がある。
なんか「CDNがどうこう」という話がでてますが、個人的には、CDN経由かどうかみたいなところはあまり考えず、ただ単に、その辺りのセオリー通りに従って、そのための設定をおこなったというところだった。
初期準備と初手 13:00-15:00
masayoshiにインフラ設定してもらっている間にレギュレーションとアプリも読む。そうすると以下の辺りが見えてきた。
- 静的ファイルをgzip配信する必要がある。特にフォント
- iconsの画像をMySQLに入れてるのは明らかによくなさそう
- fetchの未読カウントが効率悪すぎ
ベンチを回しても、大体その辺がネックになってるっぽかったので最初はそのあたりから手を付けることに。
icons画像はとりあえず、初期画像はDBから抜き出して、各サーバーに静的ファイルを配置してNginxから配信することに。後から追加で飛んでくる更新画像は各サーバーに配置するのは難しいので、WebDAVで配信するとか、Nginxで別サーバーにプロキシさせるとか、何らかのKVSに突っ込む形になるだろうなーとは思ったけど、それはそこがネックになってから考えようという話をした。
なので「これは一旦 try_files
だなー」とか言ってたら、motemenが秒速でスクリプトを作ってくれてNginxに設定入れて、各サーバーにファイルを撒くところまでやってくれた。ここのスクリプトはPerlを使っていた。Perl実装で、kazeburoさんのDBIx::Sunnyが使われていたのでそれを使うことができて便利。
masayoshiがこの段階で、AppArmorによってMySQLが起動しなくなるというトラブルに見舞われていて、皆でそれぞれサーバー設定を触る流れになった。僕も make deploy
で一撃でビルドしたアプリケーションを3台全部に撒いて起動できるようにしたりしていた。単に go build
したやつを scp
で撒いて、アプリケーションを再起動するだけの簡単なスクリプトです。全く、makeは最高だぜ。
この辺の、最初の読みは大体当たってたんだけど、この辺でわちゃわちゃと皆でサーバーを触っていたせいで混乱があった。特に、ここでiconsの画像のETagも揃えていると思ってたんだけど、実はそうなってなかった。これが後々足を引っ張ることになる。
/fetch の未読カウントの改善等 15:00-16:30
motemenがicons画像の対応やっている間に、 /fetch の未読カウントの改善方法を考えてたんだけど、 motemenがあっという間にiconsの対応終わらせてたので、ここも実装方針を話し合った後、motemenにお願いすることにした。方針としてはカウントテーブルを作る形。Redisを使うこともちょっと頭によぎったけどここでは温存することにした。
この実装を入れてベンチを回すと、35,000点くらい。なんか思ったよりスコアが伸びないし、Nginxで返しているはずのiconsでタイムアウトが出まくっている。つまり、帯域があたっている状況で何かがおかしい。
帯域の調整 16:30-18:30
iftopで見ても帯域上限当たってるし、304も全然返せてないし何かがおかしい。アプリケーション側でもgzip入れてみたり、Nginxのcacheやgzipの設定を見直して、2台配信にしたり、3台配信にしたりして試行錯誤しても、5万点どまり。この辺で、 COUNT(*)
はコードから撲滅はさせた。
チームも混乱状態で、他のメンバーも帯域ネックなのにワーカー数とかコネクション数とか調整し始めてたので、ちょっとマズイな、と思い「とにかく帯域をなんとかしよう。304の割合を増やさないとどうにもならないぞ」とメンバーを落ち着かせた。
これは何かがおかしいぞ、ということで、icons画像を各サーバーにcurlでアクセスして目視で見比べてみると、果たしてETagがずれていたのであった。
「ちょっと!ETagずれてんじゃん!」ということになり、以下のようにして各サーバーの静的ファイルの更新時刻を揃えた。
find /home/isucon/isubata/webapp/public -type f -exec touch -t 10200000 {} \;
これで、Last-Modifiedも揃うし、Nginxはデフォルトではファイルの更新時刻とファイルサイズを元にETagを生成するのでずれなくなった。(昔のApacheはinodeを元に生成したりしていましたね)
この対策でスコアは12万点まで上昇。しかし折り返し(17時)くらいまでにここまでくるイメージだったので、このタイムロスはかなり痛い。残り2時間ちょい。
この辺は、ちゃんとイメージを揃えていたつもりだったんだけど、リモートでやっていた弊害がでた所であった。
DBネック解消 18:30-19:45
この辺でやっと、帯域ネックだったところからボトルネックが移り、MySQLネックになった。ベンチかけている間にtopを眺めていると、MySQLが一番CPUを食っている状況。ここで打った手は以下。
- 追加画像をMySQLじゃなくて雑にRedisにぶち込む(!)形に
- N+1クエリ撲滅
スロークエリにはプロフィール画像の更新クエリが溜まっている状況だった。更新画像投稿はまだMySQLに行っている状況だったので、ここをなんとかしないといけない。競技当初からそこがそのうちネックになるだろうなと思っていたのでやっとそこまでたどり着いた感があった。
WebDAVとか使うのが正攻法なんだろうけど、使ったことが無いし残り時間も少なかったので、ここは雑にRedisに突っ込むことにした。ここのコードが僕が今回一番バリューを出したところだと思う。Redisはmasayoshiが2台目のサーバーに用意してくれていたのでそれを使うことに。これはナイスプレイであった。
このあたりの改善を入れたら、スコアも18万まで上昇。また、MySQLのCPU利用率も40%程度までに落ち着いた。
最終調整と再起動試験 19:45-20:40
このあたりでまた帯域が当たり始めた。この時点では、01と02の2台でしかリクエストを受けていなかったのだけど、帯域が足りない以上DBが同居している03でもリクエストを受けたほうが良いかもしれないと考え始める。MySQLは40%程度しかCPUを使ってないので、ギリギリNginxとAppの同居もいけるのではないかという判断をして、3台にベンチをかけてみることに。
すると20:27にベストスコアの246,625が出た。もう少し調整できそうだったが、もう時間が無いのでここでスコアを固めに行くことに。設定を見直して、再起動試験をかけていった。最終的な構成は以下。
- Server01
- Nginx
- App
- Server02
- Nginx
- App
- Redis
- Server03
- Nginx
- App
- MySQL
再起動後のアプリケーションの動作確認後、ベストスコアを狙って、何度かベンチをかけてみるがあまり良い点数が出ない。20:54に221,823が出たのでそれで妥協して打ち止めとした。
感想戦とか
競技が終わるまではメンバーと以下の様な話をしていた。
- やっとアプリケーションネックまで来たから、
pprof
回してボトルネック取るのとかやりたかった - 各サーバーのNginxから同じサーバーのAppに回すのではなくて、Server1-3で重み付けして振り分けて調整とかできればもう少しスコアを伸ばせたかもしれない(最終構成だとどうしてもDB同居のServer03に負荷が偏ってしまう)
しかし、出題内容は大体読み切っていたのに、やりたいことができなかったことに相当悔いが残った。当初のイメージではこの辺りまでは上位陣は確実にやってくるだろうな、と予想をしていた。そこから pprof
とか回してアプリケーション自体の改善に手を入れてスコアを伸ばさないと、予選通過は厳しいと思っていた。
予選通過 :tada:
なんとか予選通過はできたものの、最終結果を見ると、やはり予想通りで20万点強がボーダーラインであった。危うかった。
とは言え予選通過は嬉しい。これで、ISUCONは7大会連続で本戦会場に行けることになる(出題含む)。多分もうISUCON本戦皆勤なのは、941さんを除くと、fujiwaraさんと僕くらいしかいないのではないか。
本戦はチームメンバーがちゃんと3人顔を合わせて作業ができるのでもっとパフォーマンスが上がるはずである。レギュレーションの読み合わせや認識合わせをしっかりして本戦に臨もうと思う。
運営の皆様へ
予選からフルスペックのISUCON問題を出してくる辺りおみそれいたしました。「本物のISUCON」を予選から参加者に味わってもらえるようにするという気概を感じました。問題内容も素晴らしく、しかも競技環境自体もベンチマーカー含めてストレス無く快適でした。
本戦が楽しみです。