DMM.comの、一番深くておもしろいトコロ。

エンコーダー刷新とマネジメントシステム

エンコーダー刷新とマネジメントシステム

  • このエントリーをはてなブックマークに追加

序文

この記事は、動画配信事業部 配信基盤チーム連載の6本目です。
担当は配信基盤チームの向山です。以前この連載とは別内容でキーボードの記事も書かせていただきました。

2019年に刷新したエンコードシステムについてのざっくりとした話と、移行にまつわるあれこれをお伝えできればと思います。
実はチームアイコンの絵を描いたり、今回のシステムロゴの考案・作成をしたりもしています。

目次記事はこちらです。 inside.dmm.com

TL;DR

  • ぬくもり運用のエンコードシステムを自動化し、フロー化
  • エンコードシステムのモジュール化を達成。エンコード速度を超高速化
  • Go と protocol buffersを主軸に、GAE、CloudSQLを利用

従来のエンコードシステムを振り返る

f:id:yanoshi:20171208130313j:plain
今はもうなくなってしまったエンコードルーム

↑の写真は実際に運用していたエンコードサーバーたちです。さて、簡単に振り返ってみましょう。

もともとは1998年から稼働し、アップデートや追加を繰り返して、前世代の自動エンコードシステムが2012年に実装されました。
最も多い時で、300台もの自作マシンたちが動いていました。 長い間稼働しているシステムなので、専用の処理 がどんどん増えていきます。
この時はこの手順で、その時はこの手順しないで、などよくあることですね。
エンコードは自動化されていたものの、ファイルのパッケージング作業やファイルの設置作業は手動で行われていました。

エンコードの複雑化、特にHQ(High Quality)VRにより、納品からエンコード完了までの作業時間が長くなっていました。

HQVRの場合、既存のエンコーダでは0.x ~ 0.0xfps程度でエンコードされていたため、30分のコンテンツ(108,000フレーム)を処理するのにエンコードのみでも1,350,000秒(=15日程度)を要することもありました。
途中で不測の事態が発生した時は、エンコードをやりなおさなければなりません。最低でも同じ日数がかかるので、最悪のケースを想定すると 30日程度必要 な計算に……。

エンコードの規模感を振り返る

これまで動画サービスで公開してきた毎月のコンテンツ数をグラフにしました。

f:id:yanoshi:20200130200652p:plain
月ごとの公開コンテンツ数

初期の頃は数百コンテンツ/月程度が公開されていましたが、ここ数年は概ね2000コンテンツ/月程度といった感じですね。とんでもない数です!
また、弊社の特徴に マルチデバイス対応 があります。
PCだけでなくスマホ(iOS, Android)やVR機器(PSVR, Oculus他)、TVなど様々な場面で楽しんでいただくべく展開してまいりましたが、機器によって搭載されているデコーダーが違う……
ということで、それぞれに最適化した動画ファイルを用意するために、1コンテンツについて複数のエンコードを走らせ、適切に配置する必要があります。
そんな大変なことを、これまではわりとぬくもりで運用してきました。(もちろんある程度は自動化されていました)

配信対応デバイスリストは販売ページにもありますが、主に以下になります(執筆時 2020/02)

  • PC
  • iOS
    • iPhone
    • iPadOS
  • Android
  • PS4
  • PS Vita
  • TV
  • Android TV
  • Fire TV
  • PC VR
    • Gear VR
    • Oculus GO
    • Oculus Quest
  • PS VR

これだけあるとストレージの話とか、ネットワークの話とかも気になりますが、今回はこれだけの数のエンコードをどう整えていったのかを記していきます。

なぜエンコードシステムを移行するのか

前述したとおりこの大規模のシステムを改善しないと、コンテンツの増加やVRなどの高解像度に対応できなくなってしまうことが目に見えていました。
チームで考えた理想のエンコードシステムは以下です。

  • エンコード速度改善
  • モジュール化による小さいプロダクト化。それによる改善速度向上
  • 動画ファイルの必要に応じたリソースの追加・削除。またその自動化
  • 今までの作業フローを自動化して関係者全員を幸せに

また金沢の事業所統合(現事業所の新設統合)も決まっていたため、引越し日以降これまで使用していたエンコードルームが使えなくなるなどの事情もありました。
刷新するならこのタイミングだ、ということで、これらを達成するためのシステム構成を考えました。

作業を整理してみる

ここで今回のエンコードシステムが責任を持つべき作業を整理してみます。

  • 動画ファイルを受け取る
  • 配信できる形式にエンコードする
  • 配信用のストレージに配置する
    • 必要に応じて 専用の処理 を施す

これらのことを分割し、組み合わせて使えるようにしたらシステムとしてはバッチリですね!

新エンコードシステムを設計する

