POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Kurt Mackey

本記事は、原著者の許諾のもとに翻訳・掲載しております。

「CDN」(content delivery network)という言葉からは、Googleのような大企業がいくつもの巨大なハードウェアを管理し、1秒当たり何百ギガビットものデータを処理する様子が想像されます。しかし、CDNは単なるWebアプリケーションです。私たちのイメージとは違いますが、それが事実です。8年前に買ったノートパソコンを使って、コーヒーショップの席に座りながらでも、きちんと機能するCDNを構築できます。この記事では、これから5時間でCDNを開発しようとするときに、直面するかもしれないことを紹介します。

まずはCDNの機能を明らかにしておきましょう。CDNはセントラルリポジトリ(通称:オリジン)からファイルを吸い上げ、ユーザーに近い場所でコピーを保存します。初期のオリジンはCDNのFTPサーバーでした。現在、オリジンは単なるWebアプリとなり、CDNはプロキシサーバーとして機能しています。つまり、私たちが構築しようとしているCDNとは分散キャッシングプロキシです。

プロキシによるキャッシュ

HTTPが定義するキャッシュ機能は非常に細かく複雑で、取っつき難いものです。ですから、一からから構築したい欲求は抑えて、他の人が作ってくれたものを利用しましょう。

CDNにはいくつかの選択肢があります。Varnish(スクリプト、Edge Side Includes、開発者ブログあり)、Apache Traffic Server(導入すれば、今年新たにATSを採用する唯一のチームとなるでしょう)、NGINX(当社も既に利用しています)のどれを利用しても構いません。しかし、これだけは断言できますが、どれを選んでも使いこなすのは大変です。全部試してみて、一番使いやすいものを選びましょう。

(NetlifyはATSで構築されています。CloudflareはNGINX、FastlyはVarnishを利用しています。)

これから構築するCDNは、基本的な水準には達していません。しかし、全く使えないものでもありません。やるべきことは、昔ながらのRailsで開発し、複数の都市で実行するだけです。もしオーストラリアのユーザーをシドニーのサーバーに、チリのユーザーをサンティアゴのサーバーに誘導できれば、それをCDNと呼んで差し支えないでしょう。

トラフィックのルーティング

ユーザーを最寄りのサーバーにルーティングするという問題に対しては、基本的に3つの選択肢があります。

  1. エニーキャスト:ルーティング可能なアドレスのブロックを取得し、BGP4を通じて複数箇所に対して転送します。あとはTwitterで「コミュニティ」や「ルートリフレクタ」について詳しいふりをしていましょう。代わりにインターネットがルーティングを済ませてくれます。この方法のデメリットは、難易度が高く、インターネットに任せてもうまくいかない場合があることです。メリットは、知ったかぶりできることくらいでしょうか。
  2. DNS:特殊なDNSサーバーを稼働し、IPジオロケーションに基づく特定のサーバーのアドレスを返します。デメリットは、インターネットにおいて、位置を特定できるDNSソースアドレスの利用が減っていることです。メリットとしては、助けを借りずにどこにでも導入できます。
  3. ゲームのサーバーと同様の方式:複数のサーバーにpingを送り、ベストなサーバーを利用します。デメリットとしてはクライアントが必要になります。しかし、あなたはクライアントを持っていないので、ここでは関係ありません。

通常、この3つの選択肢のうち(1)と(2)をそれぞれ少しずつ利用することになるでしょう。DNSのロードバランシングは非常にシンプルです。自力で構築する必要はなく、DNSimpleなどの企業のサービスを利用してDNSをホストし、アドレスを返すための規則を定義するだけで済みます。

エニーキャストは難易度が高いです。説明すべきことがたくさんありますが、この記事ではこれ以上踏み込みません。その代わり、当社のサービスを利用すれば、エニーキャストアドレス付きのアプリを2分で実装できます。当社の宣伝のようになってしまいましたが、事実でもあります。

CDNの開発に戻ります。NGINXをそれぞれの都市群に設定し、トラフィックのルーティングのためにDNSかエニーキャストを実行したら、もう90%完了です。しかし、残りの10%に数カ月かかります。

インターネットの障害

