00
Article / AWS

動画配信でスパイクを捌く

CloudFront・事前スケーリング・オートスケール・監視 — 細井 透(Toru Hosoi)

01

はじめに.

動画配信のインフラには、月に何度か負荷の山が来ます。人気ドラマの最終回や新作配信の開始時刻。番組終了の直後に、アプリや API に対して普段の何倍ものリクエストが一気に立ち上がります。

8年この手のスパイクに付き合ってきたので、CloudFront まわりで考えていることを書いておきます。

02

スパイクの種類.

スパイクと一口に言っても、中身は少し違います。自分が相手にしてきたのは主に2種類。

予測できるスパイク

番組表でピークの時刻が読めるパターン。ドラマの最終回、スポーツ中継の開始直後。番組終了の数分後からアクセスが立ち上がり、ピークは15〜30分。事前に準備しておけば対応できます。

突発的なスパイク

ニュース速報や、SNS で急にバズった番組。タイミングが読めず、事前準備は間に合いません。オートスケールの反応速度と CloudFront のキャッシュで受けるしかない。個人的にいちばん緊張するのはこちら。

読めるものは先に広げておく。読めないものはキャッシュとオートスケールで受ける。方針は大きくこの2つ。
03

CloudFront でオリジンに到達させない.

スパイク対応でまず効くのは、オリジン(ECS や ALB)までリクエストを届けないこと。CloudFront でどれだけ吸えるかで、オリジンが見る負荷はほぼ決まります。

TTL はコンテンツ種別ごとに設計する

キャッシュできる時間はコンテンツの種類で大きく違います。一律にせず、種別ごとに設計するのが基本。自分が管理しているのは Web / API 側なので、TTL の組み方はこんな感じ。

# TTL設計の例

画像(サムネイル等)
  → TTL: 3600秒(1時間)

APIレスポンス(番組情報、おすすめ等)
  → TTL: 60〜300秒。更新頻度は高くないけど、鮮度もある程度は欲しい

静的アセット(CSS / JS / フォント)
  → TTL: 31536000秒(1年)。ファイル名にハッシュを入れて、変更時は別 URL として出す

認証が必要なエンドポイント
  → キャッシュ対象外(Cache-Control: no-store)
頻繁に変わるものは短く、ファイル名で版が切れるものは長く。キャッシュ効率と鮮度のバランスを取るのが勘どころ。

オリジンシールドで中間キャッシュを効かせる

CloudFront のエッジは世界中にあります。ただ、各エッジにキャッシュが乗るまでの短い間、そこから抜けたリクエストが一斉にオリジンへ向かう。オリジンシールドを挟むと、それらが単一のリージョンに集約されて、オリジン側の負荷が段違いに減ります。

国内向けなら ap-northeast-1(東京)を指定。スパイク時のオリジンリクエストが数分の一になるレベルで効きます。

キャッシュキーに余計なものを入れない

クエリパラメータや Cookie が多いと、中身が同じでもキャッシュキーが別物扱いになり、キャッシュミスが増えます。不要なパラメータはキャッシュポリシー側で落とす。地味ですが、効果は大きい。

# CloudFront キャッシュポリシーで除外するクエリパラメータ例
utm_source, utm_medium, utm_campaign  # 計測用
_ga, fbclid                            # アナリティクス
timestamp, t                           # キャッシュバスター(意図的なもの以外)
04

事前スケーリング.

予測できるスパイクをオートスケールに任せると、まず間に合いません。ECS Fargate や EC2 のスケールアウトには数分かかるので、ピークの立ち上がりに追いつけない。最悪、スケールアウトしている最中にエラーレートが上がってきます。

なので、番組表を元にスケジュールを組んで、先にスケールさせておきます。

ECS の Scheduled Scaling

Application Auto Scaling のスケジュールアクションを使えば、指定した時刻にタスク数を変更できます。

# 例: 人気番組の終了30分前にスケールアウト
aws application-autoscaling put-scheduled-action \
  --service-namespace ecs \
  --resource-id service/cluster-name/service-name \
  --scheduled-action-name "pre-scale-drama-finale" \
  --schedule "cron(30 20 15 4 ? 2025)" \
  --scalable-target-action MinCapacity=20,MaxCapacity=50