雑な分析をしましたが、もう少し処理を切り分けて、それぞれを モジュール化 する設計にしました。
モジュール化する理由は単純明快で、必要に応じてスケールイン/アウトしやすくするためです。
また、モジュールごとの更新が可能になるため、従来よりもアップデートが容易になります。
新システムのモジュール分けを考えた構成図が以下になります。こちらの図は以前社内ビアバッシュ発表時に作成したものです。

f:id:yanoshi:20200304184055p:plain
エンコードシステムイメージ

Managerが各モジュールに仕事を割り振るのが基本的な設計方針です。

高速エンコードの肝

ざっくりとした話になりますが、高速化の肝はファイル分割にあります。
元ファイルを適切に分割して小さくした後、それをエンコードサーバーにばらまき、完了したら結合するといったものです。

f:id:yanoshi:20200304184215p:plain
エンコードの肝

命名コンペ

f:id:yanoshi:20200130200357p:plain

余談ですが、今回の刷新にあたりチームで命名コンペをしました。
呼びやすい名前を早期に付けておくのはプロダクトの運用で大事な要素だと思っています。
大量の候補を出して、そこから厳選した案でアンケートした結果がこちらです。

f:id:yanoshi:20200304184331p:plain
命名コンペ

  • version 2 なので2に関連するなにか(→ 二郎)
  • マシマシにできるという点にScallabilityの特性が込められているゾ
  • 「自動エンコーダー」と「二郎エンコーダー」で語感が極めて近い

こうして決定した名前です。これからよろしくJIRO!

JIROは JIRO Is Reliability-Oriented encoder というスローガンを掲げ、大量の動画コンテンツのエンコードに対する信頼性を高くすることを一つの大きな目標にして作っていくことになりました。

Manager設計

私自身は各モジュールに仕事を割り振る Manager の作成を担当したので、その際に考えたことを書いていきます。
Managerはこれまで書いてきた、モジュール分けされている各システムを作業ごとに使うシステムです。
UNIX的な考え方で、各モジュールが機能を実装し、それをパイプで呼び出すようなイメージというと分かりやすいでしょうか。
実現するためにワークフロー、ジョブ、タスクという概念を作成しました。

概念 説明
ワークフロー 通常エンコード、特殊エンコードなど作業粒度。次にどのモジュールにタスクを発行するかをテーブルに定義しておく
ジョブ コンテンツ投入時に紐づけるManager内で唯一のID。このIDでリレーションする
タスク 各モジュールで実行する単位

f:id:yanoshi:20200304184407p:plain
基本的な動作設計

コンテンツの投入は、これまでコンテンツのお世話をしていたチームにお願いしています。 Webサイト運営部 映像制作チームという、石川で弊社のコンテンツやクオリティの高さを管理している非常に重要なチームです。
コンテンツの投入はもともと、ある程度自動化されています。

今回は投入時のフロー選択をすれば、公開処理済みのファイルが配置されるのを待つだけになるようにしました。

Managerが絶対に担保したいことは以下です。

  • タスクを多重発行しない
  • タスクを消失しない

タスクが正常に終わったのか、次の処理に進んで良いのかを判定するために、上記の要件は必要です。

また大量のデータを扱うため、当然ながらモジュール内での処理以外の要因(ネットワークの瞬断など)でエラーとなってしまうことは容易に考えられます。
そのため、自動で数回リトライできるような仕組みが必要です。

技術選定

いろいろと書いてきましたが、Managerがやることはかなりシンプルなため、フルスタックなフレームワークに乗る必要がないと判断しました。
また、チーム内で使われているPCが macOSWindows とバラバラなため、双方の環境構築、動作がそれなりに容易なものを選ぼうと思いました。

モジュールのインスタンス数を増やすとリクエスト数も単純に増加するため、Manager自体もスケールアウトできるようなもの(逆も然り)でなるべく意識をしたくない、ということで AppEngine の利用をこの段階でほぼ決定し、開発言語は Go にしました。
文法がシンプルで、良い意味でコードが均一化するため、チーム開発の際に向いていると判断しました。
標準ライブラリの net/http の機能で十分です!

各モジュールとのやりとりをAPIで提供するのですが、Goではstructを定義してjsonエンコード/デコードをしなければなりません。
スキーマには protocol bufferes を採用し、生成した定義を利用する形にしました。structを自動で生成してくれるので便利なほか、リプレイス時に別の言語を利用することも容易になります。

ジョブやタスクについては、我々以外のチームにも共有するためきちんとデータを保存しておく必要があります。
RDBの MySQL を利用することにしました。
同じタスクが多重実行されないことを確認するため、同時リクエストを試しました。
その結果、トランザクション分離レベルをデフォルトレベルの REPEATABLE READ から SERIALIZABLE にすることにしました。
パフォーマンスは下がりますが、確実に処理を進めたいので妥当な選択かと思います。

