AbemaTVのアーキテクチャの変遷

山中勇成氏(以下、山中):こんにちは。このセッションでは「AbemaTVのアーキテクチャの変遷」というタイトルで、AbemaTVが開局前と開局してからいろんな出来事があったんですが、その上でアーキテクチャがどう変わっていったかをご紹介します。

まず自己紹介です。私は山中勇成と言います。

インターネット上では「みゆっき」というハンドルネームで活動していますので、もし気になる方はチェックしてみてください。

私は去年新卒として株式会社サイバーエージェントに入社しまして、AbemaTVのコンテンツ配信チームのStreaming Reliability Engineerという役職です。今ちょうど2年目になるんですけど、配信周りのアーキテクチャ、アプリケーションの開発だったり設計をしています。

仕事もこういう配信をやっているんですが、僕の趣味も「映像オタク」です。

左のようなでっかいカメラを個人で持っていて、「イベントとかに行って配信する」も含めた趣味です。なので仕事と趣味が近い存在になっています。

過去にいくつか登壇していて、去年の本イベントにも登壇していたりするので、興味を持っていただけたらこちらもチェックしてみてください。

本日のアジェンダです。

最初にAbemaTVの歴史を簡単にご紹介します。その後にAbemaTVのアーキテクチャの変遷の本題を、それぞれの期間、開発開始から仮開局、仮開局から本開局、本開局から1周年、1周年から2周年。そして2周年から現在と期間に分けてお話します。

逆に本日話さないことは、メイン以外の細かいアーキテクチャの変遷や、細かい処理の仕組み、クライアントの処理の変遷は、このセッションではお話しません。ほかのセッションや過去の資料で説明されていますので、そちらをご覧ください。

AbemaTVの歴史

そして、さっそくAbemaTVの歴史についてお話していきます。AbemaTVのはじまりです。

これはWikipediaの引用なんですが、テレビ朝日の早河会長と、ウチのサイバーエージェント社長の藤田で設立したのが始まりです。

会社として設立したのが2015年4月1日で、AbemaTVの本開局が2016年4月11日なので、約1年でサービスのリリースを行なったことになります。

AbemaTVの当初からのコンセプトが「無料で観れる」そして「会員登録なし、24時間編成でリニア型の配信を行う」でした。これが今も続いています。

AbemaTVでの出来事を年表にしてみました。

2015年の10月から本格的な開発が始まりました。それから約5ヶ月、2016年の3月には仮開局。仮開局というのは、本開局に先立ちまして一部の機能を先行してリリースいたしました。

4月には本開局。4月11日に本開局したんですが、その翌12日にGCPの全リージョン障害に巻き込まれて障害を起こしてしまうという幸先の悪い感じになってしまいました。

そして2017年の1月には年末年始特番で、AbemaTVはいくつか力を入れている時期があるんですけど、開局してから1番最初の年末年始の特番の期間だったんですけど、このとき、ピーク帯のCM時に視聴できない障害がありました。

その次、2017年4月にようやく開局1周年を迎えまして、ビデオ機能・縦画面をリリースいたしました。2017年5月に、先ほどもいくつかのセッションでお話があったんですけど、「亀田興毅に勝ったら1000万円」という番組で、視聴者が殺到して約1時間半ほどダウンしました。

それから11月には「72時間ホンネテレビ」という超大型の特番をやりまして、こちらは無事配信を行い過去最高視聴率を記録しています。

そして2018年1月、2回目の年末年始なんですけど、無事配信を乗り切りました。4月には開局2周年を迎えてダウンロード・追っかけ再生をリリースしています。そういう感じで現在にいたります。

開発開始から仮開局まで

ここまでざっと概要だったんですけど、この年表にあった順に沿ってどうアーキテクチャが変化していったかをご紹介します。

まずは開発期間。開発開始から仮開局までのあいだの話をします。

仮開局まであと5ヶ月というところで、要件が「収録番組の配信ができることがマスト」という状態でした。

我々が仮開局までに作ったものは、だいたいここに上がっている機能です。

