TL;DR
- ECSのターゲット追跡スケーリング、スケールアウトは速いのにスケールインが遅い
- 100タスクから10タスクへのスケールインに3時間以上かかる
- 「スケールインは現在のタスク数の5%を上限とする固定ステップ」という公式ドキュメントにない「裏ルール」があった
材料はこちら
うちで運営しているMGRe(メグリ)というSaaSで利用しているECS Serviceについて、ターゲット追跡スケーリングの挙動を分析してみました。
集めたデータ:
- 500件超のスケーリングアクティビティ(
describe-scaling-activitiesで取得) - 同期間(約4日分)のCPU / メモリ / レスポンスタイム メトリクス(
get-metric-data、1分粒度) -
DesiredTaskCountメトリクス(Container Insights、1分粒度)
DesiredTaskCount を別途取ったのには理由があって、スケーリングアクティビティの連鎖から「直前のタスク数」を逆算しようとすると、別系統のスケーラー(自前のLambdaなど)1による変更が抜け落ちて値がズレるからです。1分粒度のメトリクスで「実際のタスク数の履歴」を持ってくることで、正確な現在のタスク数が分かります。
これらをClaude Codeに書いてもらったPythonスクリプトで結合・集計したところ、規則的な挙動が見えました。以下、具体的な発見です。
発見その1:スケールアウトは爆速
まずはスケールアウトの方。これはわかりやすい挙動でした。
新しいタスク数 = ceil(現在のタスク数 × メトリクス値 / 目標値)
たとえばCPU使用率の目標値が50%で、現在のタスク数が16、CPU使用率が80%まで上がったとします。
ceil(16 × 80 / 50) = ceil(25.6) = 26
なので、1ステップで一気に16 → 26まで増やします。CPU起因のスケールアウト25件で計算式どおりになったのが92%(23件)。残り2件は計算式から大きく外れており、よく見るとCPU使用率が目標値を下回っているのにスケールアウトしている、という奇妙なケースでした。状況的には、別系統のスケーラー(自前のLambda)1が短時間タスク数を変えた直後にターゲット追跡が反応した可能性が高そうです。1分粒度のメトリクスでは捉えきれない短時間の動きだったので、確定はできませんでした。
ちなみに「メトリクス値」として何を使うかが地味に重要で、観測したところ直近3分間のデータポイントの最大値を使っているようです。これは AlarmHigh の評価期間(3 datapoints × 60秒)と一致していて、おそらく「 AlarmHigh をALARMに遷移させた3つのデータポイントのうち最大値」を計算に使っていると思われます。
なお、 ResponseTime のような「タスク数に比例しないメトリクス」では計算式どおりにはなりません。これは後の章で触れます。
発見その2:スケールインは超慎重派
ここからが本題です。スケールインの動きは、スケールアウトとは似ても似つかないものでした。
スケールインのアクティビティを並べてみると、1回のスケーリングアクションで減らせるタスク数(ステップサイズ)は、計算式とは無関係に、現在のタスク数だけで決まっていました。
| 現在のタスク数 | 1ステップで減るタスク数 |
|---|---|
| 1〜19 | 1 |
| 20〜39 | 1 |
| 40〜59 | 2 |
| 60〜79 | 3 |
| 80〜99 | 4 |
| 100以上 | 5+ |
数式にすると max(1, floor(現在のタスク数 / 20))、つまり**現在のタスク数の5%(小数点以下切り捨て、最小1)**です。
観測した400件超のスケールインで、ほぼこの規則と一致しました。例外も2件ありましたが、いずれもメトリクスが目標値のすぐ下に来ているタイミングのケース。5%まるまる減らすと目標値を超えそうなので、AWSが減らす数を控えただけです。5%ルールが破られたわけではなく、上限の5%まで使わなかっただけ、ということですね。
ここで重要なのは、メトリクスがどれだけ目標値を下回っていても、1回のステップで減らせるのは現在のタスク数の5%までということ。
たとえば、メモリ使用率の目標値を70%に設定しているとして、現在のタスク数が36、メモリ使用率が10%まで下がっていたとします。常識的には「負荷の余裕がたっぷりあるんだから、もっと一気に減らしてよさそう」と思える状況ですが、5%ルールに従うと floor(36/20) = 1 で、1ステップで減るのは1タスクだけです。
次のクールダウンを待ってまた1タスク、その次もまた1タスク……と、じわじわとしか減っていきません。
これは公式のドキュメントに記載のない、観測でしか分からない実装ルールです。
公式に書かれていない仕様詳細
ターゲット追跡スケーリングは、CloudWatchに AlarmHigh / AlarmLow を自動生成して使っています。これらのアラームのパラメータが、AWS公式ドキュメントには明記されていません。観測すると、以下のような値で動いていました。
AlarmHigh (スケールアウトの発火条件)
| 項目 | 観測値 |
|---|---|
| EvaluationPeriods | 3 |
| Period | 60秒 |
| Threshold | 目標値と同じ |
| ComparisonOperator | GreaterThanThreshold |
つまり「直近3分間、毎分のメトリクスが目標値を超え続けたら発火」。
AlarmLow (スケールインの発火条件)
| 項目 | 観測値 |
|---|---|
| EvaluationPeriods | 15 |
| Period | 60秒 |
| Threshold | 目標値 × 0.9 |
| ComparisonOperator | LessThanThreshold |
つまり「直近15分間、毎分のメトリクスが目標値の0.9倍を下回り続けたら発火」。
目標値の0.9倍というのが地味に重要なポイントです。公式ドキュメントには usually more than 10% lower(普通は10%以上下)と書かれていますが、ここに付いている usually という曖昧表現が、観測すると実は常に厳密な値だった、ということを意味します。観測したCPU / メモリ / レスポンスタイムすべてのpolicyで、 AlarmLow のThresholdは 目標値 × 0.9 でした。
これらの数値の一部はAWSのre:Postコミュニティでも触れられていますが、公式ドキュメントには明記がない、というのが現状です。
クールダウンと評価サイクルの関係
スケーリングpolicyには ScaleOutCooldown / ScaleInCooldown というパラメータがあります。MGReでは ScaleInCooldown は180秒と短めに設定しています。
観測すると、スケールインの実際の発火間隔は 4分 でした。これは「クールダウン180秒 + 1分粒度のメトリクス評価サイクル」の合計で説明できます:
T=0:00 スケールイン発火(クールダウン開始)
T=3:00 クールダウン終了
T=4:00 次の1分境界でアラーム再評価 → ALARM継続中 + クールダウン終了
→ 再度スケールイン発火
つまり実質的な発火間隔は ScaleInCooldown 切り上げ後の次の1分境界になります。クールダウンを60秒に短くしても、実際の最短間隔は2分(60秒 + 1分粒度)になる、ということですね。
非対称設計の含意
ここまでの発見をまとめると、ターゲット追跡スケーリングは「スケールアウトは速く、スケールインは遅い」という極端な非対称設計をしています。
スケールアウトはどれだけ速いか
10タスクからCPU使用率が一気に100%まで急騰した場合、計算式は:
ceil(10 × 100 / 50) = 20
1ステップで一気に10 → 20まで増えます。さらに次のクールダウン後にもう1ステップ、と倍々ペースで追従できます(MaxCapacity に達するまで)。
スケールインはどれだけ遅いか
5%ルールに従ってじわじわ減るので、MaxCapacity 近くから減らすには長い時間がかかります。
100タスクから10タスクに減らす場合:
| 現在のタスク数 | 1ステップで減らす数 | このフェーズのステップ数 |
|---|---|---|
| 100 | 5 | 1 |
| 95 〜 79 | 4 | 4 |
| 79 〜 58 | 3 | 7 |
| 58 〜 38 | 2 | 10 |
| 38 〜 10 | 1 | 28 |
| 合計 | 50ステップ |
クールダウン設定とメトリクス評価サイクルから、MGReの設定では1ステップあたり実質4分。
50ステップ × 4分 = 200分 = 3時間20分
何が問題か
3時間以上の間、「本当は10タスクで足りる負荷状況」なのに過剰なリソースが残り続けます。「毎日決まった時間帯にスパイク → 低負荷状態が続く」ようなワークロード(プッシュ通知配信後、ECサイトのキャンペーン後など)では、この非対称性が常時コストを押し上げます。
この設計の意図と対処
AWSがこの設計を選んだ理由は明示されていませんが、おそらく:
- スケールアウトが速い → 性能保証(リソース不足はユーザー体験に直結)
- スケールインが遅い → 振動防止と性能保護(減らしすぎて性能劣化させない)
性能とコストのトレードオフで、性能側に寄った設計です。
コスト最適化が必要なら、Scheduled Scalingで時間帯ごとに MinCapacity を下げる、または独自スケーラーで補完する、といった対処が必要になります。
まとめ
ECSのターゲット追跡スケーリングを観察したところ、AWS公式ドキュメントには書かれていない実装ルールがいくつか見えました。
- スケールアウトは計算式に従う:
ceil(現在のタスク数 × メトリクス値 / 目標値)で一気に増やす - スケールインは5%ルール:1ステップで減らせるのは現在のタスク数の5%(最小1)が上限で、計算式ではなく現在のタスク数だけで決まる
-
AlarmHigh/AlarmLowのパラメータは公式に明記なし:観測では3 / 15 datapoints × 60秒、AlarmLowのThresholdは目標値 × 0.9 - 実発火間隔はクールダウン + 1分粒度の合計:
ScaleInCooldown180秒なら4分間隔 - 非対称設計のため、スケールインに数時間かかる:100→10タスクで3時間以上
「ターゲット追跡って、その名のとおり目標値に向けて追従するんでしょ?」というイメージとは違って、上下で挙動がまったく違うので、AutoScalingの動きをイメージするときはこの非対称性を意識すると現実とのギャップが小さくなると思います。
特にコスト最適化が気になる場合は、Scheduled Scalingや独自スケーラーで補完することを検討する価値があります。