システム構成

Managerの要求は以下になります。

  • モジュール増減によるリクエスト数の変化によるスケールアウト/インの管理に手間取りたくない
  • 常に動いていてほしいので安定稼働してほしい
  • 開発に集中したい
    • 構築に手間取りたくない
  • 問題があれば容易にロールバックできるようにしたい

など、運用面をある程度考慮しておきたいと考えていました。

ManagerのシステムはシンプルにAPIサーバーとDBがあれば良いので、今回はGCPで AppEngineCloudSQL を利用することにしました。

f:id:yanoshi:20200304184455p:plain
実際のざっくりとした図

実際はCloud KMSを利用したり、他チームが管理している内製CMS (それについてはこちら) のAPIを叩いたりしているのですが、ごちゃごちゃするので省いています。

Manager実装

まずよく使うビルドやテストコマンドは Makefile にまとめておきます。小さな自動化ですが、後に使えるのでやっておきましょう。 Write once, run anywhereです。
make 採用の理由は1リポジトリで完結できるため、ローカルから小さく始められるからです。

ルーティング周りはシンプルに net/http を利用しているので割愛します。初期化して、設定を読み込んで各エンドポイントの設定をしているだけです。

ステートを管理する必要があるので、ワークフローごとの遷移の定義をするテーブルをDB上に作成しました。
対応するワークフロー番号やタスクステータスなどの定数は protocol buffers なファイルに以下のように記述していきます。

message Workflow {
    enum state {
        COMMON = 0;
        NORMAL_2D = 1;
        ...
    }
}

いくつかコードを書いた結果、今回のエンドポイントの処理内容は以下の手順にまとめられることが分かったので、interfaceを定義しました。

  1. リクエストが正常か判定
  2. データ登録や更新などの処理
  3. ワークフローごとの処理
  4. 後処理
// Process エンドポイントの実行フロー
type Process interface {
    PreProcess(context.Context, http.ResponseWriter, *http.Request, *sql.Tx) (int, error)
    Run(context.Context, *sql.Tx) (int, error)
    PostProcess(context.Context, *sql.Tx) (int, error)
    IsAccept(context.Context, *sql.Tx) (int, error)
}

また、 Processor関数を用意して、各エンドポイントで呼び出しています。各エンドポイントは返されたhttpステータスコードを利用して、呼び出し元のモジュールにレスポンスします。

// Processor エンドポイント実行の流れ。引数は各所で適切なものを作成しておく
func Processor(ctx context.Context, p Process, w http.ResponseWriter, r *http.Request, tx *sql.Tx) (int, error) {
    var status int
    var err error

    if status, err = p.PreProcess(ctx, w, r, tx); err != nil || status != http.StatusOK {
        return status, err
    }
    if status, err = p.Run(ctx, tx); err != nil || status != http.StatusOK {
        return status, err
    }
    if status, err = p.PostProcess(ctx, tx); err != nil || status != http.StatusOK {
        return status, err
    }
    if status, err = p.IsAccept(ctx, tx); err != nil || status != http.StatusOK {
        return status, err
    }

    return http.StatusOK, nil
}

本稿執筆時はエンドポイントごとにファイルを作成する粒度で分割しています。
その他、設定ファイルの読み込みなど必要な部分を実装します。

タスクのリトライ

巨大なファイルの転送や各種処理時間が長いことなどにより、ネットワークの瞬断やちょっとしたことで失敗になり、それまでのリソースや処理済ファイルなどが無駄になってしまうのは金額や時間コストが大変もったいないです。
そのためリトライの仕組みを作りました。

というものの、愚直にタスクテーブルにリトライ数カラムを作成しただけです!
リトライ回数は今のところ3回に設定しており、それでも失敗した際はエラー状態へと遷移し、slackへアラートを発砲するようにしました。
動作が不安定な稼働初期に特に役に立ちました。
現在はそもそも失敗することがほとんどなくなりました。

この手のシステムでよくあるバッチ処理も実装しました。
エンドポイントを追加して、チームで利用している Rundeck にジョブを作成し、必要に応じて呼び出すように実装しました。
実行中で終わらない状態のゾンビタスク救済処理や、例えばサムネイル生成やサンプル動画作成用のバッチ処理を呼び出しています。

テスト

開発期間があまりなかったため、まず正常系のエンドポイントテストを作成しました。
テスト用DBに初期化sqlを流して実際にデータの変更を行っています。
gomock をはじめとしたツールを上手く利用できたら良かったのですが、不慣れで時間がかかりそうだったので、できるもので確実にやっていくことを優先しました。

異常系は、モジュールができたところから小さく連携テストを重ねていくことで検知する形を取りました。落ち着いた現在は徐々にテストを増やし、リファクタリングに役立てています。

