AWSを活用して全国ネット番組(NHK)によるアクセス急増を乗り切った具体的手法


まつけんです。

前回のWBSでの紹介に引き続き、今回はNHK「サキどり」にCerevoを紹介いただける機会に恵まれました。というわけで、前回同様に急激なアクセス増が放送日に集中することが予想されるので、いくつかの負荷対策をして乗り切りました、という話の第二弾になります。
今回はある程度の対策する時間があったため、前回の構成よりも急場しのぎでない対策をせっかくなのでやりました。そういった表示の高速化も含めたいくつかのテクニックを紹介したいと思います。
ちなみに、結果だけを先に書きますと、この対策をしている状態であれば、m1.small x 2台(ELBの配下に居ます。)がカスタムオリジンとして存在しているだけで、まったく問題なく放映時のアクセスをCloudFrontが捌いてくれました。(CloudFrontのキャッシュ時間が適切であれば、オリジンの性能があまり関係ないのは当然ですが。)

今回、アクセス増に対応するべきサイトの対象を再度確認しておきます。
・コーポレートサイト
コーポレートサイトに関しては、テレビでは明確にURLが詳細に紹介されるわけではないので、Cerevo,セレボといったキーワードで検索経由でのサイトへのアクセスが増えると考えられます。そうすると、やはり矢面に立つのはコーポレートサイトです。
こちらは実は特に大きな対策は施しておらず、CloudFront + S3 originで構成されており、変なことが起こらない限り、アクセス増大に対応してくれるはずです。こちらのページは下記のような対策ができておらず、できれば近々行うつもりのリニューアル時には同様の対策を入れたいなと思っています。

・LiveShellサイト
おかげさまで先日、HDに対応したLiveShell PROを発表しまして、製品ページは2製品分に増えていますが、LiveShell Dashboardと呼ばれるコントロール部分はLiveShellシリーズ共通です。こちらは製品購入後、ユーザー登録とLiveShellの初期設定がをして始めてアクセスされるページになるので、TV放映のような場合にはこの動的な部分のアクセスはほぼ伸びないというのは前回のWBSにて分かっていたことなので、対策は主に製品ページに集中して行う形にしました。
実際には、動的な部分やリアルタイムコントロールの部分に関しても、それなりのチューニングが施されていて、同時接続数は発売当時にくらべて大分余裕をもてるようにはしています。

というわけで、前回同様、LiveShellの製品ページを中心にアクセス急増に対する対策として以下の4つを行いました。順々に紹介していきます。

CloudFront + カスタムオリジンで対応

一番大きな変化点は、前回のようにS3とEC2でなんとか乗り切るという対応から、いわゆるCDNなCloudFrontを利用する形に軸足を移しました。
いくつかの理由はありますが、そもそもLiveShellの取り扱いが米国、北欧などで伸びてきており、日本以外のアクセスが増えて生きていること、また、カスタムオリジンでnginxからコンテンツを出せるので、Expiresの設定など細かなヘッダを割と自由に制御できることなどが理由です。

まずは、CloudFrontを利用するにあたっての構成について説明をします。
前提として、製品ページは基本的に開発時は動的ページとしてmakoテンプレートで書かれたものをPythonでHTMLとして出力するというかたちになっています。これは、日本語、英語の言語ごとにページを用意するのが大変なので、翻訳キーの置換でどうにかしたいということや下記の画像ファイルのURLにはリビジョンを含めたいなどの要求を満たす手間を考えると、静的ページとして各ページを作成するのは大変というところからそうしています。
そして、実際に本番向けのサイトにデプロイをする時は、自作のスクリプト(擬似的にリクエストを生成してコンテンツを取得するような感じです。)で各言語ごとのページをHTMLファイルとして吐きだしておいた上で、サーバー上にデプロイするという形にしています。
その上で、このファイル群を静的ファイルとして提供するnginxをCloudFrontのオリジンとして設定しておく、という形になっています。(単純にManagement Consoleからカスタムオリジンに指定しているだけです。)
ツッコミどころとして、CloudFrontが前にいてアクセスを捌いてくれる現状では動的なコンテンツ生成のままでも大丈夫なんじゃないかという話がありますが、まあこれはEC2からコンテンツを配信していた名残です。あとは、気持ち的にアクセスごとにほぼ変化しないコンテンツを動的に毎回生成するのはモッタイナイというところもあります。

また、nginx上では、コンテンツに直接アクセスが合った場合は、UserAgentがCloudFront以外であれば、PathはそのままにCloudFront向けのドメインへのrewriteしています。(/だけは特殊で、nginxでブラウザの言語判定などを行って該当のコンテンツにrewriteしています。)
たとえば、以下のような設定です。

location ~ ^/pro/(.*)$ {
    charset utf-8;
    expires 600s;
    index index.html;
    alias /srv/kanda/kanda/static/misc/pro/$1;

    if ( $http_user_agent != "Amazon CloudFront" ) {
        rewrite ^(.*)$ http://static-shell.cerevo.com$1 permanent;
    }
}
# /の言語判定は長くなるので、割愛。

CloudFrontではカスタムオリジンを正しく設定すれば、同一ドメインで静的なコンテンツと動的なコンテンツを配信することは充分可能そうですが、この構成を試しはじめたときにまだ対応していなかったのと、分けておくほうが動的部分の検証もやらないとみたいな話もあるし、という理由で、CloudFrontで配信するコンテンツと動的なコンテンツのドメインは分けて構成しています。