1つ目が番組表。リニア型配信を行う上で番組表はすごい重要な機能です。それからユーザー管理。会員登録は不要ですが、ユーザーという概念はあります。それからシェア機能。開局当時はコメント機能はなくTwitterでシェアをして、視聴のビデオの横にコメントを残す機能でした。

配信周りは収録された番組を変換するためのトランスコーダーと仮開局のときにはNewsとFreshの配信を行っており、こちらのインジェストサーバの開発、そして番組表のリニア型の配信を管理するための編成管理ツールを作っていました。

当初サイバーエージェントとしては、インターネットのサービスを作る知識はたくさんあったんですけど、インターネットでリニア型の配信を行うのは経験したことがなく、今思うといろいろあるんですが最終的には「よくやったな」という印象だったそうです。

技術選定の軌跡

我々が開発を始めて、どう技術を選定していったかを紹介します。

まずプラットフォームです。

オンプレだったりクラウドとかいろいろあると思うんですが、オンプレはコスト以外のメリットがなくて、判断する前に却下になっていました。すでに知見のあるAWSかGCPでプラットフォームを選択しようという話になりまして、結果的に私たちはGCPを選んでいました。

なぜGCPを選んだかという話ですが、大きく4つのメリットがあります。

1つは、Googleが使用しているのと同じレベルの高機能なL7のロードバランサがあります。

(2つ目は)GKE/Kubernetesの環境がある。 Kubernetesで構築するというのは、移行やリソースの面でも大変アドバンテージとなっていて、それをマネージドのサービスとして提供されているのが非常に強みでした。

3つ目が、ネットワーク帯域に対するコストの安さです。少し難しいんですけど、AWSですと当時マシン間の帯域を確保するためにマシンのスペックを上げる必要がありました。マシンのスペックを上げてネットワーク帯域を確保すると、「私たちが使いきれないリソースにお金を払う」「帯域のためにお金を払う」必要があって、GCPですとそういう制約がないので、1つ強みだったかなと思います。とくに動画配信を行う我々にとっては、非常に強いメリットです。

4つ目が、Stackdriver LoggingとかBigQueryのログ収集や集計サービスが非常に優れたサービスがあることがメリットです。

ここまでが選んだメリットで、先ほどの話にもあったとおり、我々はGCPの台湾リージョンを使っています。

ここから裏話です。

トピック的な話ですが、当時、GCPには日本のリージョンは無かったんです。必要があれば後にくるであろう日本リージョンを使おうと思っていたんですけど、開局してみての印象として、開局時から日本と台湾間のインジェストが問題になりました。

生放送で配信するときに打ち上げるネットワークが不安定だったり、また東京リージョンが開始したあとも、台湾から東京に移行するAbemaTV側の人的リソースが割けないということで、今もGCP台湾で運用しております。ここらへんに対する対策とかも、後々に出てきます。

それから言語です。

CyberAgentのバックエンドにおける開発言語は、当時Java→Node.js→Golangと移り変わっておりまして、社内でもいろんなサービスで使われていたり、当時のCTOがGolangを採用しているAWAからきたこともありまして、GolangをAbemaTVでも使うことにしました。

データベースについては、当初はGCPで提供されているCloud DatastoreやCloud Bigtableを使用したかったんですが、先ほどもありましたとおり編成ツールという番組表や契約を管理したり、いろんなバリデーションを臨機応変に、降ってくる要件に対してかけなきゃいけないという状況で、そういうクエリ要件やスキーマに柔軟な変更ができるメリットを活かすためにMongoDBを採用しました。

マイクロサービスアーキテクチャを採用

全体のアーキテクチャの話をします。全体としては、Microservices構成のアーキテクチャを採用しております。

説明するほどでもないと思うんですけど、機能単位でのリリースやスケール、あとはコンテナ化によるnodeに対するリソースの有効活用が可能です。

各サービス間の通信は、gRPCとProtocol Buffersを使用しております。HTTPの実際のエンドポイントがあるサービスと、さらにそれをデータベースにクエリーを投げたりロジックで処理するバックエンドのサービスに分けたGateway系パターンの構成になっています。

