この記事はDatadog Advent Calendar 2024 5日目の記事です🎅
株式会社AbemaTVでABEMAの広告開発を担当している黒崎(@kuro_m88)です。昨日はCloudflare Zero Trustの新しいSSHについて書きましたが、今日はDatadogです。
ABEMAでは動画コンテンツ内にCM表現を挿入する技術の検証を行っており、Inter BEE 2024でデモ実装を出展しました。
「Inter BEE 2024」に「ABEMA」「WINTICKET」が共同ブース出展 | 株式会社サイバーエージェント
動画配信関連の展示が主であったため特にご紹介していませんでしたが、このデモ実装はGoで実装し、モニタリングにDatadogを使っていました。
動画配信サービスに関わり始めてまだ半年経っておらず、日々新しいことを学びながら業務を進めているところですが、このデモを開発する過程で自分が知っている技術を組み合わせてみようと思い、Datadog APMで動画のストリームに分散トレーシングが導入できるのか検証してみました。
Datadog APMとは
Datadogの分散トレーシングの製品です。分散トレーシングとは複数のサービス間でリクエストがどのように伝播しているかを可視化する技術です。リクエストがどのサービスにどのような時間で到達しているかを可視化することで、サービス間の関係性を把握したり、通信の遅延やエラーの原因を特定したりするのに役立ちます。
ABEMAの広告システムの一部にDatadog APMを利用しており、マイクロサービス間のリクエストの流れを可視化したり、負荷傾向を分析したりするのに役立てています。
動画のストリーミング
今回は動画のストリーミング方式にHLS(HTTP Live Streaming)を利用しました。
ざっくり説明すると、HLSにはマニフェストファイルという概念があり、Media Playlistと呼ばれるマニフェストファイルにはある程度の長さ(数秒~数十秒程度)で切られた動画ファイル(セグメント)のURIが記述されており、HTTPサーバで提供されます。動画プレーヤーはマニフェストファイルをポーリングし、セグメントファイルを順番に読み込みながら、それらを連続して再生することでストリーミング再生を実現しています。
ここでは *.ts ファイルが上から順に再生されることだけ理解いただければ問題ありません。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:723
#EXT-X-TARGETDURATION:5
#EXT-X-DISCONTINUITY-SEQUENCE:22
#EXTINF:5.000,
main-4223.ts
#EXTINF:5.000,
main-4224.ts
#EXTINF:5.000,
main-4225.ts
#EXTINF:5.000,
main-4226.ts
#EXTINF:5.000,
main-4227.ts
Datadog APMでCM挿入をトレースする
CMの挿入はSCTE-35という規格を利用しています。HLSの場合はセグメントタグに埋め込まれています。
こちらもざっくり説明すると、ここではEXT-X-CUE-OUTがCMの開始、EXT-X-CUE-OUT-CONTがCMの継続、EXT-X-CUE-INがCMの終了を意味しています。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:723
#EXT-X-TARGETDURATION:5
#EXT-X-DISCONTINUITY-SEQUENCE:22
#EXTINF:5.000,
main-4223.ts
#EXT-OATCLS-SCTE35:/DAnAAAAAyiYAP/wBQb+B
#EXT-X-CUE-OUT:15.000
#EXTINF:5.000,
main-4224.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=5.000,Duration=15,SCTE35=/DAnAAAAAyiYAP/wBQb+B
#EXTINF:5.000,
main-4225.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=10.000,Duration=15,SCTE35=/DAnAAAAAyiYAP/wBQb+B
#EXTINF:5.000,
main-4226.ts
#EXT-OATCLS-SCTE35:/DAnAAAAAyiYAP/wBQb+B
#EXT-X-CUE-IN
#EXTINF:5.000,
main-4227.ts
今回のデモのようにHLSを入力にとり、何かしらの処理(コンテンツ内へのCM挿入等)を施して後段へHLSで出力するケースではマニフェストファイルやセグメントファイルが複数のサーバで順次処理されます。複数のサーバを横断で特定のイベントが発生していることや処理に掛かっている時間を把握できると開発時はもちろん、運用時も把握できるとトラブルシューティングに役立つのではないでしょうか。
Media Playlistにトレースのコンテキストを埋め込む
CM挿入に関するイベントをトレースした時の例です。
この情報を複数のサーバ間で引き回せれば分散トレーシングになります。本編の再生を邪魔せずにトレースの情報埋め込みに使えるタグとして、EXT-X-DATERANGEというタグがあります。
RFC 8216 / 4.3.2.7. EXT-X-DATERANGE
このタグは時間の範囲とそれに付随する任意の情報を記述でき、”X-“で始まる要素名は自由に定義して利用できます。例えば、以下のような記述ができます。
X-TRACE-x-datadog-trace-idやX-TRACE-x-datadog-parent-idがDatadog APMのトレース情報です。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:723
#EXT-X-TARGETDURATION:5
#EXT-X-DISCONTINUITY-SEQUENCE:22
#EXTINF:5.000,
main-4223.ts
#EXT-OATCLS-SCTE35:/DAnAAAAAyiYAP/wBQb+B
#EXT-X-CUE-OUT:15.000
#EXT-X-DATERANGE:ID="trace-ZAAAAAAAAA",CLASS="trace-context",START-DATE="2024-12-05T10:00:00Z",X-TRACE-x-datadog-trace-id="100",X-TRACE-x-datadog-parent-id="100"
#EXTINF:5.000,
main-4224.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=5.000,Duration=15,SCTE35=/DAnAAAAAyiYAP/wBQb+B
#EXT-X-DATERANGE:ID="trace-ZAAAAAAAAA",CLASS="trace-context",START-DATE="2024-12-05T10:00:05Z",X-TRACE-x-datadog-trace-id="100",X-TRACE-x-datadog-parent-id="100"
#EXTINF:5.000,
main-4225.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=10.000,Duration=15,SCTE35=/DAnAAAAAyiYAP/wBQb+B
#EXT-X-DATERANGE:ID="trace-ZAAAAAAAAA",CLASS="trace-context",START-DATE="2024-12-05T10:00:10Z",X-TRACE-x-datadog-trace-id="100",X-TRACE-x-datadog-parent-id="100"
#EXTINF:5.000,
main-4226.ts
#EXT-OATCLS-SCTE35:/DAnAAAAAyiYAP/wBQb+B
#EXT-X-CUE-IN
#EXT-X-DATERANGE:ID="trace-ZAAAAAAAAABkAAAAAAAAAA",CLASS="trace-context",START-DATE="2024-12-05T10:00:15Z",X-TRACE-x-datadog-trace-id="100",X-TRACE-x-datadog-parent-id="100"
#EXTINF:5.000,
main-4227.ts
Datadog APMのトレースコンテキストのシリアライズ/デシリアライズ
ここまで来ればあとはトレースコンテキストをどう文字列に変換するかを考えるだけです。
HTTPやgRPCでトレースする場合はヘッダに値を埋めんでいるようだったのでそれらがどのように実装されているか調べてみたところ、Tracerというインターフェースが使えそうです。Injectでコンテキストを外部出力可能な形に変換し、Extractで外部から取得した値をコンテキストに変換するようです。
type Tracer interface {
// Extract extracts a span context from a given carrier. Note that baggage item
// keys will always be lower-cased to maintain consistency. It is impossible to
// maintain the original casing due to MIME header canonicalization standards.
Extract(carrier interface{}) (SpanContext, error)
// Inject injects a span context into the given carrier.
Inject(context SpanContext, carrier interface{}) error
}
gopkg.in/DataDog/dd-trace-go.v1/ddtrace#Tracer
引数のcarrier
がコンテキストを運搬するための実態で、今回はTextMapCarrierを使うのが妥当そうです。
TextMapCarrierは map[string]string
のdefined typeであることから外部に引きまわしたい値をkey value形式で扱えそうです。
// TextMapCarrier allows the use of a regular map[string]string as both TextMapWriter
// and TextMapReader, making it compatible with the provided Propagator.
type TextMapCarrier map[string]string
// Set implements TextMapWriter.
func (c TextMapCarrier) Set(key, val string) {
c[key] = val
}
// ForeachKey conforms to the TextMapReader interface.
func (c TextMapCarrier) ForeachKey(handler func(key, val string) error) error {
for k, v := range c {
if err := handler(k, v); err != nil {
return err
}
}
return nil
}
dd-trace-go/ddtrace/tracer/textmap.go
TextMapCarrierを用いてMedia Playlistをシリアライズ/デシリアライズする処理をカスタマイズすることで任意の場所に上記のセグメントタグを埋めたり、取り出せるようにしました。
コードは長くなってしまったので、サンプルをGistに置きました。テストコードを見てなんとなく使われ方が伝われば幸いです。
https://gist.github.com/kurochan/fd7c68f6046f58ac85e3f2d0548f94a7
さいごに
動画コンテンツ内にCM表現を挿入する技術の検証をするついでにCMのイベントをトレースできないか検証してみた事例を紹介しました。
分散トレーシングはHTTPやgRPC等の通信を主にクライアントとサーバ間やマイクロサービスの可観測性向上の目的に使われることが多い印象がありますが、動画配信に限らず広義のストリーミング処理に対しても応用する余地はたくさんあると考えています。ただし、HTTPやgRPCと比較して用途によってプロトコルやインターフェースが違ったり、ユースケースが違えば観測したい単位も変わってくるため個別の作り込みが必要になりそうなことがわかりました。
今回はHLSに限定しましたが、もっと前段の映像が生成されるところから最終的に視聴するところまでコンテキストを引き回せればEnd-to-Endで可観測性を得ることも可能そうです。他にもトレースできると面白そうなものを見つけたらまたチャレンジしてみようと思います。