R-ISUCONを開催しました~問題公開編~

R-ISUCONを開催しました~問題公開編~

Recruit 全社対抗のISUCON、R-ISUCON (りすこん) を開催しました。 @yosuke_furukawa は主に問題作成を担当、 @orisano がベンチマークを担当し、インフラは @int-tt が担当しました。ヘルプとして Java の参考実装は @mashroomhama が担当しました。

詳しくは前回の記事を参照して下さい。

https://recruit-tech.co.jp/blog/2018/04/27/r_isucon_2018_spring/

問題の概要

いわゆるリクルートの社内システムでおなじみの「会議室予約システム」を題材にしました。
リクルートでは3ヶ月ごとに会議室を予約する仕組みをもっています。3ヶ月ごとの会議予約開始日の朝には全てのリクルートグループ各社からアクセスが頻繁に行われ、その結果会議室予約システムが遅くなったり、500エラーで落ちたりするシーンを何度か目撃しています。

これは問題としてもリクルート全体でやるのに適していると思い、R-ISUCONの問題にすることにしました。

r-isucon 会議室予約システム

r-isucon 会議室予約システム

挑戦したい方は以下のリポジトリから実施可能です。

https://github.com/recruit-tech/r-isucon

細かい不明な点があればissueで回答します。

問題に挑戦される方は以下の記事の内容はまだ見ないほうが賢明です。

ISUCON会議室予約システムの設定

とにかく何も知らないソフトウェア開発駆け出しの人が全くパフォーマンスのことを考えずにナイーブに実装した事を想定しました。

  • JOINを知らない
  • セッションの保持するべき場所を知らない
  • 画像変換はImagemagickのコマンドしか知らない
  • 行ロックもテーブルロックも知らない
  • そもそもインデックスも知らない
  • CSS, JSを圧縮する・gzipするといったコード圧縮を知らない
  • 画像フォーマットについて知らない

何もせずに起動すると大体100点とか200点程度の点数しか出ません。上記のポイントを抑えて改善を加えていきます。

CSSがおもすぎる

レギュレーションを見ると、いつものISUCONにありがちな 「与えられたアプリケーションから、JS/CSSなどのアセットファイルを変更してはいけません」の文字がありません。

そのかわりに「運営がブラウザで視認できる範囲での静的ファイルの変更」を許可するという記載があります。これは意図的にCSSやJSの変更をするように示唆しています。今回のISUCONでは、フロントエンドのチューニングであるところのJS/CSSといった変更を認めています。

そのため意図的にcssの中にまずは嫌がらせのように長文のコメントが書いてあります。まずはこのコメントを消してcssを小さくするところからスタート。

コメントを消すだけでも高速になります(13MBもコメントだけであるので)。

で、もっと言えばココで止まらずにCSSをminifyして空白を除去したり、使ってないCSSをlighthouse等で見つけて最小限の構成に変更させるのも良いでしょう。明らかに遅いのはコメントの部分ですが、CSSをminifyする、それだけではなく使っていないCSSの指定を少なするという所で磨き込めると後々効いてきます。

ちなみに JavaScript もロードしてますが、linkタグでロードしていて、よくよく見ると使ってません。なので消しても問題ないです。

ログインセッション

初期実装ではDBでセッションを保持しています。テーブルでセッションを持つのはそこまで悪いことではありませんが、そもそも初期実装はテーブルにindexを振っていません。セッションは毎回アクセスされるたびにセッションの延長を行うため、DBに対してUPDATEが頻繁に行われます。indexが振られていない状態でUPDATEが行われると、更新時のテーブルロックが頻発され、ここで詰まります。

想定ではここにindexを振るか、セッションに向く Redis や Memcached などのデータストアを使うことを想定回答にしています。また、このR-ISUCONでは1台しかサーバを持っていないため、インメモリにセッションを持つのも有りです(実運用では障害発生時にサーバがダウンするとセッションが消えてしまうのであまりやりませんが)。

トップページへのリクエストが重い

トップページはベンチマーカーからのリクエストも多いです。また、日時を指定してその日の予約状況を見ることも可能ですが、初期実装では日付に応じて予約状況一覧を取得するという実装に大きな問題を抱えています。

