🚀

ECS Fargateスケーリング設計で詰まりやすいポイントと対策──BFF×Quarkusの実践録

に公開

1人アドベントカレンダー2025 8日目。

今日は技術記事です。ECS Fargate × Quarkus の構成でスケーリング設計をしたとき、負荷テストで詰まったポイントと対策をまとめました。


はじめに

BFFパターンQuarkusの構成でECS Fargateのスケーリング設計をしていたとき、負荷テストで予想外の問題が次々と発覚しました。

  • スケールアウトしたのにトラフィックが新タスクに流れない
  • CPU が全然上がらないのにレスポンスが悪化する
  • 一箇所の遅延が全体に伝搬し連鎖的に詰まる
  • 負荷テスト特有の挙動(単一IP+keep-alive)で ALB が偏る

本記事では、ECS Fargateのスケーリング基礎を整理しつつ、
BFF構成や Quarkus(Java) 特有の落とし穴と、その対策を “実際に安定運用できた設定” とともにまとめます。

対象読者

  • ECS Fargate を本番運用している人
  • Auto Scaling がうまく効かず困っている人
  • JVM サーバ(特に Quarkus)を載せている人
  • スパイク時の挙動が安定しない / 負荷テストで偏りが出る人

前提:今回の構成

ポイント:

  • BFF がすべてのリクエストを受ける単一経路
  • バックエンドは BFF 経由でのみコールされる(複数のAPIが存在)
  • BFF は Quarkus(Vert.x/Mutiny のノンブロッキング I/O)
  • 外部 API の I/O 待ち時間が支配的で、スループットが頭打ちになる構成

この構成は柔軟だが、BFF が詰まると全体が詰まる構造的欠陥があるため、スケーリング戦略は慎重に設計する必要があります。

補足:本記事の前提
今回の構成では外部 API 呼び出しの I/O 待ちが支配的だったため、CPU 使用率がスループットの指標として機能しにくい状況でした。本記事はこの「I/O 待ちが支配する構成」でのスケーリング戦略を主題としています。

スケーリング方式の基本と落とし穴

Target Tracking Scaling(まず使うべきだが、Quarkus には罠あり)

一般的には Target Tracking が最も安定します。ただし、CloudWatch メトリクスの反映には 10〜30 秒かかり、Datapoint の評価期間によってはさらに遅れます。

よく使う指標:

  • CPUUtilization
  • RequestCountPerTarget(ALB)
  • MemoryUtilization(Java は詰まりやすい)

しかし Quarkus の場合、最大の落とし穴があります。

負荷が増えても CPU が上がらないため、CPUターゲットが正しく機能しない。

これは Vert.x/Mutiny 特有の非同期処理モデルに起因します(後述)。

なお、今回は CPU 利用率のみをターゲットに使い、MemoryUtilization は監視には使うものの、Auto Scaling の直接のトリガーにはしていません。JVM / Heap の設定で十分な余裕を持たせる方針にしました。

Step Scaling(スパイク対応に必須)

急激な負荷変動には Step Scaling が有効です。

  • 条件ごとに +1 / +2 / +3 タスクなど即応が可能
  • スパイク時の「Target Tracking だけでは遅い」問題を解決できる

スケールアウト/インの速度設計

結論:スケールアウトは早く、スケールインは遅く。

方向 推奨設定 理由
スケールアウト 30〜60秒 遅いと落ちる
スケールイン 5〜10分 早いとスラッシング(振動)が起きる

Fargate は起動に 40〜60秒 かかるため、スパイク検知は早めに行う必要があります。

また、ALB の deregistration delay(デフォルト 300 秒)にも注意が必要です。新タスクが増えても、既存タスクへの keep-alive 接続が張り付いたままになり、スケールイン・アウトの境界が揺れやすくなります。

※ 40〜60秒という数値は今回の構成で観測した値です。ECRイメージサイズやネットワーク状況により前後します。

実際に採用した Auto Scaling 設定(完成版)

今回実際に安定した運用ができた設定は次の通りです。

Target Tracking(CPU 40%)

この構成(Quarkus/Vert.x の非同期I/O)では、CPU が上がりにくいため、一般的な 60〜70% では遅すぎました。

種類 条件 アクション クールダウン
Target Tracking CPU 40% スケールアウト 90秒
Target Tracking CPU 40% スケールイン 300秒

Step Scaling(スパイク即応用)

