ISUCON6本選の出題を担当しました@edvakfです。

既に1週間以上経過してしまいましたが、ISUCON6主催者および参加者の皆さま、ご協力ありがとうございました。この素晴らしいイベントが今年も開催されたのは関わってくださった全員のおかげです。

特に名誉運営として予選終了後に参加していただいたmatsuuさんには大変なご尽力をいただき、感謝の言葉を尽くしても足りません。

さて、既にTwitterでは周知していますが、ISUCON6本選問題のリポジトリを公開しています。
https://github.com/isucon/isucon6-final

合わせて、ISUCON6裏話Nightの開催も決定していますので、もっと余韻を楽しみたいという方はご参加下さい。
http://connpass.com/event/43742/
※こちらのイベントはAmazonウィッシュリストからプレゼントを送られた方優先でご参加いただけます

講評

今回一番意識したのは、「2016年のISUCONとはこうあるべき」というところです。

ISUCONを愛する者として、このイベントには1年でも長く続いてほしいという思いがあったのですが、毎年やっていく中で「ISUCONの問題パターン」が出来上がってしまっていたような気がしまして、そうなるとこのイベントも先細るだけという危惧を抱いていました。

ISUCONはまだ、たかだか5回しかやっていないだけの技術イベントだ。守らないといけないものはないし、これだけ出題というものが大変なんだから、出題者はなんでもやりたいことをやればいいと思う。参加者は不満があれば、自分で勝手にイベントをforkして出題をしてみればいい。
http://tagomoris.hatenablog.com/entry/2015/10/31/180114