# 深夜にスケールイン
aws application-autoscaling put-scheduled-action \
  --service-namespace ecs \
  --resource-id service/cluster-name/service-name \
  --scheduled-action-name "scale-in-after-drama" \
  --schedule "cron(0 2 16 4 ? 2025)" \
  --scalable-target-action MinCapacity=4,MaxCapacity=20
スケールアウトは番組終了の30分前までに完了させておくのが目安。コスト増は一時的なので、スパイクで落とすコストと比べれば許容できます。

ElastiCache はウォームアップしておく

スケールアウト直後のタスクは ElastiCache がまだ温まっていないので、DB へのフォールバックが増えます。事前スケーリングの後に主要エントリを叩いて温めておくだけで、スパイク時の DB 負荷がだいぶ違います。

05

オートスケーリング.

事前スケーリングで捌けない突発スパイクは、オートスケーリングの設定次第。スケールアウトは速く、スケールインは遅く。基本はこれだけ。

# ECS サービスの Auto Scaling ポリシー設計方針

スケールアウト:
  メトリクス : CPU使用率 or ALBのRequestCountPerTarget
  閾値      : CPU 60% を 1分間超えたら
  アクション : +30%(または最低+2タスク)
  クールダウン: 60秒(短め)

スケールイン:
  閾値      : CPU 30% を 10分間下回ったら
  アクション : -1タスクずつ
  クールダウン: 600秒(長め)

スケールインを急ぐと、スパイクの余韻で閾値を再度超えてしまい、スケールアウト → スケールイン → スケールアウトを繰り返すスラッシングに陥ります。スケールインのクールダウンを長めに取るのがセオリー。

ALB の TargetResponseTime も見る

CPU だけ見ていると反応が遅れることがあります。ALB の TargetResponseTime が上がり始めた時点でスケールアウトを仕掛ければ、ユーザー側に影響が出る前に手を打てる。

06

WAF でオリジンを守る.

スパイク時は正規ユーザーに混じって、怪しいリクエストも増えます。WAF をエッジで効かせて、オリジンに届く前に弾きます。

  • レートベースのルール — 同一 IP の短時間リクエストに上限を設ける。スクレイピングやバグったリトライの連打を止められる
  • AWS マネージドルール — OWASP Top 10 や Bot コントロールはマネージドに任せる。自分でルールを書き続ける気力はない
  • 地域制限 — 国内向けなら、関係ない地域は素直にブロック。無駄なオリジンリクエストが減る
07

監視とアラーム.

どれだけ設計を詰めても、想定外は必ず起きます。早く気づいて、早く動くためのフローが必要。

監視対象メトリクス

# CloudWatch で常時監視するメトリクス

CloudFront:
  - CacheHitRate         (キャッシュヒット率。急落したらオリジン負荷増加のサイン)
  - 5xxErrorRate         (オリジンエラー率)
  - TotalErrorRate       (全エラー率)
  - Requests             (リクエスト数。スパイク検知)

ALB:
  - TargetResponseTime   (レスポンスタイム。劣化の早期検知)
  - HTTPCode_ELB_5XX_Count
  - RequestCount

ECS:
  - CPUUtilization
  - MemoryUtilization
  - RunningTaskCount     (スケールアウト/インの確認)

RDS / Aurora:
  - DatabaseConnections  (接続数枯渇の検知)
  - CPUUtilization
  - FreeableMemory

PagerDuty 連携

CloudWatch アラームを EventBridge で受けて PagerDuty に流しています。重要度でルーティングを分け、軽いものは Slack 通知のみ、重いものはオンコール担当に電話。

スパイク時はアラームが連鎖するので、ノイズを削るのが大事。オリジンの CPU が張り付いているときにレスポンスタイムアラームまで鳴るのは当然なので、根本原因に当たりそうなアラームだけをオンコール対象にしています。

「アラームが多すぎて本当に大事な通知を見逃す」のがいちばん怖い。アラーム設計は半年に一度くらい見直すようにしています。
08

まとめ.

8年やってみて、この順番で考えるのが一番シンプルでした。

  1. まず CloudFront で吸う — キャッシュヒット率を上げるのが一番コスパがいい
  2. 予測できるものは先に備える — スケールアウトに数分かかることを前提に動く
  3. 予測できないものはオートスケールで追う — スケールアウトは速く、スケールインは遅く
  4. 漏れた分は監視で早く気づく — 気づいてから動くまでを短くする

単独で効く施策はなく、組み合わせで対応しています。どれか抜けると、そこから穴が空きます。