ここまではだいたい紹介しましたが、ここからまた裏話的な話で「Microservicesの崩壊」があります。

このときに5ヶ月という非常に短い期間で、例えば、何かしらのデータをデータベースから取りたいときには、クエリーのスキームをprotobufに定義して、protobufをBackend Serviceに取り込み、Backend Serviceでデータベースのクエリーを書きます。

定義したprotobufをGateway Serviceのサービスに取り込んで、Gateway ServiceからBackend Serviceを呼び出すという手順を踏まえる必要があって、リポジトリを3つとか触る必要があります。

当然、投げ出したくなるわけです。

短い期間での開発のため、 クエリが複雑になる編成管理ツールとかインジェストサーバで、図にもあるように、Backend Serviceを触っているところがありながら、データベースにも直接取りにいく構成になってしまいました。

番組表の仕組み

続いて番組表の仕組みです。

番組表はデータベースから番組表を直接取得するのではなく、そのときの状態をスナップショットとしてGCSに保存しています。

これのメリットは、編成途中の番組表がユーザーに表示されるのを防ぐことです。編成の管理画面でタイトルとかを打っているタイミングでユーザーに取られちゃうと、まだ途中や未公開の情報が出てしまうので、それを防ぐためです。

あとはスナップショットとしていくつかバージョン管理をしておくことで、例えば、debug用として特定のバージョンの番組表を実機で観るということが可能になりました。

図にするとこんな感じで、編成管理ツールでGCSにスナップショットを書き出して、クライアントはGateway経由で特定のスナップショットのバージョンを取得する感じになっています。

続いて配信です。配信はHLSで配信するサーバを開発しています。

よくTVで、ジャストインタイムでリアルタイムの変換をしているんですけど、AbemaTVの場合はあらかじめ変換された動画を配信する構成になっています。

収録番組は納品された映像をトランスコーダーで、あらかじめABRとパッケージングを行います。配信に必要なメタデータと動画セグメントをGCSに保存して、それをMedia Proxyと社内で呼んでいますけど、Media ProxyがHLSの形式で出力する構成になっています。

生番組の配信

続いて生番組の配信です。

先ほど説明したとおり、仮開局の時点でニュースとFRESHを配信する必要がありました。そのために、我々が選んだのはHLSを中間の言語としてHLSを取得して、それを1回データ化して、さらにHLSで配信する構成になりました。

HLSを取得するインジェストサーバ(Watchman)を開発しました。Watchmanの機能は何かと言うと、HLSで取得した動画のセグメント情報をMongoに保存するのと、動画セグメントのデータをGCSに保存する。Media Proxyはそれらを配信するような仕組みです。

もう少し、生番組の配信アーキテクチャを詳しくご紹介します。

Newsという単語が出ておりますように、テレビ朝日の本社内から24時間配信するものです。テレビ朝日の本社内に専用のハードウェアを設置し、テレビ朝日の本社がある六本木でABRのエンコードとHLSパッケージングを行なっております。HLSのパッケージングをしたものを、GCP台湾からHLSを取得する仕組みになっています。

続いてFreshは、当時はAbemaFreshという名前でしたが、Freshの一部の番組を配信する仕組みになります。

こちらはFreshの配信基盤をそのまま流用しておりまして、配信サーバに対して、1視聴者と同じようにHLSをWatchmanから取得しています。

以上が開発開始から仮開局です。仮開局までの当時はこれだけのサービスがありました。

この期間はMicroservicesアーキテクチャでデータストアを全サービスで共通のMongoやGCSを使用しており、中にはMicroservicesアーキテクチャから抜けてデータベースを直接叩くサービスがあったというのが、ここまでのまとめになります。

本開局に至るまで

続いて仮開局から本開局までの話をします。

仮開局から本開局までは約1ヶ月だったんですが、ここでの要件が生番組の配信と広告、コメントができることがマストでした。