初期実装では以下のようなフローで予約状況一覧を取得します。

  1. ユーザーの情報から所属組織を一覧を作る。 (ユーザー情報から組織一覧に対してSELECT文を発行する)
  2. 所属組織一覧から予約可能な会議室を一覧。 (組織情報から会議室一覧に対してSELECT文を発行する)
  3. 予約可能な会議室と予約情報から予約状況テーブルを構築します。 (会議室一覧と日付から予約一覧に対してSELECT文を発行する)

ひとつひとつの処理に対してSELECT文を発行しており、典型的な N+1 Query になっています。
しかも初期実装ではIndexも貼られていないため、全く効率的ではありません。

ここを修正して、1回で取得するようにJOINを使う、Indexを使って効率化する、といった処理が求められます。以下のような要領でJOINしていくと1回のSQLで取得可能です。

ここをどこまで実施できるかが今回のR-ISUCONの鍵ですね。ほとんどJOINを知らない、という想定で作ってしまったので、かなり大量のN+1 Queryが他の箇所にも存在します。

画像変換

初期実装では Imagemagick の外部コマンド呼び出しで毎回リサイズしています。ここでも2つ問題を抱えています。

  • リサイズは25×25, 50×50, 300×300 の決められたサイズでしかリクエストされないため、毎回リサイズする必要はありません。
  • 外部コマンド呼び出しはプロセスを新しく作るため、効率的ではありません

ここは一つ事前に画像を作っておき、cssやjsと同じ静的ファイルとして配信するほうが高速です。

ついでに画像が jpg や png になっているので画像フォーマットを webp の非可逆圧縮にするとさらに高速化を見込めると思います。

フォーマットや画像品質に関しては、最後に運営のブラウザ(Chrome)で確認する際に視認できる事、という条件を記述しているため、実施するのは問題ありません。ここで視認できる限界まで品質を落とすのもありですが、さすがに複数の運営メンバーで視認不可能の場合は落としています。

最初に存在している画像であれば事前に作成可能ですが、ユーザからの新規画像アップロードもあるため、それは別途変換をかける必要があります。このときも ImageMagick の外部コマンド呼び出しで画像変換するよりは ImageMagick の native モジュールのが高速です。

HTTPヘッダ

デフォルトではレスポンス時にCache-Control用のHTTPヘッダが付いていません。ベンチマーカはCache-Controlヘッダを理解してレスポンスをキャッシュするため、キャッシュされていると、リクエストしません。

リクエストしないため、画像やjs/cssといった静的ファイルはサーバから一度だけ返せばそれ以降はリクエストせずとも点数を稼ぐことが可能です。

Cache-Control
HTTP キャッシュについての解説(Google Web Fundamentalsより)

nginx から Cache-Control ヘッダをつけ、さらに言えば、JSやCSSなどのテキストベースのファイルはgzip圧縮をかけておけば(Content-Encoding: gzip)、 nginx からの配信サイズも低くなります。

また負荷が強くなると今度はこのgzip圧縮の処理がnginxに集中するため、事前に圧縮をかけておいた上でそれを配信する (nginxでは gzip_static on にしておく)と、より負荷が軽減されます。

また、カリカリにチューニングするなら、 brotli などの新しい圧縮形式を対応するのも有りです。

(HTTP/2 *効果は不明)

これらの対応の他にHTTP2の対応をすればブラウザ上は高速になると思います(小さい画像のリクエストが多いので)。

しかしながら、ベンチマーカはブラウザとは異なりHTTP/1.1でもリクエスト数の制限を受けないため、Head of Line ブロックの影響を受けにくいです。一方で、HTTP2にしたらスコアが上がったという報告もありました。

運営としては意図していないですが、もしかしたらHTTP2にしたことで、接続の数が減り、ヘッダが圧縮され、サーバへの負荷が多少減った可能性はあります。

まとめ

R-ISUCONの問題作者として問題の解説をしました。主に下記の対応をすれば、大体200万点以上のスコアが出せます。

  • CSSを軽くする
  • ログインセッションを見直し、Redisなどの軽量セッションにする
  • トップページのN+1クエリ見直し
  • 画像変換を事前に行う、ImageMagickの外部コマンド呼び出しを避ける
  • HTTP ヘッダを適切に付ける(Cache Control)、またgzip, brotli圧縮をする
  • (HTTP/2)

またこれらの他にも高速にする方法はあると思うので是非トライしてみてください。ある程度現実的な範囲でのチューニングでしか我々も試していないので、もしもなにか新しい発見があれば教えてください。

また、試したい場合は以下のリポジトリをご一読ください。

R-ISUCON Github リポジトリ

よろしくおねがいします。