cookpadTV ライブ配信サービスの”突貫” Auto Scaling 環境構築

 インフラや基盤周りの技術が好きなエンジニアの渡辺です。

 今回は私が開発に関わっている cookpadTV の Auto Scaling 環境を突貫工事した事例をご紹介します。 同じチームのメンバがコメント配信回りについても書いていますので興味があれば合わせて読んでみてください。

クッキングLIVEアプリcookpadTVのコメント配信技術

 本エントリは Amazon EC2 Container Service(以降 ECS) をある程度知っている方向けとなっています。 細かいところの説明をしているとものすごく長文になってしまう為、ご了承頂ければ幸いです。

Auto Scaling について

 一般的に Auto Scaling は CPU やメモリ利用量によって増減させるのが一般的です。*1 私が属している部署で開発保守している広告配信サーバも、夕方のピークに向けて CPUUtilization の最大値がしきい値を超えたら指定台数ずつ scale-out するように設定しています。 scale-in も同じく CPUUtilization が落ち着いたら徐々に台数を減らしていっています。

 クックパッドもサービスとしてはピーキーなアクセスが来るサービスで、通常だと夕方の買い物前のアクセスが一番多いです。 ECS での Auto Scaling 設定当初は 5XX エラーを出さず、scale-out が間に合うように細かい調整を行っていました。

イベント型サービスと Auto Scaling

 ライブ配信の様にコンテンツの視聴出来る時間が決まっていて特定の時間にユーザが一気に集まるサービスでは、負荷に応じた scale-out では間に合わないことがあります。 立ち上げ当初はユーザ数もそこまで多くなかったということもあり、前述の負荷に応じた Auto Scaling 設定しか入れていませんでした。 その為、一度想定以上のユーザ数が来たときに scale-out が間に合わずライブ配信開始を 40 分ほど遅らせてしまいました。 基本的に cookpadTV のライブ配信の番組は週一で放送されていますので、1 週間以内に対応しないと同じ番組でまたユーザに迷惑を掛けてしまいます。 そこで cookpadTV チームではこの問題を最優先対応事項として改善を行いました。

まずはパフォーマンス・チューニング

 Auto Scaling の仕組みを改善する前にまずは API 側のコードを修正することで対応を行いました。 こういう細かい処理のロジックを見直すことで最適化、高速化することももちろん効果的ですが、ピーキーなアクセスで最も効果的なのは短期キャッシュです。 キャッシュは適切に使わないと思わぬトラブルを生むことが多く、出来れば使用は避けたいものです。 しかし短期キャッシュであれば情報の更新頻度次第ではあまり問題にならない場合があります。 そして今回のケースにおいてはライブ配信の時間は事前に決定し変更はされない、レシピ情報も事前に確定することがほとんです。 その為、短期キャッシュを入れる事でキャッシュのデメリット無くアプリケーションのスループットを上げることが出来ると判断しました。

 想定外のユーザ数が来た時に一番問題になったのは cookpadTV の更に先にある別の Micro Service(サービス B) への API リクエストでした。 サービス B は Auto Scaling 設定が入っておらず、大量にアクセスが流れてレスポンスタイムが悪化、cookpadTV API の unicorn worker が詰まって Nginx がエラーを返し始めるという状況でした。 ここの部分の API コールを短期キャッシュすることでスループットを大幅に上げることが出来ました。

f:id:wata_htn:20180426204103p:plain

Auto Scaling の改善

 さて、アプリケーション自体の改善は 1 週間以内で出来ることはやりました。 次は Auto Scaling 側の改善です。 冒頭でも記載した通り、負荷が上がった後では間に合わないのでライブ配信が始まる前に scale-out を済ませておく必要があります。 傾向からしてもライブ配信開始 15 分前ぐらいから徐々に人が来はじめ、直前ぐらいに push 通知を受けてユーザが一気に来ます。 その為、何かあったときに備え、余裕を持って 15 分前には scale-out を済ませておきたいと考えました。

クックパッドでの ECS Service の Auto Scaling

 クックパッドでは ECS Services の desired_count、pending_count、running_count を定期的にチェックして、pending_count を解消できるように EC2 インスタンスが scale-out されるようになっています。 その為 desired_count を何かしらの仕組みで増やすことが出来れば、後は EC2 インスタンスも含め scale-out されていきます。

単純に desired_count を増やせば良いわけではない

 ただし、単純にライブが始まる 30 分前に desired_count を増やすだけではまだ負荷が高くない為、徐々に scale-in されてしまいます。 さらに、API サーバは全配信で共有のため、複数番組同時に放送されると時間帯によっては単純に「番組配信前に指定 task 数増やす」だけではうまく行きません。 事前に scale-out したのが配信前に scale-in してしまっては意味が無いので、desired_count を単純に上げるのではなく min_capacity をライブ開始 30 分前に指定し、ライブ開始時間に min_capacity を元に戻す方式を採用しました。 この時限式に min_capacity を調整するのは、Aws::ApplicationAutoScaling::Client#put_scheduled_action を使用して実現しています。

 コードとしては以下のような形です。

