RailsにCloudFront噛ませたら10倍速くなった。RailsのエッジサーバとしてのCloudFrontの有用性について

RailsのエッジサーバとしてCloudFront噛ませたら10倍以上速くなりました。

# 構成

example.com(Route53) -> CloudFront -> Rails App server

単にappのdomainからcloudfrontを見るように設定し、Rails App Serverをオリジンとして設定。
今回は適当運用なのでLBすら置いてません。
しかし、後述するように冗長性は（比較的）保たれています。

# CloudFrontのいいところ

- route別でオリジン分けたり出来る
- **route別でキャッシュポリシー変えたり出来る**
- **POSTのパススルーが出来る（！）**
- めっちゃエッジサーバ多い（Brazilとかでも速い）
- オリジンのポート指定が自由（！）
- ログを吐ける
- 総括してわりと細かく設定を弄れる（Header関連以外）
- **Route53でAliasが使えるので、ルートドメイン対応している（神）**

最後のがないと今回の用途として使えません。

# 速度と、有用性

めっちゃ速くなります。

まず当たり前ですが、キャッシュ有効ならRailsまで届かないので速い。（x10オーダ）
そして、サーバの物理的な近さ。モバイルはRTTがモノを言うので。(x2-x10オーダ)
二つがあわさり、日本のPCからのApacheBenchが20-80msecくらいのオーダになりました。Appサーバ直は800msec-2sくらい。
大体10-40倍は速くなります。
負荷をかけるともっと変わります。（CloudFrontはいくら-c増やしてもほぼ一定のレスポンス）

重要なのは、他の国でアクセスしても同じくらいの数字が出せることです。
これはCDN噛ませないと達成不可能です。
今は日本にサーバを置いているので、アメリカからのアクセスだと数100msecは遅くなります。

ところでAppサーバが割と遅い件について。
ぶっちゃけ今回はAppサーバが日本なのにDBがシドニーにあるのが遅い原因です…
本気でやるならDBをレプリしてAppサーバに直接ぶちこむとか、むしろインメモリに全部おくとか。
非常に面倒くさい。
まあ日本に置けよって話なのですが。

言いたいのは、こんな適当な構成でも世界中で速いサービスが作れます。

海外のサービスが遅い…と感じることがあると思いますが、単純に遠いからです。
自前でエッジサーバをたくさん用意するのは大変なので、CloudFrontをエッジとして使うのは有用でした。

副次的な効果としては、Railsまで届かないので負荷をあまり気にしなくてもいい。
Railsキャッシュ使ってても重いものは重いです。
Page Cacheも実はCloudFront使うより運用が面倒。

一時的にオリジン死んでもCloudFrontさんが代わりに働いてくれます。
なので多少適当な運用しても冗長性を保てる。

ついでに、オリジンがCloudFrontからのみ接続を受けるので、セキュリティを考える上でも面倒が少なくなる。
ポートも自由なので奇想天外なポートでrequestを受けたり出来る。

# ところで Rails + CloudFront ってどうよ

ところでこの構成はRailsがわりと静的なページを吐くこと前提です。（コンテンツ系とか）
コミュニケーションのあるサービスだと工夫が必要で面倒くさい。

ただ、動的な内容と、インタラクションはJSで付けられるので、インターフェースとAPIの分離をはっきりと出来ればどんなサービスでもこんな構成は使えるかも、と思っています。
極論すればSPAのガワなんかはCloudFrontで配信出来るので。（そうすべきだし）

根底には、インターフェースとデータを分ける、という流れがあるでしょう。
インターフェースだけ先に読んで表示すれば速い（ように見えるから）です。

TwitterとかGmailなんかはそもそもガワをAppCacheでクライアントキャッシュしてしまいます。
極論、AppCacheが最速なのでベストを尽くすならSPA + AppCacheをやるべきでしょう。（TurbolinksでもOK）
アプリみたいに、一旦ダウンロードさえすればあとはAPI経由でデータを取る、というのが理想です。帯域も少なくて済むし。

というわけで、ベストをめざすならAppCacheが出来るインターフェースを書きましょう。（AppCache自体けっこうBuggyですが…）

# コンテンツ系をRailsで書くのって…

静的HTML出力とかやるよりこの構成が普通に良かったです。理由は

- まず、Railsで書ける（重要）
- Page Cacheがあるけど、Cacheの管理が面倒
- わりと動的なページも運用出来る（重要）
  1. TTL短くするとか
  2. Route分けてキャッシュしない設定にするとか
  3. JSで遅延ロード使うと、動的な内容も埋め込めます（Route別にBehaviorが作れるのでCORS考えなくていい）
- 面倒くさいこと考えずにRailsで書いてスピードが出る
- MiddlemanとかJekyllとかデータの取り扱いが面倒
- Railsのキャッシュ機能は細かく考えていくと面倒くさい