CI/CD

弊社はソース管理に GitHub Enterprise を利用、CIは CircleCI Enterprise を利用しています。
コミットがプッシュされるごとにテストが走り、タグを作成すると、対応した環境に --no-promote でデプロイします。デプロイだけしてルーティングされないようにするオプションです。
開発中はGitHub-Flowでガンガンリリースしていました。
実際のルーティングを移行するのは手動で行っています。

またリリースして落ち着いたので、タグリリースに切り替えました。いわゆるGit-Flowです。
基本的に master ブランチから新ブランチを作成して master ブランチにマージしています。
確実に動作するブランチとして release ブランチを用意しました。 release ブランチにはステージング環境で動作を確認した master ブランチのコミットのみをマージしています。

f:id:yanoshi:20200304184534p:plain
ざっくりとしたデプロイフロー

機能追加時にタグを追加して、すぐに検証できるのはとても便利です。
ソフトウェアの進化に合わせ、開発フローも進化させていく必要性を感じました。

余談

Managerの環境を作ることはもうなさそうですが、弊チームではクラウド環境を利用することが多いため構築をteraraform化することが多いです。 私は全然書いたことがなかったので、良い機会なのでManagerのterraformを書いてみました。

そしてやはり、今のところ(本稿執筆時)出番はないです。

移行する話

まずVRのエンコードから移行しました。VRエンコードシステムは比較的新しく、移行のハードルが低かったからです。
検証環境で何度も試し、たくさんの問題を解決しました。動かしてみないと分からない問題が多かったなという印象です。
デプロイフロー、テストを早い段階で整えたことや、StackDriver *1が利用できることでログを追いやすく、共有しやすくできたことなど、 小さな変更を高速に試せる環境 を作れたことで、功を奏したと感じています。
VRエンコードが無事JIROに切り替わり、2Dも専用処理を追加や検証をしながら実装、リリースできました。
金沢側の引っ越し作業日までに間に合った!

JIROに移行した結果

抱えていた問題 JIROで実現できたこと
大量の自作PCを自分たちで保守運用していた データセンターを利用するようになり、保守運用の手間を削減。より業務に集中できるように
ピークタイム以外(昼間)は配信サーバー等に余剰リソースがある 昼間にエンコーダーのモジュールを稼働させることで、リソースを有効活用
事業部統合による引っ越しのタイミングだった。物理スペースが大量に必要だったため、統合後も残らざるを得なかった 無事期日通り引っ越し可能に。空いたスペースは3Dプリンタ事業に活用されているとのこと
パッケージングやファイル設置が手動 パッケージング、ファイル設置まで自動化されたため、 ヒューマンエラーが低減
エンコード時間が15日程度かかっていた(HQVRの場合) ファイル転送開始から数時間で配信可能状態にすることが可能に
ミドルウェアの変更やエンコードパラメータの変更が行いにくかった モジュール化したため、ノードごとに Gracefulに更新可能 に。エンコードパラメータをAPI化したため、変更が容易
エンコードやストレージ管理が手動。いつ、どこに、どんなコンテンツが設置されたかを認知するのが難しかった ストレージ容量に応じて 動的に設置場所を変更 するように。作業ログも容易に取得、閲覧できるように

とにかくたくさん良い方向に持っていけました!

おしまい

まずは同じチームのメンバーだけでなく、携わっていただいた皆様に感謝いたします。ありがとうございます。

  • Webサイト運営部 映像制作チーム
  • ITインフラ本部 インフラ部 動画配信チーム
  • 横断開発部 業務ハックグループ
  • EC&デジタルコンテンツ本部 データ管理部

また、それぞれのモジュールを担当した動画配信基盤チームのスペシャリストな皆さんにも感謝です。皆さんがいないと絶対に完成しませんでした。技術的な話だけでなく、精神的にも助かりました。

このような基盤の作成に最初から携われたことで、コーディング以外に考えることが必然と多くなり、とても貴重な経験になりました。
これから先、 VR コンテンツや 4K8K コンテンツなどの流れがあるので、動画の容量も更に増えていくことでしょう。
そんな流れに対して、個々のモジュールは 小さくシンプルに作成したため、変化に対応しやすいシステムになりました。
従来のエンコードだけでなく、新しいフォーマットのエンコード検証やパラメーター調整を試し、次の時代への一歩を踏み出したいです。

また、現在も自動化できていないフローをどんどん導入したり、リファクタリングを進めたりしております。
より必要とされた時に、すぐ応えられるシステムにするために、今後もいろいろな手を尽くしていきます。

長文でしたが、お読みくださいましてありがとうございました。

*1:サービス名称は開発当時のもので記載しています。2020年3月現在はoperations suite