tail my trail

作るのも使うのも、結局は、人なのだ

HLS動画の配信テスト用に、JMeterで同時視聴シナリオを書いてみた

RTMP サーバ から AkamaiやCloudFrontのようなCDN 経由で HLSによるライブ配信を行っているとする。 構成は、以下のようなものを想像してもらえれば良い。

f:id:uorat:20160610000244p:plain

同時視聴者数の増加に従ってOriginにどのような負荷がかかるのか確認したかったので、シナリオを組んでみた。

前提: HLSのおさらい

HLS (HTTP Live Streaming) はAppleが開発した仕様。動画を10秒など適度な長さでセグメント化し、セグメントファイルの場所や再生時間、再生順序などが記録されたプレイリストに従って再生するもの。bitrate毎のプレイリストを用意することでAdaptive Bitrateにも対応できる。

詳しくはこのあたりにまとまっている。

背景: ライブ配信は同一ファイルへのリクエストが集中する

VODのHLS配信であれば既に動画の長さは決まっているので、セグメントファイル (*.ts) とプレイリスト (*.m3u8) がCDNのキャッシュに乗るようにCache-Controlを調整してあげれば良い。CDNのキャッシュに乗りさえすれば、あとはClientとCDN の Edgeの世界に閉じるので、Cache Hit率さえ気をつけておけば、Originの負荷はさほど気にする必要はない。

ただし、ライブ配信の場合はセグメントファイルは常に生成され、プレイリストも常に更新される。CDNに古いプレイリストがCacheされてしまうと動画視聴に支障が出るので、TTLはsegment-timeにあわせて短くする必要がある。 また、ロングテールなVODコンテンツと異なりライブ配信の場合、多数のユーザが同時に同じセグメントファイルとプレイリストを参照することになる。Cacheに乗っていなければ、当然Originにリクエストが流れる。

そのため、同時視聴者数の増加に従ってOriginにどのような負荷がかかるのか確認したかったので、シナリオを組んでみた。

HLS視聴のシナリオ作りの勘所

前置きが長くなったが本題。

プレイリストの定期取得とプレイリストに従ったセグメントファイルの取得を模倣する必要がある。ざっと流れは以下。

  1. playlist.m3u8
  2. (loop start)
    1. chunklist_${bitrate}.m3u8
    2. media-${chunk}.ts
    3. wait for segment-time
  3. (loop end)

今回はJMeterクラスタがちょうど手頃に使える環境にあったので、この流れに沿ってHLS視聴のシナリオを組んでみた。 XMLは記事末のGist参照。

全体像は以下のような感じ。

f:id:uorat:20160610153055p:plain

一つずつ説明する。

0. 初期値を設定

Test Planのユーザ定義変数を使ってドメインやポート、Pathやセグメントファイル長など環境や配信毎に変わりうる値を変数化しておく。 f:id:uorat:20160610153059p:plain

また、HTTPリクエスト初期値を定義し、後続のサンプラで冗長な設定をせずに済むようにしておく。 f:id:uorat:20160610153102p:plain

あとは、HTTP Header Manager や HTTP Cache Manager を環境やシナリオにあわせて定義する。 自分は、主要ブラウザにあわせてGzip圧縮前提で試験したかったので、Request Header に "Accept-Encoding: gzip" を入れるようにした。

1. Master Index fileを取得

初期値で定義したURLに対してHTTP Requestを送信するだけで、ここはシンプル。 f:id:uorat:20160610153105p:plain

Bandwidthとbitrate毎のプレイリストが手に入るので、正規表現を HTTP Request Sampler の後に追加して必要なプレイリストを抽出し、変数に格納しておく。 f:id:uorat:20160610153109p:plain

2. (以下繰り返し) Playlistを取得

ライブ配信の場合、プレイリストやセグメントファイルが定期的に更新/追加されるため、以降繰り返し処理となる。 1. で取得したプレイリストのファイル名をもとに、プレイリストを取得する。 f:id:uorat:20160610153112p:plain

セグメントファイルの一覧が手に入るので、1. と同じ要領で正規表現でセグメントファイル名を抽出し、変数に格納しておく。 f:id:uorat:20160610153115p:plain

3. セグメントファイルを取得する

  1. で取得したセグメントファイルのPathをもとに、セグメントファイルを取得する。ここもシンプル。 f:id:uorat:20160610153117p:plain

4. wait

以降、2と3の繰り返しになるが、マニフェストファイルはセグメントファイルの長さ程度の頻度でfetchするため、2の処理に入る前にWaitさせるようにする。 f:id:uorat:20160610153120p:plain

以上がシナリオ。 動かした時のアニメーションGIFを載せておく。

f:id:uorat:20160610175755g:plain

あとはThread数やThread Groupを増やしたり、複数のJMeterにリモート実行するなりして、多重度増やして試験すれば良い。 JMeterのリモート実行方法は、以下を参考にすればすんなり出来ると思う。

最後に

参考までに、私の環境での結果の概要を載せておく。 使っている製品や環境、セグメントファイル長、画質など様々な要因で性能や傾向はかなり変わると思うので、あくまで参考までに。 試験内容や結果に突っ込みあればコメントください。

環境

  • CDN: CloudFront
  • Origin: Wowza Streaming Engine 4 on EC2 (g2.2xlarge)
  • 配信設定: H.264, bitrate 2500 (CBR), キーフレーム2秒
  • セグメントファイル長: 2秒
  • Cache-Control
    • プレイリストファイル max-age = 1
    • セグメントファイル max-age = 3600

傾向

  • セグメント長が2秒と短く、Cache-Control も max-age = 1 のような短いものとしても、多重度を上げるとちゃんとCache Hitしてくれた
  • 多重度を10,000程度まであげて試験してみたが、Originサーバには大した負荷はかからなかった。
    • 同時視聴者数が数千レベルまで増えると、CDN - Origin server間のTCP Connection数が微増した程度。
    • CPUやメモリ、Network IOも大したレベルではなかったので、CDNの恩恵にあずかれた。
  • プレイリストファイルへのリクエストをOriginに全てPassする設定を加えたところ、CDN - Origin server間のTCP Connection数がリニアに増え、Originの負荷も増となった。
    • まぁ当然だよね。Bad knowhow。

1 RTMPサーバ + CloudFrontを挟む程度で、数千人程度のライブ配信はで捌けてしまいそう。 Originの可用性など考えると、RTMPサーバをさらにOriginとEdgeで分離する以下のような構成にしたほうが良いとは思う。 f:id:uorat:20160610000252p:plain その分サーバ稼働費やライセンスなどかかるので、このあたりは費用対効果考えて、判断すれば良いと思う。

Gist