ここからは、CloudFrontを使っても、リクエスト数がそもそも多いと、CloudFrontからオリジンへのアクセスも微量ながら増えますし、表示速度にも影響することも考えて、ページ表示時のリクエストを極力減らす施策をとっていきます。CloudFrontも従量課金なので、リクエストや転送量は少なければ少ないほど幸せという側面もモチロンあります。

JS, CSSは1つのファイルにまとめて、コンパクト化する

これは画面の表示速度にも関わるので、実際にはアクセス急増対策というよりは割と初期から行っていた部分です。
JSに関しては基本的にこのサイトではClosure Libraryが使われており、JS自体の書き方もそのお作法にしたがって書いてくれてます。そうして、本番サイトにデプロイされる時には、Google Closure Compilerでファイルを1つにまとめつつ、compilation_levelをADVANCED_OPTIMIZATIONにしても動くようにということで開発しており、実際にそういった対応してくれています。
CSSに関しても、ほぼ同様の対応で極力、1つのCSSにまとめてしまい、ファイル数を減らすという形の対応をしています。また、同時に記載された画像のURLは下記の通り、リビジョンを含んだものに置換されます。

その上で、コンパイルされたファイルをリビジョン番号を含んだURLに配置し、Expires(Cache-Controlなどのヘッダも含めてExpiresと書いています。)は1年先を設定しています。
これで、JS,CSSに関しては、こちらでページが更新されない限りキャッシュを使ってくれるので大変エコで、従量課金なCloudFront的にも良い感じになります。

画像ファイルはURLにリビジョン番号を含める形にして、Expiresを長大な形に設定

画像ファイルに関しても、JS,CSSのように、できるならExpiresは長大な期間を設定したいところです。とはいえ、JS,CSSのようなコンパイルというような作業を経るわけでもない、というところでどうやってURLにリビジョンを含めるのが管理上でも楽かという観点で以下のような対応としています。
すこしやっつけ感漂うトリックですが、静的ファイルを生成するタイミングで、/static/imgとなっているURLを/static/dst/img/{latest_revision}/に無理矢理置換します。(latest_revisionは実際にはhgのリビジョン番号です。)

しかし、そんなファイルは当然存在しないので、このままでは404になりますが、そこはnginx側でよしなに

location ~ /static/dst/img/\d+/(.*)$ {
    expires 360d;
    alias /srv/kanda/kanda/static/img/$1;
}

という形にしてaliasを張ることで、実体としては、/static/imgにあるファイルを返すようにしておきます。
画像ファイルもMercurial上で管理されており、変更があればリビジョンが上がります。そのため、こうしておけば変更時だけURLが代わり、それ以外ではExpiresに従ってキャッシュを利用してくれるという動作をします。
残念なポイントは、更新時に変更のないファイルまで再度読み込まれるところですが、これはそもそもURL生成をテンプレート内でのURL生成に変えるつもりなので、そのタイミングで、個別ファイルごとにリビジョンを埋め込む形に変えれればいいかなと思っています。
それ以外にも、CSSにもbackgroundなどで画像ファイルが指定されている場合があります、これも上記にも書きましたが、URLを書き換えるためにコンパイル時にCSSを一度読み込んでURLを見つけて置換する形で対応しています。

その他

最後、細かい話をいくつか。
これはアクセス急増に対応するため、というよりは、表示高速化の側面がおおきいですが、facebookのいいねボタン、Twitterのツイートボタンなどの遅延してロードするようにしてもらっています。これらのボタン、PageSpeedなどでみると明らかに遅くて最後にロードしないとストレスが溜まります。
あとは、画像の最適化です。pngファイルは圧縮率を最大にしたり、ということをするだけで結構サイズが変わってきたりします。お手軽な方法としては、PageSpeedをいれると最適化後のファイルをダウンロードすることができるので、とりあえず、PageSpeedで評価したあとに、最適化された画像に入れ替えるというのをやるだけで、ロード時の容量が少しは節約できます。
最後に、もっとも基本かもというところですが、そもそもコンテンツは基本的にgzipで圧縮されたものを提供できるようにすべきです。我々は基本的にnginxのgzip_staticを利用して、事前にgzip圧縮したファイルを.gzを付けたファイルとして用意をしておくことで対応しています。

最後に

以上のような割と恒久的な対策を施したおかげで、TV放映時のアクセスは難なく乗り越えることができました。
この構成になっていると一番良いのは事前にこれといって準備が必要ではないことです。数ドル単位で費用を節約するような運用を心がけている弱小サービスとしては、常に高性能なインスタンスを立ち上げて置くなどをしなくてよく、静的なページをまるっとCloudFrontさん任せたということで、基本的には突発的なアクセス増などにも特に作業が必要になりません。これは運用している自分にとってはありがたいことで、大抵アクセス急増のタイミングというのは、開発以外にもいろいろやることがあって、気にせずになんとかしてくれるならそれに超したことはありません。
今後また放映があったりしても、心配せず寝ていられる程度には安心できる構成にやっと辿り着くことができたかな、と思っています。

おまけ

ちなみに、このブログもWordpress(+Really Static) + CloudFrontを使う形で動作しています。Wordpress自体のチューニングは、私のPHP苦手意識によりできそうになかったので、静的ファイルを生成して、CloudFrontから配信するという形で動かしています。割と簡単にできるので、また機会があれば紹介したいと思っています。