深海の底に敷き詰められた海底ケーブルは、常に近くを航行する船に切断される危険にさらされています。とはいえ、陸が海に比べて安全というわけではありません。昔のネットワーク技術者は、「ショベルでうんと深く掘ると、バックボーン(回線)がダメになる」とよく口ずさんでいたものです。回線が切断される懸念は、サーバー1台を1カ所で稼働させるだけならあまり気にならないでしょう。2台以上稼働すると徐々に気になってきます。世界中でサーバーを稼働するとなると心配で夜も眠れません。

しかし、幸いにも、1つのNGINXを複数の都市で稼働すれば、大きな冗長性をすぐに確保できます。サーバーの1つが何らかの理由でダウンしたとしても、多くのサーバーにトラフィックを送信可能です。1台のサーバーがオフラインになっても、残りのサーバーは引き続きほとんどのユーザーにサービスを提供できます。

これを実現するための作業は、退屈ですが簡単です。まずはヘルスチェックを行います(ちなみに、CDNのリージョンがダウンすると通常は通信速度が遅くなります。そのため、ヘルスチェックで検知できるはずです)。すると、NGINXのサーバーがダウンしたタイミングが分かります。それに応じて、DNSを変更するスクリプトを実行したり、BGPのルートを取り消したり(場合によっては単にそのリージョンのBGP4サービスを停止したり)します。

これはサーバーの障害なので発見するのは簡単ですが、インターネットの障害は発見しづらくなります。複数のロケーションから外部のヘルスチェックを実施しなければならないからです。しかし、複数の視点を持つ、基本的なモニタリングは難しくありません。当社ではDatadogupdown.ioを利用しており、独自の社内サービスも開発中です。必要な情報は、cURLで分かる情報と大差ないはずです。繰り返しになりますが、CDNに関して特に注意すべきことは、リージョンの通信速度が遅くなることであって、インターネットが完全に遮断されることではありません。

ちなみに、こうしたモニタリングのオプションは全て、他のデータセンターから自分のデータセンターへの通信をモニタリングするものです。データセンター間のトラフィックは、出発点としては悪くありませんし、通信量も膨大です。しかし、ユーザーはデータセンターに(おそらく)住んではいないため、代表的なトラフィックとは言えません。あなたのサイトが非常に人気なら、実際のクライアントの状況がよく分かるようなモニタリングが望ましいでしょう。そうしたニーズに応えて、何百もの企業が、通常は密かに埋め込まれたJavaScriptのバグという形態でRUM(リアルユーザーモニタリング)サービスを販売しています。私が好きなRUMはお酒のほうのラムですけどね。これをたくさん飲みながら、Honeycomb.ioを利用して自分でモニタリング用のコードを追加します。

インターネットのくだらない問題には本当にうんざりします。しかし、幸いにも、多くの人がインターネットを利用する中で解決策を編み出しているため、この問題についてそれほど語る必要はありません。キャッシュの仕組みはもっと面白いものです。以下では「玉ねぎ(※後述参照)」についてお話ししましょう。

黄金のキャッシュヒット率

キャッシュの効率性の指標を「キャッシュ率」といいます。キャッシュ率は、オリジンからデータを読み込む場合に対して、キャッシュからデータを読み込む場合の割合を測定したものです。

キャッシュ率80%とは、「リクエストを受信したとき、80%はキャッシュからデータを提供できるが、残りの20%はリクエストをオリジンに転送しなければならない」という意味です。CDNのようなサービスを構築する場合、キャッシュ率が高いほうが良いです。

前述のGitHubリポジトリのリンクにアクセスすると、当社の素朴なNGINX設定ファイルで、サーバーの設定が分離された単独サーバーとなっていることに気づくかもしれません。これを20カ所で導入すると、20台の個別サーバーを所有していることになります。実にシンプルです。しかし、シンプルさの代償として、リージョンごとの冗長性がありません。20台のサーバーは全て、オリジンにリクエストを送信する必要があります。この構造は脆弱で、キャッシュ率は低くなるでしょう。もっと良い方法がほかにあるはずです。

各リージョンに2台目のサーバーを追加すれば、冗長性を簡単に高めることができます。しかし、これはキャッシュ率を大幅に低下させかねません。サーバーを1台のみにするメリットは、全てのユーザーに1つのキャッシュをホスティングすれば良い点です。サーバーが2台だと、オリジン1台当たりのリクエスト件数は2倍になり、キャッシュがないケースも倍増します。