def schedule_action(episode)
  scale_out_time = [episode.starts_at - 30.minutes, 1.minute.after].max
  scale_in_time = episode.starts_at
  aas.put_scheduled_action({
    service_namespace: "ecs",
    schedule: "at(#{scale_out_time.getutc.strftime('%FT%H:%M:%S')})", # UTC で指定する必要があります
    scheduled_action_name: "EpisodeScaleOut##{episode.id}",
    resource_id: "service/xxxx/cookpad-tv-api",
    scalable_dimension: "ecs:service:DesiredCount",
    scalable_target_action: {
      min_capacity: reserved_desired_count(from: scale_out_time), # ここで scale-out するための min_capacity を計算
    },
  })
  aas.put_scheduled_action({
    service_namespace: "ecs",
    schedule: "at(#{scale_in_time.getutc.strftime('%FT%H:%M:%S')})", # UTC で指定する必要があります
    scheduled_action_name: "EpisodeScaleIn##{episode.id}",
    resource_id: "service/xxxx/cookpad-tv-api",
    scalable_dimension: "ecs:service:DesiredCount",
    scalable_target_action: {
      min_capacity: reserved_desired_count(from: scale_in_time + 1.second), # ここで scale-in するための min_capacity を計算
    },
  })
end

def aas
  @aas ||= Aws::ApplicationAutoScaling::Client.new
end

 やっている事を図で表すと以下の通りです。

f:id:wata_htn:20180426204251p:plain

 min_capacity が引き上げられると結果的に desired_count も引き上げられ、running task 数が増えます。 上の図の結果、running task 数は以下の図の赤線の推移をします。

f:id:wata_htn:20180426204317p:plain

 そして、ライブ配信が被った時間帯に配信されることも考慮して、重複した時には必要数を合計した値で min_capacity をコントロールするようにしました。 勿論 min_capacity を戻す時も被っている時間帯の配信も考慮して計算しています。 先程の図に番組 B が被った時間に配信されるとすると以下の様になります。

f:id:wata_htn:20180426204341p:plain

running task 数で表現すると以下となります。

f:id:wata_htn:20180426204406p:plain

 そして、番組毎に盛り上がりが違うので、それぞれ違う task 数が必要です。 そこで番組毎の必要 task 数を、以前の近しい時間帯に配信された番組の視聴ユーザ数から割り出すようにしました。 前回とかでなく「近しい時間」としているのは、番組によっては週によって配信時間が変わったりし、そして平日だとお昼よりも夜の方が来場数が多かったりするからです。

以前の配信のユーザ数等のデータ処理

 さらっと「以前の近しい時間帯に配信された番組の視聴ユーザ数」と書きましたがこれは以下のように利用できるようにしています。

  1. ユーザの視聴ログから bricolage でサマリを作成(Redshift -> Redshift)
  2. redshift-connector + Queuery を使って MySQL にロード

 クックパッドでは全てのログデータは Amazon Redshift に取り込まれるようになっていて、そのデータを Tableau を使って可視化しています。 それをデータ活用基盤を利用して加工、アプリケーションの MySQL まで取り込んでいます。

 後は番組情報が作成、更新されたらその付近で配信予定の番組も合わせて min_capacity が再計算されるようになっています。 これらによって予約された Auto Scaling を管理画面からも確認出来るようにしました。

f:id:wata_htn:20180426204438p:plain ※画像はイメージです

初回と突発的な対応

 以前の配信がある場合はこれで対応出来ますが、初回や突発ケースにはこれだけでは不十分です。 初回はさすがに読めない部分が多いのですが、SNS での拡散状況等や、番組のお気に入り数等から来場ユーザ数を人が推測し予め設定出来るようにしました。 来場ユーザ数が指定されていると、それに必要な task 数を計算し、上記の流れと同じ様に Auto Scaling が行われます。 なかなか完全にはシステム化は難しく、こういう人のが介在する余地はまだまだ必要だったりします。 (勿論 SNS の情報を引っ張ってきて、人の予測ロジックをアルゴリズム化しても良いのですが)

退出

 忘れていけない事として、ライブ配信開始時の突入だけでなく退出があります。 ライブ視聴を終わったユーザが一気にアプリの他の画面に移動するため、ライブ開始時と同じぐらいの負荷が来ます。 ここをクリアするために scale-in はゆっくり目にして、一気に task 数が減らないようにして対処しています。 ライブ配信の終了時間は読めないため、ここは予定を組んで置くことが出来ないためです。

 参考までに突入と退出の負荷がどれくらいなのかリソース利用量のグラフを貼っておきます。

f:id:wata_htn:20180426204505p:plain

今後の発展

 同時アクセスが大量に来るのは push 通知によっても発生することがあります。その為、今後は remote push 通知を送信する前に送信件数をベースに Auto Scaling する仕組みを導入していく想定です。

補足

 Micro Services でサービスを開発していると、他チームの Service に依存して、自分達の Service だけ Auto Scaling してもサービス全体としては成り立たないことがあります。 その為その境界線を意識し、自分達の開発しているサービス内でカバーできるような設計にしていく必要があります。 キャッシュやリトライ戦略は各 Service が個別に開発するというよりはサービスメッシュ*2によって統合管理が達成出来るのではと考えています。

最後に

 イベント型サービス向けの Auto Scaling が必要になってから、突貫で作った形ではありますがクックパッドの既存の基盤のおかげでなんとか運用が回る Auto Scaling 環境が出来ました。 この辺りの基盤がしっかりしている事は今回非常に助かりました。

 さて、今回の事例紹介は以上ですが自分だったら、もっと改善出来る!という方で一緒にやっていきたいと思ってくださった方は是非私までお声がけください。