はじめに
こんにちは、インフラエンジニアのなみきです。
私の担当プロダクトでは、ステージング環境のAuto Scaling Groupでスポットインスタンスを利用しています。 当初、ステージング環境では費用の安さを優先して意図的にベストプラクティスから外れた設定にしていました。 そんな中、スポットインスタンスの起動に関する2つの問題に遭遇しました。
結論としては単純に「ベストプラクティスに従いましょう」というだけの話ですが、一見して何が悪いのかすぐには分かりませんでした。 したがって、今後同じ問題に遭遇した人の役に立つよう、「こんな設定にするとこんな問題が起きる」という一例として紹介したいと思います。
結論
Auto Scaling Groupの設定を以下のように変更しました。
- 対応前
- 割り当て戦略(SpotAllocationStrategy):
lowest-price
- インスタンスタイプ:
t4g.medium
- 割り当て戦略(SpotAllocationStrategy):
- 対応後
- 割り当て戦略(SpotAllocationStrategy):
price-capacity-optimized
- インスタンスタイプ:
t4g.medium / t4g.large / c6g.large / m6g.large
- 割り当て戦略(SpotAllocationStrategy):
ステージング環境では安定性よりも費用の安さを優先して、意図的に lowest-price
かつインスタンスタイプ1種類としていました。
ですが後述の問題が発生したため、割り当て戦略を変更し、インスタンスタイプを増やしました。
問題1: インスタンスが無限に起動と終了を繰り返す
ある時、ステージング環境のAuto Scaling Groupにおいて、異常な頻度でスポットインスタンスが起動と終了を繰り返していることに気付きました。
遡って確認すると05/16から発生しており、徐々に発生時間が長くなっていることが分かりました。
スポットインスタンスを使っているのはステージング環境だけなので本番ユーザーへの影響はありませんが、不安定な状態が続くことで予期せぬ問題が発生すれば開発に支障が出るため、詳しく調査することにしました。
リバランスレコメンデーションの意図せぬ挙動
Auto Scaling Groupのアクティビティ履歴を確認すると、およそ7分おきに次のようなイベントが発生していました。
At YYYY-MM-DDThh:mm:ssZ an instance was launched in response to an EC2 instance rebalance recommendation.
At YYYY-MM-DDThh:mm:ssZ an instance was taken out of service in response to an EC2 instance rebalance recommendation.
「rebalance recommendation」とは、スポットインスタンスの中断リスクが高まったときに、中断リスクの低い別のスポットインスタンスに置き換えを試みる機能です。
今回、起動と終了を繰り返すスポットインスタンスはすべてap-northeast-1aに偏っていたため、ap-northeast-1aでスポットインスタンスのキャパシティを確保しづらい状態が続いていたことが分かります。
このとき以下のようなループが発生していました。
- 中断リスクが高いスポットインスタンスに対し、リバランスレコメンデーションが発生
- 中断リスクが高いスポットインスタンスが終了する
- Auto Scaling Group内でAZのバランスをとるため、同じAZにインスタンスを起動する
- ap-northeast-1aで起動したスポットインスタンスが、中断リスクが高いと判断される
- (1へ戻る)
Auto Scaling Groupの設定見直し
この時点でのAuto Scaling Groupの設定は以下のとおりです。 前述の通り、ステージング環境では費用の安さを優先してこのような設定にしていました。
- 割り当て戦略(SpotAllocationStrategy):
lowest-price
- インスタンスタイプ:
t4g.medium
割り当て戦略を lowest-price
とすると、スポットインスタンスの中断リスクが高くなります。
lowest-price 割り当て戦略を使用している場合、代替スポットインスタンスが中断するリスクが高くなることがあります。これは、代替スポットインスタンスが起動後すぐに中断される可能性が高い場合も、必ず、その時点で利用可能な容量を持つ最低料金のプールでインスタンスを起動することが原因です。中断のリスクが高くなるのを避けるため、lowest-price 配分戦略を使用せず、代わりに price-capacity-optimized、capacity-optimized、capacity-optimized-prioritized 配分戦略を使用することを強くお勧めします。
中断リスクを許容した上で lowest-price
を選んでいましたが、ここまで頻繁に起動と終了を繰り返すのは想定外のため、まずは割り当て戦略を公式推奨の price-capacity-optimized
に変えてみました。
問題2: スポットインスタンスのキャパシティ不足
今度は別の問題が発生しました。
Auto Scaling Groupのアクティビティ履歴に Failed
が連続して発生するようになったのです。
(説明) Launching a new EC2 instance. Status Reason: Could not launch Spot Instances. UnfulfillableCapacity - Unable to fulfill capacity due to your request configuration. Please adjust your request and try again. Launching EC2 instance failed.
(原因) At YYYY-MM-DDThh:mm:ssZ instances were launched to balance instances in zones ap-northeast-1a ap-northeast-1d with other zones resulting in more than desired number of instances in the group. At YYYY-MM-DDThh:mm:ssZ availability zones ap-northeast-1a ap-northeast-1d had 0 2 instances respectively. An instance was launched to aid in balancing the group's zones.
スポットインスタンスの起動と終了を繰り返すのは収まりましたが、かわりにスポットインスタンスの起動ができなくなりました。
ここでは以下のようなループが発生していました。
- ap-northeast-1dでスポットインスタンスを起動する
- ap-northeast-1aに0台、ap-northeast-1dに2台、とAZが偏る
- AZのリバランス機能(AZRebalance)により、ap-northeast-1aに起動しようとする
- キャパシティ不足により起動失敗
- (3へ戻る)
lowest-price
では中断リスクが高いながらも起動できていたのに、 price-capacity-optimized
では起動すらできなくなるのは不思議ですが、おそらくスポットリクエストの内部処理が異なるのでしょう。
Auto Scaling Groupの設定見直し
この時点でのAuto Scaling Groupの設定は以下のとおりです。 割り当て戦略は変更しましたが、インスタンスタイプは1種類のままです。 安いインスタンスを使ってほしいためこのような設定にしていました。
- 割り当て戦略(SpotAllocationStrategy):
price-capacity-optimized
- インスタンスタイプ:
t4g.medium
キャパシティ不足の対策としては、より多くのAZを使うこと、より多くのインスタンスタイプを使うことが有効とされています。
ワークロードを設定する際には、リクエストするインスタンスタイプとデプロイするアベイラビリティーゾーンを柔軟に決定してください。 例えば、us-east-1a で m5.large をリクエストする代わりに、複数のアベイラビリティーゾーンで m4.large、c5.large、r5.large、または t3.xlarge をリクエストします。このタイプのリクエストにより、Amazon Web Services (AWS) が必要とする量のコンピューティングキャパシティを見つけて割り当てる可能性が高まります。
具体的に、スポットプール数(AZ数 x インスタンスタイプ数)が20を超えるよう設定すべしという記事もあります。
AWS ではスポットプール数について最低 20、可能なら 30 を⽬指すことをお勧めしてます。 ※スポットプール数 = インスタンスタイプの数 x AZ の数
ap-northeast-1で利用可能なAZは3つなので、インスタンスタイプが1種類の場合のスポットプールの数は 3 x 1 = 3 です。 さすがにこれでは少なすぎたため、起動テンプレートを上書きする形で、インスタンスタイプの選択肢を追加しました。
- t4g.medium
- t4g.large
- c6g.large
- m6g.large
このAuto Scaling GroupではArm64のAMIを使っているため、Gravitonベースかつなるべくt4g.mediumに近い性能のインスタンスタイプを選びました。 世代を問わないAMIを使っていれば、前世代のインスタンスタイプも含めるとさらに安定すると思います。
スポットプール数は 3 x 4 = 12 となり、20には満たないものの最初に比べると選択肢が大幅に増えました。
設定変更後…
t4g.mediumのキャパシティが確保しづらいap-northeast-1aでは、別のインスタンスタイプが起動するようになりました。
インスタンス数のメトリクスからも、高頻度の増減がなくなって安定していることが分かります。
おわりに
例えば「本番環境ではマルチAZで冗長構成、非本番環境では1台構成」というように、非本番環境ではベストプラクティスよりもコスト削減を優先することはよくあることかと思います。 ですが、スポットインスタンスの利用においてはベストプラクティスに従わないと想定以上に安定性を欠くことが分かりました。
同じ問題で悩む人の参考になれば幸いです。
付録
対応後のCloudFormationテンプレートを一部抜粋して掲載します。
AutoScalingAutoScalingGroup: Type: 'AWS::AutoScaling::AutoScalingGroup' Properties: CapacityRebalance: true MixedInstancesPolicy: InstancesDistribution: SpotAllocationStrategy: 'price-capacity-optimized' LaunchTemplate: LaunchTemplateSpecification: LaunchTemplateId: !Ref EC2LaunchTemplate # 補足: AWS::EC2::LaunchTemplateのInstanceTypeでt4g.mediumを指定している Version: !GetAtt EC2LaunchTemplate.LatestVersionNumber Overrides: - InstanceType: 't4g.medium' - InstanceType: 't4g.large' - InstanceType: 'c6g.large' - InstanceType: 'm6g.large'