私たちが本開局までに作ったものが、生番組の配信と広告配信の機能とコメントの機能です。ここからいくつかピックアップして紹介していきます。

まず生番組の配信です。NewsとFreshとは違ったSpotという放送の形態があります。

Spotは任意の場所から特定の時間だけ配信します。TVでいう普通の生放送と同じだと思うんですけど、任意の場所から特定の番組の時間だけ配信する方式です。

こちらは任意の場所からRTMPで送出して、GCP台湾に設置したストリーミングサーバでABRのエンコードとHLSパッケージングを行なっております。図にある通りStreaming ServerでRTMPからHLSに変換して、それをWatchmanが取りにいく仕組みになっております。

生番組の配信がいろいろ出てきたので、本開局後の配信アーキテクチャをまとめると、こんな感じになっております。

配信の種類が収録番組とNewsとSpot、Freshの4種類ありまして、収録番組はトランスコードされた動画をメタデータと動画セグメントをGCSに保存しています。それぞれ3種類の生放送に関しては、 WatchmanがHLSを取得してメタデータをMongoDBに保存。動画セグメントをGCSに書き込む構成になっておりました。

広告の配信について

続いて広告配信になります。広告の配信は、AbemaTVの広告局が用意したAdサーバと連携していました。

番組表のスケジュールに合わせて必要な広告をAdサーバに問い合わせて、動画ファイル自体はあらかじえめトランスコードされた動画がGCSに入っていますので、こちらをMedia Proxyという名前の通りProxyして配信するような仕組みになっています。

しかし、広告挿入のタイミングが収録番組と生番組でそれぞれ違いがあります。

収録番組はフィックスタイムの広告で、あらかじめ編成ツールで決めた時間に広告を挿入します。なので、こちらはあまり問題ありません。

生番組に関してはアンタイムの広告でして、広告を挿入する時間が未確定です。何かしらの形式で広告の挿入タイミングを送る必要がありました。そこで我々が取ったのが、Watchmanが取得するHLSにSCTE-35と呼ばれる形式でCMのタイミングを挿入することでした。

SCTE-35を軽くご紹介します。

HLSがこんな感じの出力だと思いますが、こういう1つ目2つ目3つ目のセグメントは番組のセグメントなんですけど、4つ目に#EXT-X-CUE-OUTというタグがありました。これがSCTE-35のタグになります。ここから60秒。このサンプルだと60秒のあいだは広告ですというHLSのタグを加えることにより、広告を挿入しています。

生番組における広告挿入

これを実際にどういう仕組みで運用したかと言うと、Newsの場合はTVと同じAPSと言われる番組管理装置がありまして、ここで時間が管理されています。

踏み入った話になりますが、SDIという映像の信号にSCTE-104の形式でキュー信号を多重化していまして、ハードウェアのエンコーダとパッケージャーが解釈しまして、HLSに先ほどのタグを挿入する仕組みになっています。

Hardware EncoderがSCTE-104のキューのタイミングでキーフレームを打つので、フレーム精度で挿入ができます。

続いて生番組の広告挿入のSpot生放送です。

配信管理ツールというのがありまして、配信管理ツールからRTMPでCMタイミングを送信しています。図にありますWowza Streaming Engineの拡張モジュールを開発していまして、RTMPでタイミングの信号がきたときにHLSにタグを挿入するかたちになっています。

配信管理ツールは映像が映ります。RTMPで通信していて、その信号を使ってCMボタンを押すと入る仕組みになっています。

本開局の裏話として、ターゲティング広告の話があります。

開局当初は、CMをパーソナライズできる設計でした。プレイリストはオリジンに直接、動画セグメントだけはCDN経由で配信する構成になっていました。

仮開局から本開局のまとめになります。

生番組はNews・Fresh・Spotの3種類。生番組ではリアルタイムにCMを挿入することが可能でした。ただ、プレイリストはCDNを通さず直接配信をしていました。

本開局から1周年たった頃の話

続いて本開局から1周年の話になります。

本開局後でも、やることはまだ山のように残っていました。動画セグメントのキャッシュやログ、メトリクスの可視化、検索、開発環境の整備などがありました。

