👻

HLSの標準機能を利用してマルチCDNの負荷分散を試す

に公開

何から試そうかと新しく定義されたタグを見ていたのですが、気になるタグがありました。

EXT-X-CONTENT-STEERING

新しいHLSで採用されたこのタグでは、複数のCDNをコントロールする機能を提供します。いわゆるマルチCDNですね。CDNを契約すれば心配無用、と言っていたのは昔の話。今ではCDN自体がダウンする可能性や一つのCDNだけだと負荷が捌ききれない場合などを想定して2つ以上のCDNを契約する方も多いです。そういった状況を想定したタグになっています。

環境の準備

複数CDNの用意

まずはテスト用に2つのCDNを用意します。
一つは自社製のサービスである「CDNext」使用。これなら社員であれば検証アカウントであれば無料で使用できます。もう一つはAWSのサービスである「CloudFront」を無償期間分の想定で使用します。

https://www.stream.co.jp/service/cdn/cdnext/
https://aws.amazon.com/jp/cloudfront/

アカウントを作成したら、それぞれに設定を施します。設定方法などは各種マニュアルをご参照ください。

疎通確認

hls.jsなどでドメイン部分をCDNで指定されたアドレスに変更し、再生できれば浸透が完了しています。CDNextの例ではabcdefghijklmnopqrstuvwxyz.cdnext.stream.ne.jpになったとします。CloudFrontは123456789.cloudfront.netとします。これらの2つのCDNのどちらも同じようにhls.jsで動画が再生されれば準備完了です。

ステアリング機能を知る

ドラフトを見る

まずはドラフトを見てみましょう。セクション4.4.6.6にタグの説明などがありますが、詳細はセクション7で説明されていますので、両方を見ておきましょう。また専用のドラフトもありますので、こちらも見ておくとよいでしょう。

https://datatracker.ietf.org/doc/html/draft-pantos-content-steering-00

紹介動画

また、AppleのWWDC21,22あたりの説明動画もありますので、こちらも軽く見てみます。
https://developer.apple.com/jp/videos/play/wwdc2021/10141/
https://developer.apple.com/jp/videos/play/wwdc2022/10144/

まとめ

ざっくりとまとめると、「ステアリングサーバを新規で立てれば、HLSは動的にステアリングサーバに問い合わせに行くようになる」「ステアリングサーバがどういう優先順位をつけるかは自分たちでロジックを作れる」というようなものになります。

これまでのリダンダントプレイリストでは、順番にCDN1,CDN2,CDN1と羅列しているだけでした。つまりCDN1が落ちて、その後CDN2が落ちると、もう一度CDN1に戻ったらもう可用性は失われた状態になってしまうという問題があります。当社のプレイリストも例にもれずこのパターンで運用していますので、無制限に冗長化を保証することは出来ていませんでした。まあひたすらCDN1,CDN2の繰り返しを100個ぐらい書いておけば良いのかもしれませんが、それだとマスタープレイリストが肥大化し、管理も大変になりますのであまり現実的ではありません。

想定される使用場面

今回はVOD(Video On Demand)にて検証をおこないますが、主にライブ配信において必要になりそうな機能ですね。クライアント検証が終わったらライブ配信でもテストしてみたいと思います。とはいえVODでも急なアクセスで負荷が跳ね上がることはよくありますので、その想定でまずは動的な負荷分散という想定で検証してみます。

ステアリングサーバ

サーバプログラムの準備

この機能の肝となるのは、ステアリングサーバになります。静的なjsonファイルをおいても良いのですが、ある程度条件を判断して動的にjsonを生成するとなるとプログラムが必要になりますので、今回のサーバの中で定期的に実行されjsonファイルをDocumentRootに書き出すプログラムを動かそうと思います。将来的にはjsonを動的に返すプログラムをCGI的に動かした方がよさそうですが、まずはテストなのでここまで。

refresh.py
import json
import random

OUTPUT_FILE = "/usr/share/nginx/html/steering.json"

def main():
    priority = ["cdnext", "cloudfront"] if random.random() < 0.5 else ["cloudfront", "cdnext"]

    payload = {
        "VERSION": 1,
        "TTL": 10,
        "PATHWAY-PRIORITY": priority
    }

    with open(OUTPUT_FILE, "w") as f:
        json.dump(payload, f, indent=2)

    print(f"[OK] steering.json updated: {priority}")

