ISUCON7予選の上位陣の戦略まとめ

Speee社でISUCON7の復習会をやったのでその資料を公開します。


ISUCON7復習会 2017/11/08 at Speee Lounge.

original repo: https://github.com/isucon/isucon7-qualify

概要

ISUCON7の予選突破組の上位陣の戦略をいくつか分析してみました。

下準備、前半、後半くらいにわけてみていきます。

下準備編

  • ログの分析用のツール
  • コミット&デプロイの方法
  • 開発スタイル(ローカルでがんばるか or 本番のみでいくか)

あたりを事前に決めておくと速やかに作業に入れます。上位陣の報告エントリは非常に参考になります。このエントリでは自明でないところだけさらっと確認します。

分析ツール

  • alp - Access Log Profiler for LTSV
  • pt-query-digest in percona toolkit
    • mysqlのslow logやtcpdump経由で分析するツール
  • ngrep, tcpdump
    • アプリに手を加えずにrequestを解析できる
    • requet headersはヒントの宝庫
    • たとえば If-Modified-Since がconditional getのヒントになったり
    • 例: sudo ngrep -d lo0 -W byline -q port 3000 (macOS)
    • とはいえよく分からん場合はアプリに手を加えてもいいと思う
  • 当日サーバにログインしたら一通りログ分析ツールが動くかどうかさらっておく
    • デプロイスクリプトでログローテートなどもできるとよい
    • うちは今回はそこまでやらなかったが

開発スタイル

  • サーバーにログインして直接編集 vs 必ずローカルでのみコミット
  • 直接編集するとconflictして悲惨なことになったりするのでローカルでの開発奨励
  • デプロイスクリプトは超大事
  • ローカルでアプリを動かすようにするのは大変なのでそこはチームの判断で
    • スギャブロエックスではローカルで動かさずに開発した
    • ローカルで動かせると開発は楽になるのでやってもよい

前半戦

  • コードリーディング
  • ボトルネックの把握
  • アプリの設計にかかわる大きな変更をするかどうか

このあたりをやっていくことになります。ISUCON7前半での大きな問題は「画像をMySQLに突っ込んでいる件」と「帯域がサチってスコアが伸びない」でした。

帯域がサチってスコアが伸びない件は Cache-Controlpublic を追加することでベンチマーカーがキャッシュしてくれるので、各チームに違いはありません。画像配信だけに注目するといろいろ違いがあります。

最終的な構成は後半戦のセクションで紹介するとして、前半戦はざっくり流します。

空中庭園

http://eagletmt.hateblo.jp/entry/2017/10/24/010832

とりあえずベンチマークを実行してアクセスログを見て GET /icons/:file_name が遅いことがわかり、コードを読むと MySQL に画像を入れてることがわかったので、社内 ISUCON でも見たな~と思いながらとりあえず Redis に入れることにした。

  • Redisを建てて1つのサーバに集約する
  • Goのアプリで画像バイナリを配信

スギャブロエックス

http://kazeburo.hatenablog.com/entry/2017/10/23/181843

iconの画像配信をどうするか、というのがまずの問題でした。画像データはisucon3でやったのと同じく、webdavで1台に集める方法をとりました。

  • nginxのwebdav機能で画像ファイルを1つのサーバに集約する
  • nginxで画像ファイルを配信

fujiwara組

https://beatsync.net/main/log20171023.html

@handlename に初期データをDBから吸い出してファイルに落とすのをお願いして、こちらはファイルがアップロードされたら(ひとまずDBにも書く処理は残しつつ)ローカルのnginxが配信できそうなディレクトリ(あらかじめ静的配信ディレクトリにiconsディレクトリがあって運営の優しさを感じました)に保存するように変更します。最終的にはnginxのtry_fileを使う予定でしたが、ひとまず最初はアプリ側で画像がローカルにあったらそれを返すようにしてDBの負荷を下げます。これにより、1台でしか動かせなくなりますが、どうせ詰まってるのDBなので前に2台置いてもムダってことでしばらくベンチ対象は1台で高速化を進めました。

  • 画像をファイルシステムとDB両方に保存
  • アプリは画像ファイルがあればそれを配信、なければDBから読み出して配信

MSA

http://mizkei.hatenablog.com/entry/2017/10/23/182820

ユーザのアイコン画像がDBにBLOBで入っていたため、それを引き剥がしました。 アプリとしては特に難しいこともないので、 POST /profile でmysqlにデータを入れていた箇所をファイルに書き出すようにして、 /initialize でデフォルトユーザのアイコンをDBから引き出して、ファイルに保存しました。

インフラとしては、初期の状態ではDBサーバのリソースがだいぶ余っていたのでappもおいてしまい、DB+appのサーバーに POST /profile をproxyして、静的ファイルは全てそこにおいておき、 /icons へのリクエストをそちらに流すようにしました。

  • 画像はファイルシステムに保存
  • POST /profile (画像ファイルの受け口) を画像配信サーバにproxyすることで集約