上のような意図を汲み、これまでのISUCONをちゃんと2016年のやり方で壊していこうとして作ったことで、以下のような技術的要素を盛り込んだ問題になりました。

  • NodeJS+Reactのサーバーサイドレンダリング
  • アプリケーションはJSON APIしか返さない
  • Docker
  • HTTPS
  • C10K


  • 見ての通り、フロントエンドからインフラまで、幅広い知識を総動員して挑む必要があったかと思います。

    出題側として想定していた問題のポイントをかいつまんで説明していきます。

    (出題チームで実際に問題を最後まで解ききったわけではないので、他のところがボトルネックになるかもしれません。あくまで問題を作っていた時に考えていたことと捉えて下さい。)

    Docker

    本番環境で動いているアプリケーションを手元に持ってきて開発するのには非常に便利なのですが、ネットワークがNATなので、今回のようなTCPコネクションを捌き切る問題ではDockerを外したほうが良いという想定でした。

    Dockerに慣れている人と慣れていない人がいたかと思いますが、Dockerfileにセットアップ方法が書いてあるので、Dockerを外して普段のISUCONどおりにホストマシンにセットアップすればDockerでハマって時間を潰すことはしなくて良かったはずです。

    MySQL

    MySQL部分に関しては普段通りのISUCONと変わらなかったはずです。

    トップページでは各roomにstroke_countをつけるためにN+1でCOUNTクエリを投げていましたが、roomsテーブルにstroke_countカラムを付けるなどで対策できたはずです。

    またトップページでは、strokeの最新の順番にroomを並べるということをしていましたが、roomsテーブルに最新のstrokeのcreated_atを持たせる非正規化をしないと厳しいはずです。実はこれは自分の最近の仕事で悩んでいる問題です…現実の問題においてはデータを削除したり非公開にすることもありますし、予約公開のようなことがあるので、問題はずっと複雑です。

    pointsテーブルがかなり巨大ということがありましたが、はてなさんとの予行演習ではpointsテーブルを廃止してstrokesテーブルにテキストカラムでpointsを持たせると速くなるという予想外の最適化が見られました。

    トップページやroomのページにリクエストされるたびにCSRFトークンを挿入していましたが、現実のウェブアプリケーションではこういうことはしませんし、キャッシュストアに逃したほうが良いです。あるいはcookieにしてしまってそもそもサーバー内に保存しないという手もあります。

    ちなみにMySQLの
    DATETIME(6)
    を使ったのは「2016年にもなって(略」というあたりを意識してのことです。NodeJSなど、マイクロ秒ではなくミリ秒までの精度しか持てない言語もありましたが、秒以下の精度を扱ってくれれば成功にしていました。

    React

    サーバーサイドレンダリングはめちゃくちゃCPUを使います。

    今回のベンチマーカーはほとんどPOSTして来ませんのでキャッシュが有効な手段なのですが、単にHTMLをキャッシュしただけではダメで、CSRFトークンを毎回変えなければいけません。

    今回のベンチマーカーは60秒で最大200回ぐらいしかPOSTしません(スループットが出ないと10回程度しかPOSTしません)ので、CSRFトークン以外の部分は生成済みHTMLをキャッシュしてしまうのが有効でしょう。

    ただし、「キャッシュするならPOST時に削除必須」というスタンスで作りました。FastlyのようなキャッシュのインスタントパージがあるCDNが出てきて、キャッシュのインスタントパージができることを前提にウェブアプリケーションを設計することが増えてきているためです。

    SVGに関してはReactを使っているとはいえ、
    renderToStaticMarkup
    による出力なので、NodeJSを剥がして単純な文字列操作で出力するのは簡単でした。

    SVG以外のHTMLについては難しいです。(トップページに関しては)
    data-react-id
    data-react-checksum
    をきちんとつけなければ失格にしていました。これについては以下のような方針を考えていました。

    NodeJSオンリーにする

    バックエンドのAPIとしてNodeJSの実装も用意していましたので、そちらとReactのフロントエンドをマージして、キャッシュの管理を柔軟にやる方針です。

    今回はNodeJSで予選を通過したチームが無かったのですが、予選ではisudaとisutarをマージすることが有効な問題だったので、「無駄なマイクロサービス化はやめるべき」という教訓をもって本選ではNodeJSを選択するという臨機応変さを見せるチームがあるかもと期待していました。

    APIからReactを呼び出すようにする

    キャッシュの管理を含めて主要な部分をすべて慣れている言語で行い、React部分はレンダリングのためだけに使う方針です。

    POST時にHTMLをレンダリングして保存しておけば、GET時はNodeJSは不要になります。予選問題のisupamぐらいに「気にする必要のないマイクロサービス」にしてしまうことができたはずです。

    この変更は
    server.jsx
    をちょっと書き換えるだけで可能です。現代のウェブ開発者はこのぐらいのJavaScriptは書けて当然、というスタンスでした。

    APIでレンダリングする

    ISUCONでしか通用しない最適化ですが、Reactの生成するHTMLを文字列操作だけで作るのも可能です。とは言え粗悪な実装を許したくなかったので、ベンチマーカーでは
    data-react-checksum
    を計算するアルゴリズムをReactのソースコードから移植していました。

    (この講評を書いている今になって、roomのページでは「現在入室している人数」をHTMLに表示していたので、素直にキャッシュするのが難しいことがわかりました。roomのページでは実はベンチマーカーが
    data-react-checksum
    を見ていなかったり、実はHTML表示時の「現在入室している人数」も見ていなかったので、キャッシュで凌ぐこともできたはずですが…これについては出題時の考慮漏れでした)

    Server-Sent Events (SSE)

    SSE部分に関するレギュレーションは以下のようなものでした。

  • /api/streams以下はServer-Sent Eventsの仕様に従っていれば挙動を変えてもかまわない
  • POSTしたstrokeが2秒以内にstreamされたら1点


  • 最終的に10万コネクションを捌けば、1回のstrokeで10万点入ることを想定していました。

    SSEの仕様ではコネクションが切れたら自動的にクライアントが再リクエストすることを期待します。

    初期実装では1回あたり3秒でコネクションを切っていましたが、1コネクションが1プロセスを専有するようなフレームワークでは、まったくループせずに一瞬でレスポンスを返してしまう方法もありました。初期実装の
    retry:500
    retry:1500
    のようにすることで再リクエストの頻度を抑えることもできました。また、HTTP/2を使えば再リクエストのたびにTCPコネクションを確立しなくても良かったはずです。

    逆にNodeJSやGoやScalaではむしろもっと長く接続を貼ったままにして、イベントをブロードキャストするということもできました。

    RedisのPub/Subを使ったチームがいくつかありましたが、サーバー内のローカルポート枯渇やプロセスあたりが開けるファイル数(TCPソケット数)も問題の焦点になれば良いと考えていました。

    TLS

    以上をやると最終的にTLS終端のCPU負荷が問題になるはずです。ベンチマーカーがアクセスするIPアドレスが一つだけなので、TLS終端を5台に分散させるには、NATを使うなどの構成を考える必要があります。

    このあたりは最適解が見えないのですが、ベンチマーカーではnginxのstreamでTCPのフォワードプロキシを実現することで最終的に65535コネクション以上を開くこともできるようになっていました。

    おわりに

    本選中に痛感したことなのですが、出題者としての「色々盛り込みたい欲」や「複数チームが完全解答する恐怖」と8時間で成果の出せる難易度のバランスを取るのが非常に難しかったです。

    その中でもダントツでトップを独走していた「この技術部には問題がある!」や、最後の最後でボトルネックを複数台にスケールすることでスコアを伸ばした「morimoto組」の実力には感服しました。

    初めてのISUCON本選出題という大役で、至らない点も多々あったかと思いますが、貴重な経験をさせていただくことができました。改めて関係者の皆さまに感謝を申し上げます。