ECSで通常時とスパイク時のオートスケールを運用する

こんにちは、サーバサイドエンジニアの@Juju_62qです。 今回はタイミーで実践しているECSのオートスケール戦略についてお話ししようと思います。

TL;DR

  • タイミーではTarget Tracking ScalingとStep Scalingを組み合わせてオートスケールをしています
  • Target Tracking Scaling -> 通常のスケールアウト・スケールイン
  • Step Scaling -> スパイク時のスケールアウト
  • 2つを組み合わせることで、様々なリクエストに対し適切なリソースを用意しています

タイミーのアクセス量の変化とビジネス要求

タイミーのアクセス量の変化とこれまでのオートスケール

タイミーは空いた時間に働きたい人とすぐに人手が欲しい店舗・企業をつなぐスキマバイトアプリです。 したがって、仕事の募集数や働いてくださるワーカーさんの数は世の中の動向に大きく左右されます。

例えば昨年2019年の12月で言えば、忘年会シーズンということもあり飲食店を中心に仕事の募集が大幅に増えました。 重ねてそのタイミングでテレビCMを行いました。伴ってメディア露出も急増しサービスへのアクセスは波があるとは言え常時圧倒的に多かったです。

一方で4月以降はCOVID-19の影響もあり一時的に仕事の募集数、アクセス数共に減少しました。 近頃は職種を多様化させることにより再び盛り上がっていますが、アクセスの傾向は昨年の12月とは異なるものになっています。 具体的には弊社のマーケティングチームが通知やキャンペーンの施策を行ったタイミングでアクセス数が急増するようになりました1。 これまでのタイミーのオートスケールではアクセスの特性上滑らかなアクセス量の増減に対応とすることを目的としてきましたが、アクセスの変化量が大きくなったことにより従来のオートスケールでは対応が難しくなりました。

特に7/20に通知を行ったキャンペーンではその影響は顕著で、アクセスの増加からサーバ増加までのラグが10-15分ありました。 ユーザとしては通知が来たからアプリを開いているので、表示が適切にされなかったり待ち時間が長いのは体験がよくないです。

timee.co.jp

f:id:Juju_62q:20200827173142p:plain:w500

ビジネス要求の変化

前述した通り、タイミーの仕事の募集数、アクセス数はCOVID-19の影響もあり一時的に減少しました。 しかしながらセールスチーム、サポートチーム、マーケティングチームの必死の努力が実り今現在かなり盛り返してきています。

そうした努力の一部にワーカーさんにむけたPush通知やキャンペーンの実施があります。 メディア露出などの明らかに予期できるスパイクについては事前共有をいただいた上でサーバの増強を行ってきましたが、マーケティング施策となるとスピード感も頻度も異なります。 全てのPush通知やキャンペーンを実施する前に開発チームの承認を取るのは非効率です2。 一方でプロダクトの技術的な都合でビジネスチームが必死で考えた施策の効果を低下させるのは言語道断です。

以上を踏まえ、スピード感を持ってビジネス施策を実施でき、その効果をなるべく減少させない環境を作るためにオートスケール設定を見直しました。

ECSのオートスケールポリシー

ECSのオートスケールポリシーはAWSが提供しているものとしては2種類存在します3。 なお、記事内ではオートスケールをコンテナの増減を指す言葉として扱います。VMインスタンス数を増減させるオートスケールは扱いませんのでご了承ください。

  • Step Scaling Policy
  • Target Tracking Scaling Policy

Step Scaling Policy

指定した閾値に基づいてスケールアウト/インを行うオートスケールです。 スケールアウト/インを段階的に定義できるのが特徴で、例えば以下のような設定が可能です。

  • CPUの平均使用率が61-70% -> コンテナを1つ増やす
  • CPUの平均使用率が71-80% -> コンテナを3つ増やす
  • CPUの平均使用率が81%以上 -> コンテナを5つ増やす
  • CPUの平均使用率が50%以下 -> コンテナを1つ減らす

適切な値を設定する難易度は低くないですが、うまく使うとリソースを急激に変化させることができます。

Target Tracking Scaling Policy

指定したメトリクスが指定した数値になるようにスケールアウト/インを行うオートスケールです。 イメージとしてはKubernetesHorizontal Pod Autoscalerが近いと思います。

例えばCPUの平均使用率が60%となるように指定した場合

  • CPUの平均使用率が70% -> スケールアウト
  • CPUの平均使用率が50% -> スケールイン

というような振る舞いをし、CPUの平均使用率が60%になるように努めてくれます4

