CloudFront・事前スケーリング・オートスケール・監視 — 細井 透(Toru Hosoi)
動画配信のインフラには、月に何度か負荷の山が来ます。人気ドラマの最終回や新作配信の開始時刻。番組終了の直後に、アプリや API に対して普段の何倍ものリクエストが一気に立ち上がります。
8年この手のスパイクに付き合ってきたので、CloudFront まわりで考えていることを書いておきます。
スパイクと一口に言っても、中身は少し違います。自分が相手にしてきたのは主に2種類。
番組表でピークの時刻が読めるパターン。ドラマの最終回、スポーツ中継の開始直後。番組終了の数分後からアクセスが立ち上がり、ピークは15〜30分。事前に準備しておけば対応できます。
ニュース速報や、SNS で急にバズった番組。タイミングが読めず、事前準備は間に合いません。オートスケールの反応速度と CloudFront のキャッシュで受けるしかない。個人的にいちばん緊張するのはこちら。
スパイク対応でまず効くのは、オリジン(ECS や ALB)までリクエストを届けないこと。CloudFront でどれだけ吸えるかで、オリジンが見る負荷はほぼ決まります。
キャッシュできる時間はコンテンツの種類で大きく違います。一律にせず、種別ごとに設計するのが基本。自分が管理しているのは 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 # キャッシュバスター(意図的なもの以外)
予測できるスパイクをオートスケールに任せると、まず間に合いません。ECS Fargate や EC2 のスケールアウトには数分かかるので、ピークの立ち上がりに追いつけない。最悪、スケールアウトしている最中にエラーレートが上がってきます。
なので、番組表を元にスケジュールを組んで、先にスケールさせておきます。
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
スケールアウト直後のタスクは ElastiCache がまだ温まっていないので、DB へのフォールバックが増えます。事前スケーリングの後に主要エントリを叩いて温めておくだけで、スパイク時の DB 負荷がだいぶ違います。
事前スケーリングで捌けない突発スパイクは、オートスケーリングの設定次第。スケールアウトは速く、スケールインは遅く。基本はこれだけ。
# ECS サービスの Auto Scaling ポリシー設計方針
スケールアウト:
メトリクス : CPU使用率 or ALBのRequestCountPerTarget
閾値 : CPU 60% を 1分間超えたら
アクション : +30%(または最低+2タスク)
クールダウン: 60秒(短め)
スケールイン:
閾値 : CPU 30% を 10分間下回ったら
アクション : -1タスクずつ
クールダウン: 600秒(長め)
スケールインを急ぐと、スパイクの余韻で閾値を再度超えてしまい、スケールアウト → スケールイン → スケールアウトを繰り返すスラッシングに陥ります。スケールインのクールダウンを長めに取るのがセオリー。
CPU だけ見ていると反応が遅れることがあります。ALB の TargetResponseTime が上がり始めた時点でスケールアウトを仕掛ければ、ユーザー側に影響が出る前に手を打てる。
スパイク時は正規ユーザーに混じって、怪しいリクエストも増えます。WAF をエッジで効かせて、オリジンに届く前に弾きます。
どれだけ設計を詰めても、想定外は必ず起きます。早く気づいて、早く動くためのフローが必要。
# CloudWatch で常時監視するメトリクス
CloudFront:
- CacheHitRate (キャッシュヒット率。急落したらオリジン負荷増加のサイン)
- 5xxErrorRate (オリジンエラー率)
- TotalErrorRate (全エラー率)
- Requests (リクエスト数。スパイク検知)
ALB:
- TargetResponseTime (レスポンスタイム。劣化の早期検知)
- HTTPCode_ELB_5XX_Count
- RequestCount
ECS:
- CPUUtilization
- MemoryUtilization
- RunningTaskCount (スケールアウト/インの確認)
RDS / Aurora:
- DatabaseConnections (接続数枯渇の検知)
- CPUUtilization
- FreeableMemory
CloudWatch アラームを EventBridge で受けて PagerDuty に流しています。重要度でルーティングを分け、軽いものは Slack 通知のみ、重いものはオンコール担当に電話。
スパイク時はアラームが連鎖するので、ノイズを削るのが大事。オリジンの CPU が張り付いているときにレスポンスタイムアラームまで鳴るのは当然なので、根本原因に当たりそうなアラームだけをオンコール対象にしています。
8年やってみて、この順番で考えるのが一番シンプルでした。
単独で効く施策はなく、組み合わせで対応しています。どれか抜けると、そこから穴が空きます。