株式会社MIXIで
この連載では、これまでみてねのSREグループにおける取り組みを紹介してきました。今回からは同じくみてねにおけるData Engineeringグループの取り組みをご紹介します。
みてねのData Engineeringグループは、
今回の記事では、1秒動画をどのように生成・
1秒動画とは
1秒動画とは、ユーザがみてねへアップロードした写真・
この1秒動画には、全家族へ3ヵ月ごとに配信する
項目 | 四季版 | 月版 | 年間版 |
---|---|---|---|
動画の中身 | 直近3ヵ月の振り返り | 先月の振り返り | 去年1年間の振り返り |
配信対象 | 全家族 | みてねプレミアム加入家族 | みてねプレミアム加入家族 |
配信頻度と大量配信の期間 | 3ヵ月に1回 |
毎月1回 |
年に1回 |
含まれる素材数 | 動画のみ最大20点 | 動画・ |
動画・ |
動画編集 | 動画のみの基本的な編集 | エフェクトを含むリッチな編集 | 1年間の振り返りに最適化した、よりリッチな編集 |
なお上記の
生成・配信の流れ
1秒動画の生成・
(1)対象家族抽出
1秒動画の生成・
四季版の対象家族抽出にはBigQueryを用いています。これは、みてねにご登録いただいている全家族が配信対象であり、必然的に生成・
※対象家族抽出の詳細についてはこちら:BigQuery で1秒動画の配信対象家族を爆速で抽出する / How to create 1sec movie schedules with bigquery - Speaker Deck
これに対し月版・
これは、月版・
なお月版・
(2)素材選択
素材選択の処理では、(1)で抽出した各家族および1秒動画のバージョンと期間に対し、各家族がもつ動画・
この素材選択処理には、独自の素材選択AIを利用しています。これは、顔検出や人物検出などのMLモデルを使った動画・
※素材選択処理の詳細についてはこちら:いい感じの素材選択ロジック / How to select videos for 1sec Movie - Speaker Deck
(3)動画エンコード
素材選択の完了後には、動画・
(4)配信
transcoderで生成した1秒動画の動画ファイルは、iOS/
transcoderにおける1秒動画生成の詳細
ここまで1秒動画の生成・
みてねのバックグラウンド処理においては、大規模かつ広範囲にSidekiqを利用しています。Sidekiqは、Rubyで実装された非同期のジョブキューシステムであり、データストアとしてRedisを利用します。
transcoderは、Sidekiq Proの一機能であるSidekiq Batchを利用し、FFmpegの処理群からなる1秒動画エンコードの処理をパイプラインとして定義・
以下では説明のため、1秒動画の生成処理を簡略化したイメージを用いてご説明します
※transcoderについては過去の登壇資料も併せてご参照ください:Scalable Microservice for Media Transcoding / Transcoder - Speaker Deck
1秒動画の生成パイプライン
1秒動画ファイルの生成は、以下のようなFFmpegの処理
- 動画の切り出し
( trim
) - 写真の動画化
( overlay
など) - エフェクトの付与
( zoompan
など) - ウォーターマークの付与
( overlay
) - クロスフェードでの動画連結
( fade
)
transcoderではこれらのFFmpeg filterの処理を、それぞれ1つのSidekiq Workerとして実装しています。また各Workerの依存関係や直列・
このパイプライン設計により、各Workerを並列実行することで生成処理を高速化
1秒動画生成パイプラインのSidekiq Batchによる実装
続いてSidekiq Batchによる具体的なパイプライン実装について、下記のコード例を用いて説明します。
OneSecondMoviePipeline
がパイプライン全体の定義であり、ヘルパメソッドdefine_
を用いてtrim
、effect
、add_
、crossfade
の順に各ステップの処理をパイプラインに登録しています。
これに対し、各ステップの実装はTrimWorker
のように、通常のSidekiq Workerとして実装します。
このパイプライン全体としては、はじめにOneSecondMoviePipeline#perform
で実行するtrim
処理からステップが始まり、各ステップに登録されたジョブがすべて完了するごとに、次のステップへと進む、といった具合に処理が行われます。
# パイプライン全体の実装
class OneSecondMoviePipeline
include Sidekiq::Worker
def perform
status = nil
options = {}
trim(status, options) # パイプラインの先頭はtrimの処理
end
# パイプライン定義のためのhelper
def self.define_sidekiq_method(method:, worker_class:, on_success_method:, on_success_options:)
define_method(method) do |status, options|
parameters = worker_class.create_parameters(options: options)
# @see https://github.com/mperham/sidekiq/issues/3522
parent_batch = ::Sidekiq::Batch.new(status.try(:parent_bid) || bid)
parent_batch.jobs do
options = on_success_options.call(parameters)
step = ::Sidekiq::Batch.new
step.callback_queue = 'one_second_movie_callback'
step.on(:success, "#{self.class}##{on_success_method}", options)
step.on(:complete, "#{self.class}#on_complete", options)
step.description = "#{self.class}##{__method__}"
# 各Workerのenqueueメソッドでperform_asyncすることで、このstepにおけるジョブを登録する
step.jobs { worker_class.enqueue(parameters: parameters, queue: 'one_second_movie') }
end
end
end
# パイプラインの定義。trim, effect, add_watermark, crossfadeの順で処理する
define_sidekiq_method(
method: 'trim',
worker_class: TrimWorker,
on_success_method: 'effect',
on_success_options: proc { |parameters| { outputs: parameters.map { |p| p.output.to_h } } },
)
define_sidekiq_method(
method: 'effect',
worker_class: EffectWorker,
on_success_method: 'add_watermark',
on_success_options: proc { |parameters| { outputs: parameters.map { |p| p.output.to_h } } },
)
define_sidekiq_method(
method: 'add_watermark',
worker_class: AddWatermarkWorker,
on_success_method: 'crossfade',
on_success_options: proc { |parameters| { outputs: parameters.map { |p| p.output.to_h } } },
)
define_sidekiq_method(
method: 'crossfade',
worker_class: CrossfadeWorker,
on_success_method: 'on_success',
on_success_options: proc { |_| {} },
)
def on_complete(status, options)
# パイプライン完了後の処理として成果物として得られた動画ファイルの保存・返却を行う(省略)
end
end
# 各ステップの実装
class TrimWorker
include Sidekiq::Worker
def perform(parameters)
# FFmpegで実際にtrim処理を実行する(省略)
end
def self.create_parameters(options:)
# trim処理に必要なパラメータを組み立てる(省略)
end
def self.enqueue(parameters:, queue:)
# trim処理を行うSidekiqジョブを登録する
parameters.each do |parameter|
TrimWorker.set(queue: queue).perform_async(parameter.map(&:to_h))
end
end
end
# EffectWorker, AddWatermarkWorkerなども同様に実装する(省略)
transcoderのインフラ
上記Sidekiq Batchによるパイプラインが実際に動くtranscoderの動作環境としては、みてねではAmazon EKSを利用しており、実際の処理はAmazon EC2インスタンスが処理します。
この詳細は、当連載における過去の記事
インフラとSidekiq Batchパイプラインの最適化事例
ここまででtranscoderの設計と実装を簡単にご紹介しました。ここからはtranscoderにおけるインフラとSidekiq Batchパイプラインの最適化事例についてご紹介します。
従来のtranscoderのインフラ・Sidekiq Batchパイプライン設定とその課題
従来のtranscoderでは、1秒動画生成処理に関するあらゆるWorkerを1つのキューone_
キュー)
# 各Workerにおけるqueueの指定例
class OneSecondMoviePipeline
include Sidekiq::Worker
sidekiq_options queue: 'one_second_movie' # 他のTrimWorker, EffectWorkerなども同様
end
# EKS環境の1秒動画生成用DeploymentにおけるSidekiqプロセスの実行例
# このSidekiqプロセスでは[one_second_movie_callback, one_second_movie]キューの優先順位でジョブを実行する
# one_second_movie_callbackキューでは、Sidekiq Batchによるコールバックの処理が実行され、これは最優先で実行する必要があるためキューを分けている
# see also: https://github.com/sidekiq/sidekiq/wiki/Advanced-Options
sidekiq -q one_second_movie_callback -q one_second_movie
これにより、各ステップにおける複数のジョブを並列実行することで、パイプライン全体としての高速化が可能である一方、以下の問題が発生していました。
-
①一度
(1日) の1秒動画生成バッチ処理全体が終わりに近づくまで、1家族分も最終的な成果物 (1秒動画ファイル) が完成しない→これにより、障害などで生成バッチの処理が遅れた場合、バッチの締め切り時間 (配信時間) に、大部分のジョブ (家族) の処理が間に合わなくなるリスクがある。 -
②各ステップにはCPUやメモリの使用状況に差があり、インフラ設定
(コンテナのCPU・ メモリ割り当て量) の最適化が難しい→これにより、CPUやメモリのリソースを余裕を持って割り当てる必要が生じ、インフラコストが無駄になる。
これらの問題が発生する理由は以下のとおりです。
- Sidekiq Batchは、1ステップのWorker
(たとえば TrimWorker
)のジョブがすべて完了した時点で、はじめて次のステップ ( TrimWorker
に対するEffectWorker
)のジョブをenqueue ( perform_
)async する、という仕様です。 - ここで前述のように各ステップのWorkerを1つのキューにまとめると、すべてのジョブがenqueueされた順に実行されます。
- この条件において、バッチ処理的に多数の1秒動画
(多数の家族の1秒動画) をいちどに生成開始することを考えます。このときtranscoder全体の処理の流れは、 「全家族に対するステップA → 全家族に対するステップB → …… → 全家族に対するステップN」 といった具合に、 「パイプライン定義のステップ順に従い、全家族の処理をまとめて、1つ目のステップから順に実行する」 という挙動になります (上記問題の①) (下図の黒矢印)。 - また、各ステップのFFmpeg処理は、それぞれが必要とするCPUやメモリに大きな差があります。たとえば動画から特定の秒数を切り出す
TrimWorker
では、オリジナルの動画ファイル(最長10分) をダウンロードするためI/ O待ちの時間が長く、相対的にCPU使用率やメモリ使用量が減ります。これに対し、素材となる動画・ 画像をすべてクロスフェードでつなぎ合わせる CrossfadeWorker
では、多数の動画・画像ファイルを処理するために相対的に多くのメモリが必要となります。このような計算リソースへの要求量の差により、 「transcoderの動くインフラ (EC2インスタンス) を、1秒動画生成パイプライン全体を通して、できるだけCPUもメモリも効率良く安定的に動かし続ける」 ことが難しくなります (上記問題の②)。
改善策:パイプライン先頭のWorkerのみ優先順位を下げる
この問題への改善策として、1秒動画生成パイプラインのうち先頭にあるOneSecondMoviePipeline
の優先度を下げ、
# 各Workerにおけるqueueの指定例
class OneSecondMoviePipeline
include Sidekiq::Worker
sidekiq_options queue: 'one_second_movie_low' # 他のTrimWorker, EffectWorkerなどはone_second_movieキューのまま
end
# EKS環境の1秒動画生成用DeploymentにおけるSidekiqプロセスの実行例
# このSidekiqプロセスでは[one_second_movie_callback, one_second_movie, one_second_movie_low]キューの優先順位でジョブを実行する
sidekiq -q one_second_movie_callback -q one_second_movie -q one_second_movie_low
これにより、transcoder全体の処理の流れは、
より厳密には、上記の挙動をtranscoderにおける各Sidekiqプロセスが行うため、
ここでSidekiqプロセスの動く
この改善策により、前述の2つの問題点はほぼ完全に解決され、transcoderからは成果物である1秒動画ファイルがコンスタントに出力され、かつ計算リソースを安定的かつ効率的に利用できるようになりました。
※transcoderにおける処理の効率化や負荷テストといった観点では、過去に以下のようなテーマでもご紹介しており、あわせてご参照ください:
FFmpegのfilter graphを用いて、複数のfilter処理を1つのFFmpegコマンドにまとめる方法をご紹介しています。
毎年1月2日ごろから大量配信を行っている年間版1秒動画に関し、生成負荷安定化のために行った施策と負荷テストについてご紹介しています。
まとめ
この記事では、みてねData Engineeringグループにおける取り組みの1つとして、1秒動画の生成・
今回ご紹介したようなパイプラインあるいはワークフローの実装にSidekiq Batchを用いる場合、この記事で見たように、ある程度のboilerplate実装を要求される点には注意が必要です。
ただしみてねのように、すでにSidekiqを運用している場合などには、その他のSidekiq Pro機能、たとえばReliabilityなどとセットで利用でき、かつ個別のWorkerは通常のSidekiq Worker同様に実装できるため、選択肢の1つとして検討に値するかもしれません。この記事がどなたかのお役に立てれば幸いです。