Apache JMeter™では、Azure Media Servicesによるストリーミングに対しても負荷テストを実施することができます。
ここでは、JMeterのBlazeMeter - Video Streaming Plugin (HLS Plugin)を利用してAzure Media Servicesのストリーミングの負荷テストをし、その結果を参照する方法を紹介します。

なお今回は、コンテンツ保護などがされておらず一般に公開されている動画のストリーミングを対象とします。

また、JMeterやプラグインの標準では、動画配信の負荷テストにふさわしいレポートやグラフが用意されていないので、結果をAzure Application Insightsに送ることで、Application InsightsやLog Analyticsで参考になる結果を見ることができるようになります。

この結果は、Azure Media Servicesに限らず、他の動画配信サーバーでも同様に見ることができます。

設定概要

  1. 動画のエンドポイントURLを取得
  2. Azure Application InsightsのエンドポイントURLを取得
  3. JMeterプラグインを導入
  4. スレッドグループを追加
  5. bzm - Streaming Samplerを追加
  6. バックエンドリスナーを追加
  7. テストを実行
  8. Application Insights / Log Analyticsでの結果の参照

設定詳細

  1. 動画のエンドポイントURLを取得
    Azure Media Serviceでストリーミングしている動画のエンドポイントURLを取得します。
    Azure Portalで、対象Media Serviceの「資産 (新規) > {対象の資産}」で『URL の表示』をクリックすることで表示されるテスト対象とするプロトコルのエンドポイントURLをメモします。
    この値は、後のJMeterでのVideo Streamingサンプラーの設定で利用します。
    ストリーミング URL

  2. Azure Application Insightsのインストルメンテーション キーを取得
    JMeterが結果を送信するApplication Insightsのインストルメンテーション キーを取得します。
    Azure Portalで該当Application Insightsの概要ブレードからインストルメンテーション キーをメモします。
    この値は、後のJMeterでのBackend Listerの設定で利用します。
    Application Insights - インストルメンテーション キー

  3. JMeterプラグインを導入
    MPEG-DASHやHLSのストリーミングのサンプラーであるBlazeMeter - Video Streaming Plugin (HLS Plugin)と、Azure Application InsightsにJMeterの結果を送信するためのjmeter-backend-azureプラグインを導入します。

    • JMeter Plugins Managerを導入
      1. JMeter Plugins Managerのダウンロード
        Webブラウザを利用して以下のページから最新のPlugins Managerをダウンロードします。
        https://jmeter-plugins.org/install/Install/
        コンソールで以下のようなコマンドを実行することでもダウンロードすることができます。

        $ wget -O jmeter-plugins-manager.jar https://jmeter-plugins.org/get/
        
      2. ダウンロードしたファイルをJMeterのプラグイン用ディレクトリに配置
        先ほどダウンロードしたJMeter Plugins Managerのjarファイルを $JMETER_HOME/lib/ext にコピーします。

    • プラグインを導入
      1. JMeterを起動または再起動
      2. メニューの Options (オプション) > Plugins Manager から"JMeter Plugins Manager" を表示
      3. Available Pluginsタブで"BlazeMeter - HLS Plugin"と"Azure Backend listener"の2つを選択し、[Apply Changes and Restart JMeter]をクリックしてJMeterを再起動

    BlazeMeter - Video Streaming Plugin version 3.0.3以前は、Azure Media ServicesからのHLSプロトコルでの配信には対応していません。
    version 3.1以降を利用するか、version 3.0.3を当方がカスタマイズしたjarファイルに置き換えます。

  4. スレッドグループを追加
    Test Planに"スレッド グループ"を追加します。
    Add (追加) > Threads (Users) > Tread Group (スレッド グループ)

  5. bzm - Streaming Samplerを追加
    追加したスレッド グループに"bzm - Streaming Sampler"を追加します。
    Add (追加) > Sampler (サンプラー) > bzm - Streaming Sampler
    bzm - Streaming Sampler

    • Video
      • URL: 先ほどメモしたAzure Media ServicesのエンドポイントURLを入力
        例)

        https://jmeterdemo-jpea.streaming.media.azure.net/00000000-0000-0000-0000-000000000000/Filename.ism/manifest(format=m3u8-aapl)
        
    • Protocol
      Azure Media ServicesのHLSプロトコルをテストする場合は、"HLS"を選択する必要があります。
      MPEG-DASHの場合は、"Automatic"または"MPEG-DASH"のいずれかを選択してください。

    帯域幅(Bandwidth)や解像度(Resolution)などで適切な選択をしてください。
    (多くの場合はどちらもMaxを選択するのではないでしょうか?)

  6. バックエンドリスナーを追加
    Backend Listenerをテストプランまたはログを送りたいスレッドグループに追加し、いくつかのパラメーターを設定します。
    Add (追加) > Listener (リスナー) > Backend Listener
    Backend Listener

    • Backend Listener implementation: "io.github.adrianmo.jmeter.backendlistener.azure.AzureBackendClient"
    • Parameters
      • testName:
        テスト名。この値はApplication Insightsログのname列に入ります。
        kustoでテスト結果を参照するときに、フィルタ条件として利用することもできます。
      • instrumentationKey:
        先ほどメモしたApplication Insightsのインストルメンテーション キー
      • responseHeaders: "X-MEDIA-SEGMENT-DURATION"
  7. JMeterでテストを実行