Route別のTTL設定がスバラシイ
-

前述した通りCloudFrontはRoute別でキャッシュTTL変えたり出来るので、例えばTopは更新頻度に応じた時間でexpireさせ、/assets下は1years、/articles下は1daysとか出来たりします
/articlesは追記とかしなければ、1yearでもいい。または、1yearとかにして、追記したらCloudFrontのAPIでpurgeさせてもいい。
大変柔軟にキャッシュ出来ます（Cache-Headerは変えてくれないけど！！！）

# 結論

- RailsにCloudFront噛ませると10倍以上速くなる
- Route53のAnycast使えばお手軽にルートドメインでmulti languageが出来る（かも）
- 負荷が小さいので運用が楽
- CloudFrontが生きてればサーバが一時的に死んでてもサービスが動くので、胃に良い。
- 速度のために国毎にAppとEdgeサーバ置くとか考えたくないので、今後は出来るだけCloudFrontを噛ませたい。

ほぼ自分用なので、つらつらと書きましたがこんな感じ。

# ところで CloudFrontのダメなところ(機能リクエストw)

SPDY対応してない
--
対応してるのが珍しいレベルだけど、SPDYはもうユーザベースで60%+対応してるので。
もうそろそろ諸CDNにも対応してほしい…
MaxCDNはエッジサーバの数と機能が貧弱なので。（アジア系が弱すぎる）

Cache-Control Headerを付加出来ない
--
Object Cachingという設定フィールドがあり、これかと思いきや違います。
Cloudfront側でキャッシュするTTLしか変えてくれません。
Cache-Control headerを改変する方法はありません。

Gzip、明示しなければしてくれない
--
オリジンサーバのHeaderでGzipオンを明示的に指定しないとgzipしてくれません。
いちいちS3でメタデータ弄るの面倒くさい。

適切なContent-typeを付加してくれない
--
S3は普通にアップロードすると、Content-typeがapplication/octet-streamになります。
S3はメタデータを弄るのが面倒…1万個くらいファイルがあると、スクリプトでも終わりません。
これはブラウザのパフォーマンスを悪化させるのでなんとかしたいが、Cloudfrontはcontent-typeそのままパススルーしか出来ません

結局Varnish噛ませてやりました
むしろS3の欠陥かもしれない

Minifyしてくれない
-
これを求めるのは酷だけど、cloudflareとかしれくれるので。
Railsで出力したHTMLをminifyとかしてくれると嬉しい。（こっちでRackを噛ませれば済む話なのだが…）

意外と高い
--
転送料が凄いことになってくると意外と高い。
Asiaだと10TBまで20円/GBくらいする
もっと安くて速いところは探せばあります。

Staleできない
--
もし万が一オリジンが死んだとき、（TTL終わってても）代わりに最後のキャッシュを参照する機能。
CloudFrontでは出来ません。

オリジンが死んだときちょうどexpireになったコンテンツは503が返ります…
Stale出来るCDNと違って、死活管理は重要（どちらにせよ重要ですが）
長めのTTLを設定しておきましょう。

rare caseです。

LocationでHeader付けたりしてくれない
--
i18nをやるときにこの機能があると便利。
まあ、最初にアクセスした国でキャッシュしてしまうので意味ないですが。
ただ、Route53のAnycast（場所ベースでDNS応答を変えられる）っぽい機能で国別にCloudFront作って…というときに不便です。
（言語でCloudFront分けないとi18n不可能）

と思ったけど、単にjp.example.comとかをOriginにしたらいいコトに気付いた。
でRails側でサブドメインを見てlocaleを設定。
GeoIP不要。

US -> example.com(Route53) <-> usrailsapp.cloudfront.net -> us.example.com(Origin)
JP -> example.com(Route53) <-> japanrailsapp.cloudfront.net -> jp.example.com(Origin)
こんな感じ。

ただしAccept-Languageを見てのi18nは無理。
JSベースで見てリダイレクトとか、移動を促すくらいしか出来ない。
Accept-Language毎にBehaviorを変える機能があると出来るけど、まあ付かないかな…

Route53と合わせてお手軽にマルチ言語サービスが作れるっぽい、ってことで。（これについても書くかも）
ただ、最近は同じドメインではなく、言語見てサブドメインにリダイレクト、って流れなので、このやり方はよくないかも。
Googlebot様がどう認識してくれるか、の問題。（調べなきゃ）
**FacebookとかGoogleとかYoutubeとかはどんな国でも同じドメインなのでGooglebotは問題なさそう**

Purgeが遅いし高い
--
この規模でエッジ持ってると仕方ないですが…
そもそもPurgeを前提とした運用をしなければいいので、問題有りません。

#追記

CloudFlareは転送料課金ないしSPDY対応してるので神