if __name__ == "__main__":
    main()

この程度のプログラムであればChatGPTが即座に生成してくれるのはよいですね。まずはテスト的に、このように50:50の確率でCDN1とCDN2の優先順位を変えたものを書き出します。更新頻度も高めに10秒とします。

サーバプログラムの実行

こちらを定期的にcronなどで実行することで、ラウンドロビン的な動きができればという感じで実際に動かしてみましょう。定期的に実行するために、シェルスクリプトから起動します。

refresh.sh
#!/bin/bash
while true
do
  python3 /usr/share/nginx/html/refresh.py
  sleep 10
done

cronでもよいのですが、とりあえずはテストなので手元でコントロールできる方が楽ですね。
では動かしてみましょう。

$ chmod +x refresh.sh
$ ./refresh.sh
[OK] steering.json updated: ["cloudfront", "cdnext"]
[OK] steering.json updated: ["cdnext", "cloudfront"]
[OK] steering.json updated: ["cdnext", "cloudfront"]
[OK] steering.json updated: ["cloudfront", "cdnext"]
[OK] steering.json updated: ["cdnext", "cloudfront"]

動作的には問題ないようです。これを使用してCDNがランダムに切り替わるかを見て行きましょう。

HLSへの組み込み

単純な挿入

では先ほどのテスト用素材を使用して、タグを追加してみましょう。

$ cp -r 1_normal/ 2_steering/
$ cd 2_steering
$ vi master.m3u8

まずは正常に再生が動作した1番のディレクトリをそのままコピーして、そこに色々と手を加えて行きます。加えるのはmaster.m3u8だけで良さそうなので、以下のように編集します。

master.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-CONTENT-STEERING:SERVER-URI="https://videostream.tech/steering.json",PATHWAY-ID="cdnext"

#EXT-X-STREAM-INF:BANDWIDTH=1914531,AVERAGE-BANDWIDTH=761372,RESOLUTION=640x360,CODECS="avc1.64001e,mp4a.40.2",PATHWAY-ID="cdnext"
v0/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=713105,AVERAGE-BANDWIDTH=402174,RESOLUTION=426x240,CODECS="avc1.640015,mp4a.40.2",PATHWAY-ID="cdnext"
v1/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=379287,AVERAGE-BANDWIDTH=229047,RESOLUTION=256x144,CODECS="avc1.64000c,mp4a.40.2",PATHWAY-ID="cdnext"
v2/playlist.m3u8

まずは最低限の書式を追加。一気に色々追加すると何か起こった時の因果関係がわからなくなりますので、ちょっとずつ追加しては確認してという作業を繰り返します。

今回はEXT-X-CONTENT-STEERINGSERVER-URIを先ほどのjsonのアドレスとして追加しました。また初期値はcdnextに指定しています。これで10秒ごとにjsonへのアクセスがあるかを確認していきます。またPATHWAY-IDも各レンディションに追加し、cdnextという関連付けを行います。まずはこれで動作を見てみましょう。

Chromeの開発者ツールでみると、10秒ごとにjsonにアクセスがありますね。とりあえずHLSがjsonを確認しに行くという動作は行われていそうです。
アクセスされたアドレスを見てみると以下のようになっていました。

https://videostream.tech/steering.json?_HLS_pathway=cdn1&_HLS_throughput=42386347

ここで付与されている_HLS_pathway_HLS_throughputは、規格に記載されている内容です。スループットは1秒ごとのクライアント(今回はhls.js)が概算で計算した値を付与するように決められています。このあたりをデータ取得して集計するとPATHWAYごとのスループットなども集計から、CDNごとのパフォーマンス測定などもできるかもしれませんね。ただこれはまた別の記事で。

CDNの切り替えによる負荷分散

では次に、実際に2つのCDNを切り替えてみます。

$ cp -r 2_steering/ 2_multicdn/
$ cd 2_multicdn
$ vi master.m3u8

マルチCDN用のテストディレクトリを作成し、master.m3u8ファイルを編集していきます。

master.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-CONTENT-STEERING:SERVER-URI="https://videostream.tech/steering.json",PATHWAY-ID="cdnext"