Application Insights / Log Analyticsで結果の参照

今回はApplication InsightsのWorkbooksで結果を表示します。
Log Analyticsのブックで表示する場合は、テーブル名や列名が異なるので、以下の対応表に従って置き換えてください。

- Application Insights Log Analytics
テーブル requests AppRequests
name Name
success Success
duration DurationMs
customDimensions Properties
  1. Application Insights Workbookの作成
    JMeterが結果を送信しているApplication InsightsのWorkbooks (Log Analyticsの場合は「ブック」)を開き、クイックスタートの『空にする』をクリックします。
    Workbooks - Gallery

  2. パラメーターの追加
    表示対象のテスト結果を選択するために、『+追加』から「パラメーターの追加」をし、以下のパラメーターを追加します。

    • パラメーター#1

      • パラメーター名: DaysAgo
      • 表示名: 過去?日のデータから取得
      • パラメーターの型: テキスト
      • 必須: On
      • Add Validations
        • Regular Expression: ^[0-9]+$
        • Match: On
        • Message: 1以上の数値を入力してください
    • パラメーター#2

      • パラメーター名: name
      • パラメーターの型: ドロップ ダウン
      • 必要?: On
      • データの取得元: クエリ
      • Application Insights ログ クエリ
        • 時間の範囲: クエリに設定します

        • Kusto

          requests
          | where timestamp >= ago({DaysAgo}d)
          | summarize
              TestStartTime
                  = max(tolong(customDimensions.TestStartTime))
              by name = tostring(name)
          | sort by TestStartTime desc
          | project name
          
    • パラメーター#3

      • パラメーター名: TestStartTime
      • パラメーターの型: ドロップ ダウン
      • 必要?: On
      • データの取得元: クエリ
      • Application Insights ログ クエリ
        • 時間の範囲: クエリに設定します

        • Kusto

          requests
          | where name == "{name}"
              and timestamp >= ago({DaysAgo}d)
          | extend TestStartTime
              = tostring(customDimensions.TestStartTime)
          | distinct TestStartTime
          | project
              TestStartTime,
              formattedTestStartTime
                  = format_datetime(
                      unixtime_milliseconds_todatetime(tolong(TestStartTime)),
                      'yyyy/MM/dd HH:mm:ss'
                  )
          | sort by TestStartTime desc
          
    • パラメーター#4

      • パラメーター名: interval

      • パラメーターの型: ドロップ ダウン

      • 必要?: On

      • 説明: interval for graph

      • データの取得元: JSON

      • JSON Input

        ["1s", "1m", "10m", "1h", "1d"]
        

    設定が済んだら、適当な値を選択しておきましょう。
    パラメーター

  3. Video Streaming Reportの作成
    『+追加』から「クエリの追加」をし、表を追加します。

    • 時間の範囲: クエリに設定します
    • Application Insights ログ クエリ
    requests
    | where name == "{name}"
        and customDimensions.TestStartTime == "{TestStartTime}"
    | extend seg = iif(success == True, (duration / (toreal(customDimensions.['aih.x-media-segment-duration']) * 1000)), real(null))
    | summarize
        Samples = count(),
        SegmentSamples = count(),
        Average = tolong(avg(duration)),
        Min = min(duration),
        Max = max(duration),
        MediaSegmentDuration = round(avg(toreal(customDimensions.['aih.x-media-segment-duration']) * 1000), 1),
        SegAvg = avg(seg),
        lt50per = countif(seg <= 0.5),
        50to90per = countif(seg > 0.5 and seg <= 0.9),
        90to100per = countif(seg > 0.9 and seg <= 1),
        gt100per = countif(seg > 1),
        ErrorCount = countif(success == false),
        ReceivedKB = sum(tolong(customDimensions.Bytes)),
        StartTime = min(tolong(customDimensions.SampleStartTime)),
        EndTime = max(tolong(customDimensions.SampleEndTime))
        by Label = tostring(customDimensions.SampleLabel)
    | extend s = 0
    | union (
    requests
    | where name == "{name}"
        and customDimensions.TestStartTime == "{TestStartTime}"
    | extend
        seg = iif(success == True, (duration / (toreal(customDimensions.['aih.x-media-segment-duration']) * 1000)), real(null)),
        segWerror = customDimensions.['aih.x-media-segment-duration']
    | summarize
        Samples = count(),
        SegmentSamples = countif(toreal(segWerror) > 0),
        Average = tolong(avg(duration)),
        Min = min(duration),
        Max = max(duration),
        MediaSegmentDuration = round(avg(toreal(customDimensions.['aih.x-media-segment-duration']) * 1000), 1),
        SegAvg = avg(seg),
        lt50per = countif(seg < 0.5),
        50to90per = countif(seg > 0.5 and seg <= 0.9),
        90to100per = countif(seg > 0.9 and seg <= 1),
        gt100per = countif(seg > 1),
        ErrorCount = countif(success == false),
        ReceivedKB = sum(tolong(customDimensions.Bytes)),
        StartTime = min(tolong(customDimensions.SampleStartTime)),
        EndTime = max(tolong(customDimensions.SampleEndTime))
    | extend Label = 'TOTAL', s = 9
    )
    | extend
        tp = Samples / ((EndTime - StartTime) / 1000.0),
        KBPeriod = (EndTime - StartTime) * 1024 / 1000.0
    | sort by s asc
    | project
        Label, Samples, ['Avg.'] = Average,
        Min, Max,
        ['Avg. seg. len.'] = iif(SegAvg >= 0, toreal(MediaSegmentDuration), real(null)),
        ['Resp. / Seg. len.'] = iif(SegAvg >= 0, strcat(round(SegAvg * 100, 3), '%'), ''),
        ['<50%'] = iif(SegAvg >= 0, strcat(round(lt50per * 100.0 / SegmentSamples, 2), '%'), ''),
        ['50-90%'] = iif(SegAvg >= 0, strcat(round(50to90per * 100.0 / SegmentSamples, 2), '%'), ''),
        ['90-100%'] = iif(SegAvg >= 0, strcat(round(90to100per * 100.0 / SegmentSamples, 2), '%'), ''),
        ['>100%'] = iif(SegAvg >= 0, strcat(round(gt100per * 100.0 / SegmentSamples, 2), '%'), ''),
        ['Error %'] = strcat(round(ErrorCount * 100.0 / Samples, 2), '%'),
        ['Throughput'] = iif(tp < 1.0,
                            strcat(round(tp * 60, 1), '/min'),
                            strcat(round(tp, 1), '/sec')
                        ),
        ['Received KB/sec'] = round(ReceivedKB / KBPeriod, 2),
        ['Avg. Bytes'] = round(ReceivedKB / toreal(Samples), 1)
    | project-reorder
        Label, Samples, ['Avg.'], Min, Max,
        ['Avg. seg. len.'], 
        ['Resp. / Seg. len.'],
        ['<50%'], ['50-90%'], ['90-100%'], ['>100%'],
        ['Error %'], ['Throughput'],
        ['Received KB/sec'], ['Avg. Bytes']
    

    Video Streaming Report
    Summary Reportをベースに、以下の項目を加えています。

    • Avg. seg. len.: メディア セグメントの長さの平均(ms)
    • Resp. / Seg. len.: メディア セグメントの長さに対するレスポンスタイムの割合
    • <50%: メディア セグメントの長さに対するレスポンスタイムの割合が50%以下のリクエストの割合
    • 50-90%: メディア セグメントの長さに対するレスポンスタイムの割合が50%~90%に含まれるリクエストの割合
    • 90-100%: メディア セグメントの長さに対するレスポンスタイムの割合が90%~100%に含まれるリクエストの割合
    • >100%: レスポンスタイムがメディアセグメントの長さを超えてしまうリクエストの割合
  4. グラフの作成
    『+追加』から「クエリの追加」をし、グラフを追加します。
    Video segmentとAudio segmentのセグメントの長さ(ms)と、レスポンスタイムをプロットします。

    • Application Insights ログ クエリ
    requests
    | where name == "{name}"
        and customDimensions.TestStartTime == "{TestStartTime}"
        and customDimensions.SampleLabel matches regex "- (audio|video|media) segment$"
    | extend
        Label = customDimensions.SampleLabel,
        SampleStartTime
            =  unixtime_milliseconds_todatetime(tolong(customDimensions.SampleStartTime)),
        SegmentDuration = toreal(customDimensions.['aih.x-media-segment-duration']) * 1000
    | summarize
        ['audio resp.'] = avgif(duration, Label hassuffix "- audio segment"),
        ['audio length'] = avgif(SegmentDuration, Label hassuffix "- audio segment"),
        ['video resp.'] = avgif(duration, Label hassuffix "- video segment"),
        ['video length'] = avgif(SegmentDuration, Label hassuffix "- video segment"),
        ['media resp.'] = avgif(duration, Label hassuffix "- media segment"),
        ['media length'] = avgif(SegmentDuration, Label hassuffix "- media segment")
        by bin(SampleStartTime, totimespan("{interval}"))
    | project
        SampleStartTime,
        ['audio resp.'],
        ['audio length'],
        ['video resp.'],
        ['video length'],
        ['media resp.'],
        ['media length']
    | render timechart
    
    • 時間の範囲: クエリに設定します
    • グラフの設定

      『グラフの設定』ボタンが表示されない場合は、「視覚化」で一度『クエリごとに設定』以外を選択し、再度『クエリごとに設定』を選択します。

      • 凡例の設定
        • メトリック情報の表示: Off
        • 系列の凡例の表示: On

    Video Streaming Graph

  5. Workbookの保存
    「保存」アイコンをクリックして、作成したWorkbookを保存しておきましょう。

これで、パラメータを適当に選択することで、以下のように表示されます。
Video Streaming Report

メディア セグメントに対するリクエストのレスポンスタイムが、そのメディア セグメントの長さを超えてしまうと、プレイヤー クライアントが持っているバッファを消費することになります。
これが積み重なると、クライアントのバッファが不足しメディアの再生に待ちが発生することになるため、メディア セグメントの長さに対するレスポンスタイムの割合が低いことが望ましいと考えられます。

以上、JMeterからAzure Media Servicesのストリーミングに対してテストを実施し、その結果をAzure Application Insights / Log Analyticsで参照する方法を紹介しました。

【宣伝】
AzureにJMeterの分散テスト環境を簡単構築
『Load Tester Powered by Apache JMeter™』