ここからまたいくつかピックアップしてご紹介します。まず動画セグメントのキャッシュです。

課題として、先ほどの構成では生番組の動画セグメントをGCSを使用して書き込みと配信をしていたんですが、手を抜いたところがありまして。GCSが単一障害点になるとか、GCSへの書き込めなかったときに生番組が配信できないことがあるので、GCS以外に生番組の動画セグメントをキャッシュするようにしました。

実際のアーキテクチャとしては、Watchmanで取得した動画セグメントを一時ストレージとしてGCSのほかに、Redisにも保存するようにしました。

続いてログ改善です。当時AbemaTVではfluentdを使用していましたが、Golangでの扱いづらさや、fluentd自体のサーバー、リソースを管理する必要がありました。

それを解決するためにCloud Pub/SubとBIgQueryを使用して置き換えました。

実際は各ゲートウェイでCloud Pub/Subにログを流して、それをBigQueryにInsertするBatch Inserterを開発して、各ゲートウェイが投げたアクセスログなどをBigQueryに蓄積するかたちになりました。

もう1つのメリットとして、ログ解析チームなど、データ解析の基盤があるんですけど、そちらは例えば「このログ欲しい」と言うときに、Cloud Pub/SubでSubscribeしてもらえれば、勝手に取れるような副次的なメリットもありました。

続いてメトリクスの可視化です。当時、Stackdriver Monitoringを使用しており、各サービスの詳細なメトリクスやカスタムメトリクスを可視化したいということで、PrometheusとGrafanaでモニタリング環境を構築しました。

アーキテクチャ的にはこのような感じで、Grafanaはエンドポイントとしてあって、prometheusをgrpcやredisの細かなサービスごとに分けて1つの大きなprometheusサーバーとしていました。実際にこんな感じのメトリクスになっています。

開局依頼初の年末年始

ということで、開局以来初めての年末年始がやってきました。年末年始は、先ほどもあったとおり障害が起きてしまいます。大晦日の夜CMに入ったタイミングで再生ができなくなる視聴障害がありました。

ピーク帯の視聴者数同時接続数の多い時間に発生しました。とりあえず暫定対処は行ったんですけど、1月2日の夜にも視聴数が見込まれる番組があるので、なんとしても対応する必要性があるという感じでした。

そこで我々が年末年始の対応が2つあります。

1つは、広告サーバへの問い合わせをキャッシュしました。

原因の1つとして、配信サーバから広告サーバへの問い合わせが詰まっており、広告サーバからの結果をキャッシュしています。

2つ目は、Redis Clusterのスケールアウトを行いました。

同時接続数が多いので、広告やコメントのキャッシュによって負荷がかかっておりまして、Redis Clusterを当時見積もった6倍にスケールアウトしようと思ったのですが、redis-trib.rb(3.0.6)にバグがあり、スケールができない状態でした。

これは年末なんですけど、我々が取った行動は1月1日に検証して、1月2日にRedis Clusterのバージョンアップを実施する状態でした。なんとか1月2日の夜の番組には無事間に合って配信することができました。

本開局から1周年のまとめをしますと、最初は環境の整備や改善を行ってきました。年末年始のピーク帯に障害が発生しましたが、その後2日でRedisのバージョンアップを実施しました。

1周年から2周年にかけて

1周年から2周年の話になります。

ここで行ったのは配信品質の向上です。日本と台湾間のインジェスト安定化と、CMキューの送信をRTMPからHTTPに変えました。

日本と台湾間のインジェスト安定化は、GCP台湾までのネットワークレイテンシが大きく、RTMPのコネクションが切れることがあるので、伝送プロトコルをUDPベースのZixiへ変えることにしました。

実際には、途中ストリーミングサーバーとエンコーダの間にZixiというプロトコルと、それを変換するサーバーがいる感じになっています。

あとCMキューの送信をRTMPで送信していたんですけど、RTMPのコネクションが切れると配信にCMが入らないことがあったので、HTTPでCMキューを送信するようにしております。

