ホンネテレビの負荷対策-配信編

この記事は、[AbemaTV Advent Calendar 2017](https://adventar.org/calendars/2216) の25日目の記事です。
今年、サイバーエージェントに新卒入社し、AbemaTVの配信チームに所属している @miyukki です。

このアドベントカレンダーも最終日になりました。11月末まで誰もやる気配がなかったのですが、社内で声を上げて広めた結果、多くの人が書いてくれてありがたいです。なんでも気軽に自由に発言できるのもこの職場の良いところです。

# 72時間ホンネテレビ

さて本題に戻り、AbemaTVでは11月2日-5日にかけて、[稲垣・草彅・香取 3人でインターネットはじめます『72時間ホンネテレビ』](https://abema.tv/channels/abema-special/slots/AkkG6KZAFkUszf)（以下、ホンネテレビ）を放送しました。

<!-- ![w700c-ez_f9f33b351336bd83242df10d70bf3898088e9fd0c64759e5.jpg](https://qiita-image-store.s3.amazonaws.com/0/33754/89cb1806-ebb4-91d9-4be9-dd8bbac9a937.jpeg) -->

言い出しづらいのですが、過去には[「亀田興毅に勝ったら1000万円」AbemaTV史上最多アクセスでサーバダウン](http://www.itmedia.co.jp/news/articles/1705/08/news074.html)と記事になるなど、急激なアクセス増加でサービスを提供できなかった例がありました。今回の72時間ホンネテレビでは、負荷対策プロジェクトを立ち上げ、フロントエンドからバックエンドまで一丸となって負荷対策に当たりました。

# 負荷対策について

負荷対策プロジェクトでは、このアドベントカレンダーの1日目に柿島が [ホンネテレビの負荷対策プロマネのホンネ](https://developers.cyberagent.co.jp/blog/archives/12275/) で書いている6つの方針に則って対策を行いました。

> - キャッシュ可能なもの、または改修によってキャッシュが可能になるものはCDNからレスポンスを返すように変更
> - CDNでのキャッシュヒット率を向上させる
> - 不要なリクエストの廃止
> - 不要なタイミングでのリクエストを適切なタイミングに切り替える
> - 短いポーリング間隔でのリクエストは、間隔を調整
> - 別のシステムに切り離せるものは、切り離してオフロード

配信では、映像をリアルタイムで配信するという特性上、不要なリクエストや間隔を伸ばせるリクエストは存在しません。（端末側の実装不備は除く）

上記の負荷対策の方針のうち、配信側で実施できるのは以下の3点になりました。

- **キャッシュ可能なもの、または改修によってキャッシュが可能になるものはCDNからレスポンスを返すように変更**
    - HLSやMPEG-DASHのプレイリストをCDNから返す
- **CDNでのキャッシュヒット率を向上させる**
    - プレイリスト、セグメントのキャッシュヒット率の調整を行う
- **別のシステムに切り離せるものは、切り離してオフロード**
    - 配信で利用するDBをユーザーアクセスに関連する処理から分離する

この記事では、私が担当したCDNを使った負荷対策についてお話します。

# AbemaTVの配信の仕組み

負荷対策の話をする前に、まずはAbemaTVの配信の仕組みを簡単に説明します。

*※ AbemaTVでは、Abemaビデオなどのサービスもありますが、ここではリニア配信（番組表に基づいた24時間配信）のことに焦点を絞ります。*

AbemaTVでは、20-30チャンネルを24時間配信しています。これを配信している配信サーバー（社内ではMedia Proxyと呼ばれている）は、現在の番組表の情報を常に持っており、リクエストに応じて該当チャンネルの現在再生すべきコンテンツを返します。

![Pasted image at 2017_12_25 03_18 PM.png](https://qiita-image-store.s3.amazonaws.com/0/33754/3907c7c5-a1c0-bc7b-9bac-40d0ddf195f0.png)

配信では、主にプレイリストとセグメントの2種類のファイルを配信します。プレイリストは、再生するべきセグメントファイルを記述したもので、セグメントファイルが動画の実体となります。

AbemaTVでは、今のところHLSとMPEG-DASHの2種類の配信を行っています。この記事では主にHLSのことについてお話します。

## HLS

HLSの基本的な仕様等については、多くの記事がインターネットにあるので、ここでは割愛します。

ライブ形式のHLSでは、プレイリストが返したセグメントの順序を識別するために、最初のセグメントの順序を[`#EXT-X-MEDIA-SEQUENCE`](https://tools.ietf.org/html/rfc8216#section-4.3.3.2)（以下、メディアシーケンス）タグを用いて表します。

例えば、最初に取得した時のプレイリストが以下だった場合、`0.ts`が0番目、`1.ts`が1番目、`2.ts`が2番目ということがわかります。

```playlist.m3u8
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:2,
0.ts
#EXTINF:2,
1.ts
#EXTINF:2,
2.ts
```

次に取得したプレイリストが以下だった場合、`1.ts`が1番目、`2.ts`が2番目、`3.ts`が3番目ということがわかります。

```playlist.m3u8
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:2,
1.ts
#EXTINF:2,
2.ts
#EXTINF:2,
3.ts
```

その次に取得したプレイリストが以下だった場合、理想としては`2.ts`が一番最初のセグメントとして来て欲しいところですが、ネットワークの遅延かサーバーの処理の影響か多少時間が空いてしまったようです。
しかし、メディアシーケンスのおかげで最初のセグメントの順序が3番目であることがわかるため、`3.ts`が3番目、`4.ts`が4番目、`5.ts`が5番目ということがわかります。

```playlist.m3u8
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:3
#EXTINF:2,
3.ts
#EXTINF:2,
4.ts
#EXTINF:2,
5.ts
```

今回の例では、分かりやすいようメディアシーケンスとセグメントのパスを一緒にしましたが、これが複雑なパスや変化するパスの場合でも動作することができます。

# 配信のCDN化

## いままでのコンテンツの配信
インターネットで動画配信をするにおいてCDNは欠かせません。

ここから本題なのですが、AbemaTVでは既にCDNを使用していましたが、CDNを経由するのはセグメントのみで、プレイリストはMedia Proxyでリクエスト毎に処理していました。

理由は、AbemaTVではユーザーやデバイス別でのCMやフィラー（代替映像）の映像を切り替えを行うため、全てのリクエストに対して同じコンテンツを返すことができない経緯がありました。

ユーザー別のプレリストファイルをHLSで実現するためには、メディアシーケンスを返す必要があるため、`ユーザーID x チャンネルID`をキーとしてユーザーがどこのセグメントまでを再生したかのセッション情報（以下、デバイスセッション）を保持しており、リクエスト毎にRedisにアクセスしていました。

これにより、2人のユーザーが同じチャンネルを視聴していても、映像は同じでもプレイリストの内容（主にメディアシーケンス）は異なる状況になります。

![Pasted image at 2017_12_25 04_09 PM.png](https://qiita-image-store.s3.amazonaws.com/0/33754/a5e0e6d7-01e2-a01a-f0be-4ceda0930dcb.png)

## コンテンツの統一化

CDNを使用するためには、ユーザー毎にコンテンツを出し分けていては、効果的ではありません。

そのため、ユーザーやデバイス毎に出し分けていたコンテンツを、コンテンツが変わる要素のグループごとに出し分けることにしました。

コンテンツが変わる要素を以下のように洗い出しました。
・デバイス種別（ブラウザ種別）
・DRM技術
・コーデック
*※ここでは主な3種類の要素をあげる。*

プレイリストのアクセスの際に、これらのコンテンツが変わる要素のパラメーターを付け、CDNでは指定したパラメーター別でコンテンツをキャッシュすれば、キャッシュパターンはとても少なくなります。

例えば、以下のようなリクエストのURLになります。
`playlist.m3u8?device=chrome&drm=widevine&codec=h264`
`playlist.m3u8?device=ios&drm=fairplay&codec=h264`

## メディアシーケンスの計算

以前の設計では、`ユーザーID x チャンネルID`をキーとして保持していたデバイスセッションを用いてメディアシーケンスを計算していましたが、ユーザーごとの出し分けではなく、`デバイス種別 x DRM技術 x コーデック`をキーとしてどこのセグメントまでを再生したかのセッション情報（以下、チャンネルセッション）を用いて、メディアシーケンスを計算します。

Media Proxyがユーザーからのプレイリストのリクエストに対して、内部で持っている「現在の再生すべきコンテンツ」を取得します。
チャンネルセッションをRedisに問い合わせ、そのチャンネルセッションと比較して、メディアシーケンスを計算します。
チャンネルセッションに更新があれば、Redisのチャンネルセッションの更新を行ってから、ユーザーにプレイリストを返します。
![Pasted image at 2017_12_25 08_12 PM.png](https://qiita-image-store.s3.amazonaws.com/0/33754/86df2934-3900-96dc-0dd5-96fc1b00ba35.png)

また、今まで`ユーザーID x チャンネルID`のハッシュをキーとしていたのが`デバイス種別 x DRM技術 x コーデック`のハッシュキーになると、キーの数が少なくなりホットキーが生まれます。そのため、Media Proxyの内部で一定時間でチャンネルセッションのキャッシュを持つようにしています。

さらに、ここではMedia Proxyが1台であることを前提に書きましたが、Media Proxyはk8s上に構成されており、数十台のpod上で動作しています。これらが協調して動作し、数台おかしなプレイリストを持ったpodがいても、正常に配信できるような仕組みも実装しました。

## CDNのチューニング

負荷対策を行うにあたり、CDNのパラメーターについても以下の項目を調整しました。
*※AbemaTVでは、AkamaiのAMDを使用しているため、特有のパラメーターもあるかもしれません。*

- キャッシュキー
    - キャッシュ基準のURLやパラメーター
    - ホワイトリスト方式のパラメーターでない場合、故意に適当なパラメーターをつけた場合キャッシュヒット率が低下する
- キャッシュ時間
    - プレイリストは一定時間で更新するため、数秒の指定
    - セグメントは恒久的に変わることがないが、リニアコンテンツは直近でしかアクセスされないため、数分の指定
- ネガティブキャッシュ時間
    - サーバーが何らかの原因でエラーを返した場合のキャッシュ時間
    - 短くすると負荷につながり、長くするとリニア型配信では視聴に影響があるため、AbemaTVでは通常のキャッシュ時間と同じに指定
- Make Public Early
    - エッジサーバーでキャッシュヒットしておらず、オリジン（若しくは親エッジ）に問い合わせている間に来たリクエストに対して、既に行われている問い合わせを待つか待たないか
    - AbemaTVではなるべく負荷を減らしたいため、有効にしている

また、AbemaTVでは日本国外からのアクセスを規制していますが、日本国内から見ようとしているユーザーのAkamaiのエッジが日本国内である保証はないため、Akamaiからのアクセスは無条件で許可し、AkamaiのGEO Filterで制限するようにしています。

# まとめ

駆け足でしたが、72時間ホンネテレビを支えた配信周りの負荷対策についてちょっとでも理解していただけたら、幸いです。

クリスマス当日に記事を書き始めたため、本当は詳しく解説したいことが盛り沢山なのですが、このままだとクリスマスが終わってしまいそうなため、ここらへんでまとめたいと思います。

また、余裕があったらどこかで解説したいと思います。

## 宣伝1

この記事を見てピンときた方！ちょっと動画に興味があるかもと思った方！是非、Wantedlyからご連絡ください！
https://wantedly.com/companies/abema

## 宣伝2

また、会社とは超えた枠組みで、ストリーミングのコミュニティも作ろうと思っているので、興味のある方はリプライ等でご連絡ください。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">【ゆるぼ】動画やライブ配信の技術や開発に関するコミュニティ/勉強会を作ろうと思うのですが、動画やライブ配信に興味のある方、知見のある方</p>&mdash; みゆっき (@toriimiyukki) <a href="https://twitter.com/toriimiyukki/status/931510675523776512?ref_src=twsrc%5Etfw">2017年11月17日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>