白金動物園

/icons の画像を MySQL から普通のファイルに引っ張りだす

rosylilly が実装。 /profile でファイルに保存するようにして MySQL に入れるのをやめた。 isu1 が front だったので、isu1 に画像を置くことにして、 /profile, /icons は isu1 の app プロセスで捌くことに。

  • MSAと同じ

後半戦

  • 3台のマシンリソースを使い切るかというのが重要
  • つまり分散システムの設計と実装
  • N+1などの細かいところも潰していく

タスクの優先順位はpt-query-digestやalpで分析して重要そうなものから手をつけるのが王道です。

空中庭園

http://eagletmt.hateblo.jp/entry/2017/10/24/010832

最後に Redis の負荷を分散させるために3台に Redis を入れてシャーディングするというのをやっていた。均等にばらけるのか不安だったけど id を3で割ったり icons のハッシュ値を適当に数値化して3で割ったりして接続先の Redis を切り替えるようにした。 MGET とか HMGET を使っている箇所が面倒だったので、そこは3台に同じクエリを投げてからその結果をマージするようなコードを書いた。

() benchmarker

package "isu1" {
  [nginx1]
  [app1]
  [redis1]
}

package "isu2" {
  [nginx2]
  [app2]
  [redis2]
}

package "isu3" {
  [nginx3]
  [db3]
  [redis3]
}

[nginx1] ..> [app1]
[nginx2] ..> [app2]

[app1] --> [redis1]
[app1] --> [redis2]
[app1] --> [redis3]
[app2] --> [redis1]
[app2] --> [redis2]
[app2] --> [redis3]

[nginx3] ..> [nginx1]
[nginx3] ..> [nginx2]

[benchmarker] --> [nginx1]
[benchmarker] --> [nginx2]
[benchmarker] --> [nginx3]
  • 点線はただのproxy
  • DBにはapp1, app2 からアクセスされるけど自明なので省略

所感: 最後までappでファイル配信して1位通過したのはすごい。これはGoでなければできない判断。ただしこれにより sendfile(2) が使えなくなったはずなのでベストかどうかは不明。水平分割も負荷分散という意味では大変すばらしいが、当人が不安だったと書いているとおりややリスクの高い判断だったとは思う。

スギャブロエックス

http://kazeburo.hatenablog.com/entry/2017/10/23/181843

3台のサーバをisu701、isu702、isu703とすると、

  • isu701 - nginx(reverse proxy), app
  • isu702 - nginx(reverse proxy), app
  • isu703 - nginx(reverse proxy, webdav), mysql

のような構成になりました。703はmysqlが動くことがあり、appは動かさず他の2台にproxyしています。

  • 画像ファイルはWebDAVでDBサーバに集約させる
  • DBサーバはリクエストを受けるだけで他2台へproxy
  • nginx.conf, nginx03.conf
() benchmarker

package "isu1" {
  [nginx1]
  [app1]
}

package "isu2" {
  [nginx2]
  [app2]
}

package "isu3" {
  [nginx3]
  [db3]
}

[nginx1] ..> [app1]
[nginx2] ..> [app2]

[nginx1] ..> [nginx3] : proxy /icons
[nginx2] ..> [nginx3] : proxy /icons

[nginx3] ..> [nginx1] :  proxy /
[nginx3] ..> [nginx2] :  proxy /

[app1] --> [nginx3] : upload icons
[app2] --> [nginx3] : upload icons

[benchmarker] --> [nginx1]
[benchmarker] --> [nginx2]
[benchmarker] --> [nginx3]

所感: 早期にWebDAVでファイルを集約したおかげで後半まで構成を変えずに済んだ。これによりSQLのチューニング(count(*) を潰すなど)に集中できたのはよかった。

fujiwara組

http://sfujiwara.hatenablog.com/entry/2017/10/23/123240

そろそろ複数台構成を伺いたい。となるとファイルの共有なり分散配置が必要になるので、以下のような戦略を決定 (@fujiwara)

  • アップロードされたホストが自分のホスト名を元にディレクトリを切って icons/01/xxxxxxx.png というファイル名で保存、DB にも 01/xxxxxxx.png を入れる
  • nginx(01)
    • /icons/01/ はローカルファイルを見る (そこに保存されているので必ずある)
    • /icons/02/ は nginx(02) へローカルのネットワークで proxy_pass
  • nginx(02)
    • /icons/02/ はローカルファイルを見る
    • /icons/01/ は nginx(01) へローカルのネットワークで proxy_pass
  • nginx(03)
    • /icons/01, 02 はそれぞれ nginx(01, 02) に振る
    • それ以外は app(01, 02) に均等に振る
() benchmarker

package "isu1" {
  [nginx1]
  [app1]
}

package "isu2" {
  [nginx2]
  [app2]
}