条件 アクション クールダウン
CPU 30%超過 +1 タスク 60秒
CPU 50%超過 +2 タスク 45秒
CPU 70%超過 +3 タスク 30秒
CPU 20%以下 -1 タスク 300秒
CPU 15%以下 -2 タスク 420秒

注意: この数値はBFF が軽量(主にルーティングとプロキシ処理)で、タスク起動が比較的速かったため可能でした。重い処理を行うサービスや起動が遅いアプリケーションでは、+3 タスク/30秒といった設定は危険です。必ず負荷テストで調整してください。

なぜ Target 40% なのに Step 30% で発火させるのか?
Target Tracking は平均 CPU に反応するため、スパイク初動では反応が遅れます。Step Scaling を先に動かすことで、急激な負荷増加に即応できます。また、Quarkus の場合は CPU 50% 以上になる時点で既に「詰まり始めている」状態になりやすいため、早めにスケールアウトしています。

AWS 公式の注意点:IN/OUT 閾値は十分に離すこと

AWS の Application Auto Scaling のドキュメントでは、Target Tracking と Step Scaling を併用する場合に「スケールインとスケールアウトの閾値に十分な差をつけておかないと挙動が振動する」といった趣旨の注意があります。

今回は 10% 以上の差 を目安にしました。

当初はスケールインが 25% でしたが、警告が出たため 20% に下げました

これにより、Target 40% と Step 30/50/70% に対し十分な差が確保され、
安定したスケーリングになりました。

スケールポリシー全体図(Mermaid)

Quarkus(Java)特有の課題

Event Loop のブロッキング問題と対策

Quarkus の HTTP 実装は Vert.x/Netty ベースの イベントループ方式 です。

少数スレッドでリクエストを捌くため、Event Loop Threadのブロッキングが混ざると即詰まります。
Quarkusの基本原則として、Event Loop Thread 上でブロッキング処理を実行してはいけません

Quarkus でブロッキング処理を安全に行う方法

  • @Blocking アノテーションを付けると、Event Loop ではなくワーカースレッドで実行される
  • 戻り値を Uni / Multi 以外の通常クラスにすると、Quarkus が自動的にブロッキング扱いにする

詳細は Quarkus リアクティブアーキテクチャ を参照してください。

今回の構成でも、当初はアプリ側で意図せず Event Loop をブロッキングしてしまっている箇所があり、これを修正しました。(Uni/Multiを適切に扱えていなかった箇所を修正し、ブロッキングを解除しました)

外部 API の I/O 待ちで CPU が上がらない問題

Event Loop のブロッキング問題を解消した後も、外部 API の I/O 待ちが長いため CPU 使用率が上がらないという課題が残りました。

この構成では:

  • 外部 API の応答待ち時間が支配的で、スループットが頭打ちになる
  • CPU 30% でもスループットは限界に近い状態
  • Target Tracking は CPU を見て「余裕がある」と判断し、スケールアウトが遅れる

「Java なのに CPU が上がらないの?」という疑問について

Java は「スレッドを大量に使うので CPU を食う」というイメージを持たれがちですが、
Vert.x(Quarkus の基盤)は少数スレッドの Event Loop モデルで動作します。
非同期 I/O が中心の場合、処理の大半が “待ち時間” になるため、
CPU はほとんど使われずアイドルに近い状態になります。

この “CPU が上がらない動き” が、従来の Java のイメージと異なる理由です。

→ このため 低め(40%)のターゲット値設定が必要 でした。

対策まとめ

Event Loop のブロッキング対策(必須):

I/O 待ちが支配的な構成でのスケーリング対策:

  • CPU ターゲットを低め(40%)に設定
  • Step Scaling を併用してスパイクに即応
  • BFF → Backend の遅延をサーキットブレーカーで吸収

補足:理想的な構成では
Event Loop を正しく使い、外部 API 依存が少ない構成であれば、CPU をフルに使えるようになり、Target Tracking の設計もシンプルになります。今回は外部 API の I/O 待ちが支配的だったため、CPU 40% という低めの閾値が必要でした。

Fargate タスク起動遅延(40〜60秒)

今回の負荷テストではタスク起動に 40〜60 秒かかるケースがほとんどでした。

内訳:

  • ENI 割り当て
  • ECR イメージ pull
  • JVM 起動
  • Quarkus の DI / 初期化

これにより、

スパイク後にスケールアウトしても、実際に捌き始めるまで1分弱遅れる

ため、Step Scaling で早めに増やす必要がありました。