#EXT-X-STREAM-INF:BANDWIDTH=1914531,AVERAGE-BANDWIDTH=761372,RESOLUTION=640x360,CODECS="avc1.64001e,mp4a.40.2",PATHWAY-ID="cdnext"
https://abcdefghijklmnopqrstuvwxyz.cdnext.stream.ne.jp/3_multicdn/v0/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=713105,AVERAGE-BANDWIDTH=402174,RESOLUTION=426x240,CODECS="avc1.640015,mp4a.40.2",PATHWAY-ID="cdnext"
https://abcdefghijklmnopqrstuvwxyz.cdnext.stream.ne.jp/3_multicdn/v1/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=379287,AVERAGE-BANDWIDTH=229047,RESOLUTION=256x144,CODECS="avc1.64000c,mp4a.40.2",PATHWAY-ID="cdnext"
https://abcdefghijklmnopqrstuvwxyz.cdnext.stream.ne.jp/3_multicdn/v2/playlist.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=1914531,AVERAGE-BANDWIDTH=761372,RESOLUTION=640x360,CODECS="avc1.64001e,mp4a.40.2",PATHWAY-ID="cloudfront"
https://123456789.cloudfront.net/3_multicdn/v0/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=713105,AVERAGE-BANDWIDTH=402174,RESOLUTION=426x240,CODECS="avc1.640015,mp4a.40.2",PATHWAY-ID="cloudfront"
https://123456789.cloudfront.net/3_multicdn/v1/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=379287,AVERAGE-BANDWIDTH=229047,RESOLUTION=256x144,CODECS="avc1.64000c,mp4a.40.2",PATHWAY-ID="cloudfront"
https://123456789.cloudfront.net/3_multicdn/v2/playlist.m3u8s

こんな感じで、PATHWAYごとにアドレスを記載していきます。この状態でsteering.jsonの値が変わった際に、それぞれのセグメントがどちら経由でアクセスされているかを見て行きます。

https://123456789.cloudfront.net/3_multicdn/v0/segment_026.ts
https://abcdefghijklmnopqrstuvwxyz.cdnext.stream.ne.jp/3_multicdn/v0/segment_027.ts
https://abcdefghijklmnopqrstuvwxyz.cdnext.stream.ne.jp/3_multicdn/v0/segment_028.ts
https://abcdefghijklmnopqrstuvwxyz.cdnext.stream.ne.jp/3_multicdn/v0/segment_029.ts
https://123456789.cloudfront.net/3_multicdn/v0/segment_030.ts

一部抜粋すると、このようになっていました。
26~27と29~30の間でCDNの切り替え指示があったようです。ではそのタイミングでのjsonの値を確認してみます。

26-27
PATHWAY-PRIORITY:["cdnext", "cloudfront"]
TTL:10
VERSION:1
29-30
PATHWAY-PRIORITY:["cloudfront", "cdnext"]
TTL:10
VERSION:1

Chromeの開発者ツールでみるとこのようになっていました。
26はCloudFrontですが、27を読み込む前にステアリングサーバからCDNext優先の指示がきていたことがわかります。実際に27から29はCDNextのアドレスですね。
その後29の後のjsonでは、CloudFrontが優先の指示になっております。その後の30ではアドレスがCloudFrontになっていますので、jsonの指示通りアドレスがさし変わっているのがわかります。

これで一つのCDNだけでなく、複数のCDNから分散してダウンロードするようになりましたので、CDN事業者に負荷をかけすぎないという配信も簡単にできるようになっていますね。

ステアリングサーバのダウン

ここでsteering.jsonを削除し、refresh.pyを止めてみました。ステアリングサーバにアクセスできなくなった時の挙動確認です。実はこのあたりがマルチCDNのアキレス腱で、結局のところマルチCDNシステムが落ちてしまうとどちらのCDN経由でも再生できなくなるものが多いです。ただこのタグでは、ステアリングサーバにアクセスができなくなっても動画が止まることはありませんでした。

またその後refresh.pyを起動し、steering.jsonがまた作成され始めると、そこからはまた普通にステアリング機能が復活しました。これはいいですね。このあたりも今後色々ストレスを与えてどこまで耐えられるかなど確認していきたいと思います。

今後は負荷分散だけでなく耐障害性という観点や、ステアリングサーバの動的化による時間帯や地域ごとのCDN選択、またプレイヤーから送られる_HLS_throughputのリアルタイム集計を元に、よりパフォーマンスの良いCDNへ寄せるというような挙動も試してみたいと思います。


https://voice.stream.co.jp/

Discussion