package "isu3" {
  [nginx3]
  [db3]
}

[nginx1] ..> [app1]
[nginx2] ..> [app2]

[nginx1] <..> [nginx2] : proxy /icons/{01,02}

[nginx3] ..> [nginx1] :  proxy /
[nginx3] ..> [nginx2] :  proxy /

[benchmarker] --> [nginx1]
[benchmarker] --> [nginx2]
[benchmarker] --> [nginx3]
  • PlantUML力が足りずに表現しきれない…
  • nginx.conf はrepoに入れ忘れて消滅とのこと。残念!

所感: 非常に効率よく負荷分散できていて、今回の問題にたいしてはこれがベストな構成な気がする。ISUCON3の想定解答ということなので出題者としての経験を遺憾なく発揮した形に。

MSA

http://mizkei.hatenablog.com/entry/2017/10/23/182820

ここでボトルネックがDB+appサーバーのリソースに移っていたので、appを消し去り、DBのみとして、他2台でappの処理をすべて行うようにしました。 アイコン画像の処理はメモリを使うこともあって、1台で POST /profile を受けることはせず、それぞれ保存して、nginxからtry_fileで存在しなかったら、もう片方にproxyするようにしました。 ここでスコアは46万点を超えました。

  • app servers x 2, db server x 1 という構成で、DBサーバでappは動かさない
    • 上位陣はすべてこの構成か
  • 画像は1サーバに集約せず、 nginxの try_files directive でproxyするかどうかを判断することに

  • 構成図は省略

所感: try_files はいいアイデア。ただfujiwara組のIO不要で負荷分散できる仕組みには一歩とどかなそう。

白金動物園

https://diary.sorah.jp/2017/10/23/isucon7q

  • golangによるlong polling proxyを試すも本番投入は見送り
  • 結果、構成はMSAとほぼおなじで try_files も使う
  • long pollingの /fetch だけisu3で受けるように
  • RedisをDBサーバに置いて並用する

    • ここはMSAとは違うところ
  • 構成図は省略

所感: メモリもCPUもあまり使わない /fetch だけDBサーバでも受けるのは面白い判断。

その他注意点

  • 今回はキャッシュ用KVSを用意しなかったチームが多い
    • マシンスペック的にそこまで余裕がなかったというのはある
    • 実際のプロダクションにもいえることだが、「キャッシュしない」という判断も必要
  • count(*) ... where ... はindexが効いていても遅いので改善の余地あり
    • スギャブロエックスでは message_count tableを用意して count(*) をなくした
  • nginxのリバースプロキシでUNIX domain socketを試してもよさそう

FAQ

ベストなチーム編成は?

一言で: インフラ 役1人とアプリケーションエンジニア役2人。

ISUCONはインフラ設定がわりと大事なのと、複数台の場合は分散環境の設計と実装力が問われるので、インフラスキルが絶対に必要です。そしてだいたいインフラ役が司令塔になります。

アプリケーションエンジニアは司令塔の言うことを聞いているだけかというとそんなことはなくて、クエリの改善とかアプリレベルの改善でやれることは沢山あります。また、分散環境の設計はアプリの構造がわかってないとうまくできなかったりするので、そのへんの把握はアプリエンジニアの仕事です。

ISUCON6では正規表現の最適化(トライ木の導入)などもあって、このへんはアプリエンジニアの腕の見せどころでしたね(ぼくは予選突破できませんでしたが!)。

選択する言語で有利不利はありますか?

一言で: あります。ただし言語というよりはRuby on Railsで日頃開発しているとISUCON向きのスキルが育たないというのが大きいのではないかと思ってます。

ISUCON7 オンライン予選の利用言語比率 : ISUCON公式Blog によると、言語ごとの予選通過率はこんな感じです。

  • Ruby 9% (6 / 68)
  • Go 26% (16 / 62)
  • NodeJS 22% (4 / 18)

ちなみにISUCON6でもRubyとGoはほぼ同じ傾向でした。

  • Ruby 10% (7 / 68)
  • Go 27% (11 / 41)
  • NodeJS 0% (0 /11)

母数が違うので素直に比較はできないものの、予選突破はgolangが有利にみえますね。

ただし、Ruby使いは日頃Railsで開発することが多く、生SQLに慣れてないとかSinatraでのパフォーマンスチューニングに慣れてないとか、そういうところがボトルネックになっているのではないかと思っています。

普段Rails書いているときはSQLとか使わないし…

生SQLを書く必要はかならずしもないんですが、N+1の展開の仕方くらいは知って損はないし、explainも読めるようになっておくと安心です。 rails console でSQLがログにでるので、意識をそこに向けると世界が広がります。

また、ウェブアプリで扱うSQLとはだいぶ異なるものの、ログの分析にもSQLは使えます。詳しくは 『10年戦えるデータ分析入門』 など。

まとめ

来年もがんばりましょう :muscle:

参考文献