「YouTube を設計せよ」は人気のあるシステム設計の問題です。面白い部分が、人が予想する場所にないからです。Web サーバーはごく普通です。難しいのは、スマートフォンからストレージへ数ギガバイトを確実に運ぶこと、1 回のアップロードをあらゆる端末と回線で再生できる十数種類のバージョンに変えること、そしてそのバイトを CDN から配信し、自前のサーバーがほとんど目にしないようにすることです。
この記事では、そのパイプラインを順に追います。アップロード、トランスコード、パッケージング、配信です。最後に、そのまま流用できるアーキテクチャ図と、図に書き込むべき判断を示します。
システムは 1 つではなく 2 つ
動画プラットフォームは、1 つのデータベースを共有する 2 つのシステムです。
- 取り込み: クリエイターがアップロードし、プラットフォームがトランスコードとパッケージングを行います。遅延は分単位で測られ、誰も気にしません。
- 再生: 視聴者が再生ボタンを押します。最初のフレームは数秒以内に表示されなければならず、止まってはいけません。
この 2 つは 2 本のレーンとして描きます。スケールの基準となるシグナル(1 時間あたりのアップロード数と同時視聴者数)も、障害の起き方も違うからです。
アップロード:API サーバーを経由させない
動画ファイルは大きく、モバイル回線は切れます。そこから 2 つのルールが導かれます。
- オブジェクトストレージに直接アップロードする。 アップロード API はクリエイターを認証し、動画のレコードを作成して、署名付き URL を返します。クライアントはバイト列をバケットに直接送ります。API がギガバイト単位のデータを中継することはありません。
- 再開できるようにする。 マルチパートアップロードを使い、接続が切れても失敗したパートだけを再試行できるようにします。たとえば Amazon S3 のマルチパートアップロード は、パートを個別にアップロードし、クライアントがアップロードを完了したときに結合します。
アップロードが完了すると、ストレージのイベント(またはクライアントの完了呼び出し)がトランスコードのジョブをキューに入れます。
トランスコード:1 回のアップロードから、アダプティブビットレートのラダーへ
プレイヤーは帯域に応じて画質を切り替えるので、動画はそれぞれ何度もエンコードします。これが アダプティブビットレート(ABR)ラダー です。一般的なラダーは 240p か 360p あたりから 1080p か 4K まであり、段ごとに独自のビットレートを持ちます。
トランスコードは CPU 負荷が高く、並列化がとても簡単です。
- ソースをキーフレームで区切り、数秒ずつのチャンクに分割する。
- 各チャンクと各レンディションを、ワーカーフリート上の独立したジョブとしてエンコードする。通常は FFmpeg を使う。
- 結果をつなぎ合わせるか、セグメントのまま保持する。
そうすれば 2 時間の映画も数時間ではなく数分でトランスコードでき、失敗したジョブは 1 つのチャンクだけを再試行します。ジョブは動画、レンディション、チャンクをキーにして冪等にし、再試行で重複が生まれず上書きされるようにします。
すべてのコンテンツに 1 つのラダーを使うと、ビットを無駄にします。静止したスライドのプレゼンは、サッカーの試合よりはるかに低いビットレートで済みます。Netflix は、動画ごとの複雑さに応じてラダーを選ぶ、タイトル単位のエンコード(per-title encoding)を広めました。最初は固定のラダーで始め、あとからタイトル単位に移行できます。
パッケージング:HLS、DASH、CMAF
プレイヤーは 1 つの大きなファイルをダウンロードするわけではありません。レンディションとセグメントを列挙したマニフェストを読み、数秒ずつのセグメントを普通の HTTP で取得します。
| 形式 | 定義 | 主なプレイヤー |
|---|---|---|
| HLS | RFC 8216 | Apple のデバイス、hls.js などのプレイヤーを使うほとんどのブラウザー |
| MPEG-DASH | ISO/IEC 23009-1 | Android、スマートテレビ、dash.js や Shaka Player を使うブラウザー |
| CMAF | ISO/IEC 23000-19 | HLS と DASH の両方のマニフェストに使える、1 組の断片化 MP4 セグメント |
パッケージャーが重要なのは CMAF のためです。CMAF を使えば、すべての動画を 2 部ずつ持つのではなく、1 組のセグメントを保存して 2 つのマニフェストを書けば済みます。短いセグメントは再生開始が速く、画質の切り替えも早くなります。長いセグメントはリクエスト数が少なく済みます。Apple の HLS オーサリング仕様 には、セグメントの長さ、キーフレーム、ラダーについての具体的な推奨が載っています。
有料コンテンツには DRM が加わります。パッケージャーは共通暗号化(CENC)でセグメントを一度だけ暗号化し、各プラットフォームの DRM システム(Widevine、FairPlay、PlayReady)がライセンスサーバーを通じてプレイヤーにライセンスを発行します。
配信:仕事をするのは CDN
セグメントは静的なファイルであり、そこが要点です。CDN が視聴者の近くでキャッシュするので、人気の動画はほぼすべてエッジキャッシュから配信されます。オリジンはレンディションのバケットで、多くの場合はオリジンシールドの背後に置きます。そうすれば、50 か所のエッジでのキャッシュミスが、ストレージへの 1 回のリクエストにまとまります。
再生 API は小さいままです。視聴者がその動画を見てよいかを確認し、マニフェストの URL を返し(非公開コンテンツなら署名付き)、分析用に再生イベントを記録します。
ライブ配信は別のパイプライン
ライブ動画はパッケージングと CDN を再利用しますが、取り込みは置き換えます。エンコーダーが RTMP や SRT などのプロトコルで取り込みサーバーにストリームを送り、サーバーはラダーをリアルタイムにトランスコードして、数秒ごとに新しいセグメントを書き出します。遅延はセグメントの長さとプレイヤーのバッファーで決まります。HLS と DASH の低遅延モードは、セグメントが完成する前にその一部を届けることで、数秒程度に近づけます。ライブとオンデマンドは、CDN、プレイヤー、メタデータを共有する別々のパイプラインとして保ちます。
アーキテクチャ図
ダイアグラムの内容
- Creator
- Upload API
- Raw uploads
- Transcode jobs
- Transcode workers
- Packager
- Renditions
- Video metadata
- Viewer
- Playback API
- CDN
- DRM license server
- 1. upload URL
- 2. multipart
- 3. done
- ladder
- segments
- ready
- manifest URL
- cache miss
- license
図の読み方は次のとおりです。
- 上のレーン、取り込み: クリエイターはアップロード API からアップロード URL を受け取り(1)、未加工のアップロード用バケットにパートを直接アップロードします(2)。完了したアップロードがトランスコードのジョブをキューに入れます(3)。ワーカーがラダーをエンコードし、パッケージャーがセグメントをレンディションのバケットに書き出して、メタデータストアで動画を準備完了にします。
- 下のレーン、再生: 視聴者は再生 API にマニフェストの URL を要求し、CDN からセグメントをストリーミングします。キャッシュミスのときはレンディションのバケットを読みます。
- DRM は保護されたコンテンツだけが必要とするので点線です。プレイヤーはライセンスサーバーからライセンスを取得します。
- Markdown ブロック はラダーの例を示しています。実際の数字は、自分のコンテンツと視聴者の帯域から決めます。
図に載せるべきこと
- バイトの流れ。 アップロードとセグメントが API を通らず、ストレージと CDN に向かう様子を描きます。
- ジョブの単位。 「レンディションとチャンクごとに 1 ジョブ」と書けば、再試行と並列性の仕組みがレビュアーに伝わります。
- 形式と保護。 HLS、DASH、CMAF のどれを使うか、DRM を使うならそれも書きます。
- 準備完了のシグナル。 パッケージングが終わるまで動画は再生できません。その状態がどこにあるかを示します。
キューとワーカーという同じ形は、イベント駆動アーキテクチャの記事 にも出てきます。クラウド側については、複数リージョンからの配信を AWS のマルチリージョンアーキテクチャ図 で、ワーカーフリートの代わりにストレージのイベントで関数を起動する方法を サーバーレスアーキテクチャ図 で扱っています。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- クリエイターと視聴者用に、C4 モデルパックから 人物 ノードを 2 つ追加します。
- アップロード API、トランスコードワーカー、パッケージャー、再生 API 用に コンテナ ノードを、動画のメタデータ用に データベース を追加します。
- クラウドパックから オブジェクトストレージ ノードを 2 つと CDN を、トランスコードのジョブ用にデータパックから キュー を、DRM ライセンスサーバー用に 外部システム を追加します。
- 取り込みを上のレーンに、再生を下のレーンに描き、エッジにアップロードの手順の番号を付けて、ラダーを書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 13 個で、Free プランの範囲に収まります。
よくある質問
アダプティブビットレートのラダーとは何ですか? 動画をエンコードするレンディションの一覧で、それぞれが独自の解像度とビットレートを持ちます。プレイヤーは視聴者の回線で維持できる最良のものを選び、帯域が変わるたびに切り替えます。
HLS と DASH のどちらを使うべきですか? 多くの場合は両方です。CMAF のセグメントとして一度だけパッケージングし、HLS と DASH のマニフェストを書けば、2 部保存しなくても Apple のデバイスとほかのプレイヤーの両方に対応できます。
なぜオブジェクトストレージに直接アップロードするのですか? 大きなファイルが API サーバーを通ると、接続とメモリを占有し、接続が切れるとアップロードが最初からやり直しになります。署名付きのマルチパートアップロードならストレージに直接届き、パート単位で再開できます。
トランスコードはどう並列化しますか? ソースをキーフレームでチャンクに分け、各チャンクと各レンディションを別々のジョブとしてエンコードし、結果をつなぎ合わせるかパッケージングします。
動画には CDN が必要ですか? ごく小さな視聴者層を超えるなら必要です。セグメントは静的なファイルなので、CDN は繰り返しのリクエストを、ストレージではなく視聴者に近いキャッシュから返します。