負荷テストの落とし穴:単一IP + keep-alive による偏り

負荷テスト環境の制約により、

  • クライアント IP が 1 つに固定
  • keep-alive が有効
  • BFF → Backend の接続も keep-alive で再利用

この条件が揃うと、ALB は以下の挙動になります。

結果:

  • BFF は一部のタスクだけ過負荷
  • Backend はスケールアウトしても新タスクが使われない

対策:負荷テストでは Connection: close で偏りを軽減

注意:これは「完全な対策」ではなく「偏り軽減策」です。 TLS セッション再利用など、HTTP レイヤー以外の要因で接続が再利用されることもあります。あくまで「偏りを減らす」効果として認識してください。

※ 今回の負荷テストは「単一IP+keep-aliveの偏り」を再現するための“特殊な構成”です。
本番環境では多数のクライアントIPから ALB に接続され、keep-alive も必須となるため、今回のような極端な偏りは原則起きません。本番設定と混同しないよう注意してください。

const params = {
  headers: { "Connection": "close" }
};
http.get(url, params);

Sticky Session の罠(潜在的な注意点)

今回の案件では、ALB の設定はアプリケーション開発側ではわからない(管理が別チーム)ため、stickiness(セッション固定)が有効かどうかは開発側から確認できない状況でした。また実際に stickiness に起因する事象は観測できていません。

ただし ECS Fargate × ALB では、stickiness が ON になっているとスケールアウト時に新タスクへリクエストが分散されなくなる問題が一般的によく起こることが知られています。

特に、Quarkus BFF のように処理が軽量な API サーバでは、

  • 一部タスクだけにセッションが集中する
  • 新タスクが増えても負荷が偏ったまま
  • スケールが効かず 503 が発生し続ける

といった事象が起こりやすく、本番構成では stickiness を OFF にすることが多いです。

今回の構成でも 潜在的に気をつけるべきポイント として整理しておきます。

対策チェックリスト

□ Target Tracking は CPU 40% に設定
□ Step Scaling の IN/OUT は 10%以上離す
□ スケールアウトは短く、スケールインは長く
□ Mutiny の隠れブロッキングを排除する
□ JVM Heap/GC は十分な余裕を確保
□ タスク起動は40〜60秒かかる前提で設計
□ 負荷テストでは Connection: close を使用
□ Sticky Session が有効な場合は注意(潜在的リスク)
□ BFF→Backend 遅延が全体遅延に連鎖する点に注意

まとめ

ECS Fargate の Auto Scaling は「設定すれば動く」ものではなく、
アプリケーション特性(特に Quarkus のようなノンブロッキング I/O)と組み合わせた時に初めて本質が見えてきます。

今回のポイント:

  • 外部 API の I/O 待ちが支配的な構成では CPU が上がりにくい → CPU 40% ターゲットが必要
  • スパイクは Step Scaling で即応
  • IN/OUT 閾値は 10%以上離さないと揺れる
  • 単一IP + keep-alive の負荷テストは本番と挙動が違う
  • Fargate 起動は1分弱かかるので早めにスケールする必要がある

これらを踏まえた設計により、BFF × Quarkus × ECS Fargate の構成でも安定したスケーリングを実現できます。

まずは CPU ターゲットを 40〜45% にして負荷テストしてみると、Quarkus の特性がつかみやすいと思います。

補足(2025/12/9 追記)

公開後、Quarkus の Event Loop とブロッキング処理に関する補足コメントをいただきました。

  • Event Loop Thread 上でブロッキング処理を実行してはいけない(Quarkus の基本原則)
  • @Blocking アノテーションを使えば、ワーカースレッドで安全にブロッキング処理を実行できる
  • Event Loop を正しく使えば CPU をフルに使える構成も可能

本記事では「外部 API の I/O 待ちが支配的で CPU が上がらない構成」でのスケーリング戦略を主題としていますが、初稿では Event Loop のブロッキング問題と I/O 待ちの問題を区別せず、混合したまま"ブロッキング"と表現していたため、誤解を招く書き方になっていました。ご指摘を踏まえて、両者を明確に分離する形で記事を修正しています。

参考


明日は「Leafletで作るコンビニ分布マップ──MarkerClusterで16,000件を可視化する」を書く予定です。

📚 1人アドベントカレンダー2025 全記事一覧


megusunu

🐦 X (@megusunu)
🧶 megusunuLab(ハンドメイド)
🏢 Wells合同会社

GitHubで編集を提案

Discussion