オートスケールの設定をする時にみなさん頭を悩ませるのがスケールインの閾値だと思いますが、これをある程度いい感じにやってもらえるのが便利です。 タイミーでは12月時点からTarget Tracking Scaling Policyを利用しています。

いい感じにオートスケールしつつスパイクに抗うAutoScale Policyを設定する

以上を鑑みると、リソース使用量が滑らかに変化するオートスケールをTarget Tracking Scalingに任せ、スパイクが発生した場合にはStep Scalingを利用してどかっとリソースを増やすのが良さそうです。 Step Scalingで増やしたコンテナは、Target Tracking ScalingでスケールインしていくためStep Scalingにはスケールアウトの設定だけすればたりそうです。

タイミーのサービスで設定している実際の数値は出しませんが、Terraformでの実装例を載せておきます。 厳密には正確な記述ではありませんが、雰囲気は掴んでいただけると思います。

Target Tracking Scaling

resource "aws_appautoscaling_target" "target" {
  max_capacity = 50
  min_capacity = 5
  resource_id  = "service/${cluster_name}/${service_name}"

  role_arn = ${iam_role_arn}

  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

resource "aws_appautoscaling_policy" "scale" {
  name               = "${service_name}-scale"
  resource_id        = "service/${cluster_name}/${service_name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
  policy_type        = "TargetTrackingScaling"

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }

    // CPUの平均使用率が60%になるように維持する
    target_value       = 60
    // スケールインの間隔は60秒空ける
    scale_in_cooldown  = 60
    // スケールアウトの間隔は30秒空ける
    scale_out_cooldown = 30
  }

  depends_on = [aws_appautoscaling_target.target]
}

Step Scaling

resource "aws_appautoscaling_policy" "spike_scale_out" {
  name               = "${service_name}-spike-scale-out"
  policy_type        = "StepScaling"
  resource_id        = "service/${cluster_name}/${service_name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"

  step_scaling_policy_configuration {
    adjustment_type = "ChangeInCapacity"
    // スケールアウトの間隔は30秒空ける(測定間隔は60秒ですが、バッファを持つため小さい値にする)
    cooldown        = 30
    metric_aggregation_type = "Maximum"

    // CPUの平均使用率が70%-80%の場合コンテナを3つ増やす
    step_adjustment {
      metric_interval_lower_bound = 0
      metric_interval_upper_bound = 10
      scaling_adjustment          = 3
    }

    // CPUの平均使用率が80%-90%の場合コンテナを5つ増やす
    step_adjustment {
      metric_interval_lower_bound = 10
      metric_interval_upper_bound = 20
      scaling_adjustment          = 5
    }

    // CPUの平均使用率が90%-の場合コンテナを10増やす
    step_adjustment {
      metric_interval_lower_bound = 20
      scaling_adjustment          = 10
    }
  }

  depends_on = [aws_appautoscaling_target.main]
}

resource "aws_cloudwatch_metric_alarm" "spike_scale_out_alerm" {
  alarm_name          = "${service_name}-spike-scale-out"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "1"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = "60"
  statistic = "Average"
  // Step ScalingのCPU平均使用率の閾値の基準は70%とする
  threshold = 70
  dimensions = {
    ClusterName = ${cluster_name}
    ServiceName = ${service_name}
  }
  alarm_actions = [aws_appautoscaling_policy.spike_scale_out.arn]
}

この2つの組み合わせで、CPU使用率を普段は60%に維持するようにオートスケールをしつつ、CPU使用率が80%や90%など跳ね上がった際に大きくスケールアウトできるようになります

結果

f:id:Juju_62q:20200827173202p:plain:w500

画像のグラフですが横軸のスケールを合わせるのを失念しておりました。失礼いたしました。

アクセスの増加から、1,2分で大幅なスケールアウトをしています。 結果的にレスポンスタイムの悪化も最小限に止めることができました。

終わりに

複数のスケールポリシーを組み合わせることで、滑らかなアクセス数の増減とスパイクの2つの状況に対応ができるようになりました。 とはいえまだまだ完璧なオートスケールには程遠く、リソースを最適に使えているとはいえない状況です。 より高いコストパフォーマンスを発揮できるようにこれからも勉強していきたいと思います。


  1. アクセスの絶対量の減少により、施策によるアクセス変化がもたらす影響が大きくなった。また、新規登録は継続的にあるので通知対象ユーザ数も増加している。

  2. 想定影響に応じて事前に連絡をいただいたり、単位時間あたりの流量制御もやってます。

  3. APISDK経由でコンテナ数を増減させることでもっと柔軟な設定もできます。

  4. 厳密には異なりますが、イメージはできると思います。