今まで若干手を抜いていたのを、HTTPを使ってちゃんと送信するようにしました。

我々の大きなターニングポイントとなったのが「亀田興毅に勝ったら1000万円」という番組です。

ニュースでも取り上げられていたんですが、視聴者が殺到し、第一試合が始まる前にサーバーがKOされてしまいました。

実際に、障害時にはお祭りのような騒ぎになってまして、アラートのOn Callを受け取ったんですが、根本の原因を探るのに時間を要していました。

その障害の原因としては、mongosのコネクションが滞留していたので、暫定対処としては再起動で解決しました。

これらの反省としまして、共有のリソース(MongoDBやRedis)が1種類しかないので、それらに依存していたことでした。解決策はデータストアの分離と巨大なサービスの分離でした。

データストアの分離は、共有のMongoDBやRedis Clusterにユーザー情報とか配信に必要な情報、ユーザーからのリクエストと配信と、それぞれ一緒に保存していたので、ユーザー、コメント、配信など、それぞれのドメインごとに分けてデータストアを持つような設計にしました。

巨大なサービスの分離も行っており、mediaという名前のサービスがなんでも扱う大きなサービスになっていたので、こちらも細かく分解・配信に関係ある部分とそうでない部分で分けていきました。

「72時間ホンネテレビ」の特別対応

そして「72時間ホンネテレビ」がやってきます。

エンジニアが出演情報を知ったのは一般発表と同じ9月末で。年末年始への負荷対策を行っていたんですけども、この「72時間ホンネテレビ」に向けて特別対応をすることにしました。

基本的にやったことはオリジンの負荷を下げることです。今まではスケールアウトでキャパシティを増やしてきましたが、水平スケールでは想定される視聴数では対応できない見積もりになっていました。AkamaiやCloud CDNを使用してキャッシュ化を行っています。

大まかにまとめると、番組表のデータはCloud CDNを使用して配信して、プレイリスト動画のファイルをAkamaiを使用して配信していました。この時点で完全に開発、当初からありましたターゲティング広告の設計を無くしました。

無事その甲斐もありまして「72時間ホンネテレビ」では事故なしで配信することができまして、障害が発生しないことが記事になりました。

メデタシメデタシということで、年末年始もうまくいきました。

1周年から2周年のまとめをしますと、亀田特番をきっかけにシステムの見直しを行い、別サービスへ影響を及ぼさない構成にしました。「72時間ホンネテレビ」対策で大規模改修を加えて、見事年末年始も無事に配信することができました。

2周年から今日まで

2周年から現在の話をします。

2周年目以降は、開発当初から行いたかったターゲティング広告を開始しました。

仕組みとしてはユーザーをいくつかのセグメントに分けて、図にある通りChannel AのSegment 1とかChannel AのSegment 2とか、チャンネルとセグメントのパターン分プレイリストがある感じなんですが、分けて配信を行っています。

それから現在までに大きなアーキテクチャ変更はなく、去年までの負荷対策で現状のアーキテクチャで安定的な稼働を取れる構成になりました。

今やっていることは、将来に向けてデータフローの改善だったり、MAMやプレイアウト周りなどのシステムの評価検討とか、映像配信品質。そもそもの低遅延とか高画質などです。

あと当初からチャンネルごとに独立したリソースにしようとしていまして、チャンネルごとに独立データストアを持つことによって、影響範囲を下げることを目標としています。

長くなったんですけど、ここまでの発表のまとめを行います。

開局当初は、インターネットでリニア型の配信を行う知見が我々に少なく、最適なアーキテクチャを作ることはできませんでした。

しかし現在までの改修で、現行のアーキテクチャで出せるほぼ最大のキャパシティが作れる構成になりました。2周年以降は大きなアーキテクチャの変更はなく、将来を見据えてどういうアーキテクチャにしていけばいいかを模索している状態になります。

まだまだこれから成長していくAbemaTVですが、人を募集しておりますので、よろしくお願いします。

以上になります。ありがとうございました。