この問題を解決するには、サーバー同士がコミュニケーションを取り、相手のサーバーがコンテンツをキャッシュしている場合はそれを要求できるようにすれば良いでしょう。そのための最も簡単な方法は、キャッシュシャードの作成です。データを分割し、各サーバーがその一部を保持するようにして、リクエストを受けたサーバーは適切な部分のキャッシュシャードを保持しているサーバーにリクエストを転送します。

これは複雑に思えますが、NGINXに搭載されたロードバランサーはハッシュベースのロードバランシングをサポートしています。この機能ではリクエストをハッシュ化し、サーバーが利用可能であると想定して「同じリクエスト」を同じサーバーに転送します。このブログ記事のホーム版をご覧の方は、こちらからすぐに使えるNGINXクラスタの例を確認できます。このクラスタでは、ピアを発見し、URLをハッシュ化し、利用可能なサーバーを通じてリクエストを送信します。

a.jpgのリクエストがNGINXインスタンスに到達すると、全てのインスタンスがリクエストをクラスタ内の同じサーバーに転送します。b.jpgについても同様です。この設定によって、サーバーはロードバランサーのプロキシとストレージの一部としての両方の機能を果たします。さらに先進的な機能をCDNに搭載したい場合は、これらのレイヤーを分離することもできます。

PRのためのちょっとした余談

クラスタNGINXの例ではFlyを利用しています。当社は、Flyの機能は本当に素晴らしいと考えています。永続ボリュームはNGINXのアップグレード間もキャッシュ率を高水準に維持することに寄与します。暗号化されたプライベートネットワーキングは、NGINX間の安全・簡単なコミュニケーションを実現し、mTLSの複雑な設定の手間を省きます。ビルトインのDNSサービスディスカバリーは、サーバーを追加・削除するときにクラスタを最新に保つのに役立ちます。こうした機能がCDNとあまりに完璧にマッチしているではないかと思われるかもしれませんね。それは当社がこれらの機能をCDN型のワークロード専用に構築したからです。 もちろん、上記の機能は全てFly以外でも実現できます。しかし、Flyなら簡単です

玉ねぎのレイヤー

2つの真実をお伝えしましょう。高いキャッシュ率は善、インターネットは悪です。一石二鳥を好む人なら、キャッシュ率の問題と、インターネットのルーティングがうまくいかない問題を同時に解決できればありがたいと思うでしょう。どちらの問題の解決にも、インターネットがHTTPリクエストを雑に処理しないようにすることが必要となります。キャッシュ率を高めるには、コントロールできないインターネットは迂回して、適切に機能して信頼が置けるネットワークを通じてオリジンへのリクエストを送信することです。

CDNは通常、顧客のオリジンに近いリージョンへサーバーを設置しています。当社のNGINXの例をバージニア州に設定すれば、即座にAWSの最大のリージョン近くにサーバーを所有したことになります。そして、あなたには間違いなくAWSを利用している顧客がいるはずです。これが大規模で強力な独占企業に寄り添うメリットです。

NGINXとプロキシを少し調整すれば、オリジンサーバーへのリクエストを全てバージニア経由で送信できます。これはうれしいことです。というのも、あなたのバージニアのサーバーと、顧客のus-east-1(AWSのバージニア州北部リージョン)のサーバーの間には、インターネットに比べてデータ通信を妨げる罠が少ないからです。これで特定の顧客のリクエストを処理する単独の正式なサーバーの組み合わせが設定できました。

うれしいことに、上記の設定はキャッシュ率を改善するだけでなく、インターネットによるルーティングを回避することも可能です。さらに、追加のCDN機能の基礎としての役割も果たします。

CDNを選ぼうとすると、「シールディング」や「リクエスト集約」といった言葉を目にします。オリジンのシールディングとは通常、単に全てのトラフィックを所定のデータセンターを通じて送信することを意味します。オリジンサーバーへのトラフィックを最小化するだけでなく、自分のCDNリージョンが利用しているIPが分かる可能性が高いので、シンプルなL4ファイアウォール規則によってアクセスを制御できます。

また、リクエストの集約もオリジンのトラフィックを最小化できます。この方法は、多くのユーザーが同じコンテンツにアクセスしようとする大規模なイベントで特に有効です。10万人のユーザーが一斉に、あなたが執筆した最新のブログ記事にアクセスし、しかもその記事がまだキャッシュされていない場合、オリジンに10万件のリクエストが同時に送信される可能性があります。これはほとんどのオリジンが処理しきれないレベルのトラフィックです。この問題を解決するには、特定のURLを「ロック」し、1つのNGINXサーバーがリクエストをオリジンに送信している間、他のクライアントはファイルがキャッシュされるまで一時停止するようにすれば良いのです。当社のクラスタNGINXの例では、この設定はわずか2行で済みます

それでも通信が遅いときは

1つのリージョンを通じてリクエストを送信してキャッシュ率を高めるのは、ちょっとした裏技のようなものです。CDN全体の目的は、ユーザーのために速度を速めることにあります。オーストラリアからバージニアにリクエストを送れば、速度はほんの少しだけ速くなります。コンテンツをキャッシュしたNGINXサーバーからの応答は、ほぼ必ずオリジンサーバーよりも速いからです。しかし、これでも速度は遅く、満足とは言えません。

この問題は玉ねぎのレイヤーを増やすことで解決できます。

オーストラリアからのリクエストを、シンガポールを通じてバージニアに送信すれば良いのです。オーストラリアからバージニアまでの14,624kmは光の速さでも時間がかかります。そこで、オーストラリアからの距離が4,300kmのシンガポールへリクエストを送信し、シンガポールのキャッシュを利用することで、はっきりと分かるような遅延を回避します。この場合、キャッシュが存在しないと、直接バージニアにリクエストを送信する場合より通信速度が少し遅くなります。しかし、ここでの差は「イライラするほどの遅さ」と「イライラするほどの遅さよりも150ミリ秒遅い」の違い程度のわずかなものです。

汎用的なCDNを構築している場合は、これは良い方法といえます。各リージョンのキャッシュデータを集積するいくつかの広域リージョンを設定すればよいのです。

汎用的なCDNではなく、単にアプリケーションの速度を速めたい場合、この解決策には不安があります。アプリケーションの一部を複数のリージョンに分散させるほうが良いでしょう。

まとめ

CDNの基本的な考え方は目新しいものではなく、簡単に理解できます。一方で、CDNの構築は従来、野心的なチームのための事業であり、個人の開発者が週末に手掛けるようなプロジェクトではありませんでした。

しかし今や、有用なCDNを構築するための基本要素をNGINXなどのツールで利用できるようになってずいぶん経ちました。自宅で前述のGitHubリポジトリを試した方は、ここで取り扱っている最も複雑な設計のイテレーション、つまりリージョンごとの冗長性を有し、リクエストのリージョン間ルーティングの基本的な制御が可能な設計でさえも、ほとんどの場合はNGINXを設定するだけで済むことに気づいたでしょう。しかも、その設定もそれほど複雑ではありません。当社が追加した「コード」は、アドレスのプラグインに必要なbashだけです。

これでCDNの出来上がりです。単純なキャッシュなら問題なくこなせます。ただし、複雑なアプリで利用するには足りない部分があります。

特に、この記事ではキャッシュの期限切れについて全く取り上げませんでした。CDNを利用するときに必ずと言っていいほど起きるのが、ローンチのリリースに恥ずかしいタイプミスがあり、気づくのが遅れ、あらゆるキャッシュサーバーに誤字入りのコピーが保存されることです。分散されたキャッシュの削除は、CDNにとって厄介で大きな問題です。この問題だけでまるまる1本記事が書けるでしょう。

CDNレイヤーはアプリの機能を追加する場所としても非常に優れています。画像の最適化、WAF、APIレート制限、ボット検知などの機能が利用できます。これらのテーマだけで10本記事が書けるでしょう。

最後にもう1点。前述のとおり、この記事は全体にバイアスがかかっています。当社がこのCDN設計を取り上げているのは、それをとても簡単に実現できるプラットフォームを当社が構築したからです(皆さんもぜひお試しください)。このプラットフォームの機能を利用すれば、FlyでCDNを容易に構築できるだけでなく、アプリケーション全体を簡単に配信することも可能です。エッジ配信用に設計されたアプリケーションは、CDNを全く必要としない場合もあります。


詳細についてはこちらまでお問い合わせください。

最終更新日:2021年3月16日(*訳註 翻訳時の原文記載更新日)