SRE Lounge #11 で「安定・安価なECS auto scalingを目指して」を発表しました

SRE Lounge #11 で Repro で行っている ECS の auto scaling について発表しました。ECS autoscaler で工夫している点についてがメインですが、一般的な auto scaling にも使える知見もあるかと思います。

補足

時間の都合上、発表で言及しなかったこともあるので補足です。

ECS_ENABLE_SPOT_INSTANCE_DRAINING について

発表でも軽く触れましたが、ecs-agent 1.32.0 以降を使っている場合、ECS_ENABLE_SPOT_INSTANCE_DRAINING パラメータに true を指定することで、spot instance の interruption warning (notice) を受け取った場合に勝手に draining 状態にしてくれます。
cf. Amazon ECS supports Automated Draining for Spot Instances running ECS Services

ただ、instance を draining 状態にするとリソースが減るので、auto scaling group でクラスタのサイズを管理する場合、自前で draining にしつつ auto scaling group からも外して新しい instance を追加する方がベターだと思います。
ECS autoscaler ではそのようにしています。
cf. Detach interrupted instances from ASG to launch other instances by abicky · Pull Request #55 · reproio/ecs_deploy

よって、ECS_ENABLE_SPOT_INSTANCE_DRAINING は自前で draining しようとして失敗した時のセーフティーネットとして利用するのが良いでしょう。

tasks の起動に 5 分以上かかることがあることを検証したスクリプト

検証スクリプトの全容は次のような感じです。スライドに載せているものにちょっと手を加えてますが、やっていることは変わらないです。

require 'aws-sdk-ecs'

CLUSTER = ENV['ECS_CLUSTER']
SERVICE = ENV['ECS_SERVICE']

client = Aws::ECS::Client.new
client.register_task_definition(
  family: SERVICE,
  container_definitions: [
    {
      name: 'main',
      image: 'k8s.gcr.io/pause:3.1',
      essential: true,
      cpu: 64,
      memory: 64
    },
  ],
)

client.create_service(cluster: CLUSTER, service_name: SERVICE, task_definition: SERVICE, desired_count: 0)

logger = Logger.new($stdout)

container_instance_arns = client.list_container_instances(cluster: CLUSTER).container_instance_arns
10.times do |i|
  logger.info "#{i + 1}th try"

  sleep_sec = i * 10

  client.update_container_instances_state(
    cluster: CLUSTER,
    container_instances: container_instance_arns,
    status: 'DRAINING',
  )

  logger.info "Update desired_count to 1"
  client.update_service(cluster: CLUSTER, service: SERVICE, desired_count: 1)
  updated_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)

  logger.info "Sleep #{sleep_sec} seconds"
  sleep sleep_sec

  client.update_container_instances_state(
    cluster: CLUSTER,
    container_instances: container_instance_arns,
    status: 'ACTIVE',
  )

  client.wait_until(:services_stable, { cluster: CLUSTER, services: [SERVICE] }, delay: 1, max_attempts: nil)
  elapsed_sec = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - updated_time).round
  logger.info "The service became stable (#{elapsed_sec} seconds elapsed after service update)"

  task_arns = client.list_tasks(cluster: CLUSTER, service_name: SERVICE, desired_status: 'RUNNING').flat_map(&:task_arns)
  logger.info "Update desired_count to 0"
  client.update_service(cluster: CLUSTER, service: SERVICE, desired_count: 0)
  client.wait_until(:tasks_stopped, cluster: CLUSTER, tasks: task_arns, max_attempts: nil)
end

当時は ecs-agent 1.29.1 の入った ami-04a735b489d2a0320 を使って検証した気がしますが、最新版の ecs-agent 1.32.1 の入った ami-06c98c6fe6f20c437 で試してみても同じような結果になりますね。

I, [2019-11-03T20:26:01.992737 #24617]  INFO -- : 1th try
I, [2019-11-03T20:26:02.047399 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:26:02.149503 #24617]  INFO -- : Sleep 0 seconds
I, [2019-11-03T20:26:24.043734 #24617]  INFO -- : The service became stable (22 seconds elapsed after service update)
I, [2019-11-03T20:26:24.062389 #24617]  INFO -- : Update desired_count to 0
I, [2019-11-03T20:27:39.199460 #24617]  INFO -- : 2th try
I, [2019-11-03T20:27:39.235235 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:27:39.280391 #24617]  INFO -- : Sleep 10 seconds
I, [2019-11-03T20:28:19.079676 #24617]  INFO -- : The service became stable (40 seconds elapsed after service update)
I, [2019-11-03T20:28:19.102544 #24617]  INFO -- : Update desired_count to 0
I, [2019-11-03T20:29:28.269465 #24617]  INFO -- : 3th try
I, [2019-11-03T20:29:28.297482 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:29:28.344527 #24617]  INFO -- : Sleep 20 seconds
I, [2019-11-03T20:30:07.598579 #24617]  INFO -- : The service became stable (39 seconds elapsed after service update)
I, [2019-11-03T20:30:07.619196 #24617]  INFO -- : Update desired_count to 0
I, [2019-11-03T20:30:51.086894 #24617]  INFO -- : 4th try
I, [2019-11-03T20:30:51.137439 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:30:51.187962 #24617]  INFO -- : Sleep 30 seconds
I, [2019-11-03T20:31:31.058341 #24617]  INFO -- : The service became stable (40 seconds elapsed after service update)
I, [2019-11-03T20:31:31.085816 #24617]  INFO -- : Update desired_count to 0
I, [2019-11-03T20:32:46.410962 #24617]  INFO -- : 5th try
I, [2019-11-03T20:32:46.446727 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:32:46.487350 #24617]  INFO -- : Sleep 40 seconds
I, [2019-11-03T20:33:38.271333 #24617]  INFO -- : The service became stable (52 seconds elapsed after service update)
I, [2019-11-03T20:33:38.299204 #24617]  INFO -- : Update desired_count to 0
I, [2019-11-03T20:34:53.301309 #24617]  INFO -- : 6th try
I, [2019-11-03T20:34:53.346061 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:34:53.394063 #24617]  INFO -- : Sleep 50 seconds
I, [2019-11-03T20:40:19.445940 #24617]  INFO -- : The service became stable (326 seconds elapsed after service update)
I, [2019-11-03T20:40:19.463952 #24617]  INFO -- : Update desired_count to 0
I, [2019-11-03T20:41:28.606239 #24617]  INFO -- : 7th try
I, [2019-11-03T20:41:28.653191 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:41:28.690279 #24617]  INFO -- : Sleep 60 seconds
I, [2019-11-03T20:42:35.299096 #24617]  INFO -- : The service became stable (67 seconds elapsed after service update)
I, [2019-11-03T20:42:35.316733 #24617]  INFO -- : Update desired_count to 0
I, [2019-11-03T20:43:18.723598 #24617]  INFO -- : 8th try
I, [2019-11-03T20:43:18.755883 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:43:18.800947 #24617]  INFO -- : Sleep 70 seconds
I, [2019-11-03T20:49:07.600974 #24617]  INFO -- : The service became stable (349 seconds elapsed after service update)
I, [2019-11-03T20:49:07.833891 #24617]  INFO -- : Update desired_count to 0
I, [2019-11-03T20:50:23.332626 #24617]  INFO -- : 9th try
I, [2019-11-03T20:50:23.360826 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:50:23.405506 #24617]  INFO -- : Sleep 80 seconds
I, [2019-11-03T20:56:35.232263 #24617]  INFO -- : The service became stable (372 seconds elapsed after service update)
I, [2019-11-03T20:56:35.250612 #24617]  INFO -- : Update desired_count to 0
I, [2019-11-03T20:57:50.094668 #24617]  INFO -- : 10th try
I, [2019-11-03T20:57:50.124154 #24617]  INFO -- : Update desired_count to 1
I, [2019-11-03T20:57:50.160448 #24617]  INFO -- : Sleep 90 seconds
I, [2019-11-03T21:03:40.488997 #24617]  INFO -- : The service became stable (350 seconds elapsed after service update)
I, [2019-11-03T21:03:40.517277 #24617]  INFO -- : Update desired_count to 0

リソース不足が原因で tasks が 5 分以上起動しない問題はデプロイ時にも起きる

ECS service の更新時に deploymentConfigurationmaximumPercent に 200、minimumHealthyPercent に 50 を指定しているケースがけっこうあるんじゃないかと思うんですが、この設定で複数の service を同時に更新すると、それらの service の task が 2 倍起動するだけのリソースがない限りデプロイがめちゃくちゃ遅くなる可能性があります。scale-out の際にリソースが足りないのに desired count を増やすと task の起動に時間がかかるのと理由は同じです。

デプロイの際に、古い revision の task が終了してリソースが余っているはずなのになかなか新しい revision の task が起動しない場合、deploymentConfiguration を見直したり、デプロイ時に一時的にリソースを増やしたりするとデプロイ速度が劇的に改善するかもしれません。

AZRebalance に tasks を強制終了させられない方法について

発表では spot instance を使うなら次のどちらかだろうという話をしました。

  • auto scaling groups の lifecycle hooks て ゙instancesをdraining する
  • AZRelabance を suspend する

が、よく考えたら task が動いている instance を instance protection で保護することでも回避できる気がしますね。

インスタンスを draining した時の注意点

spot instance が落とされる時などに container instance を draining するのは必須なわけですが、daemon type の service が動いていると、他の service より先にその service が終了することがあります。
よって、例えば fluentd を daemon type の service として動かしていると、fluentd にデータを post している service よりも先に fluentd が終了してデータが消失するという問題が起きます。
ECS タスクの終了時にコンテナの依存関係が考慮されない問題を解決するコマンドを作ったの前に fluentd を daemon type で動かすことを考えたんですが、この問題に気付いて ecswrap を作ったという経緯があります。

というわけで、daemon type の service を作成する際は十分注意しましょう。
daemon type の service にも enhanced container dependecy management みたいなことができると良いんですけどね…。

追記

2020 年 11 月に、全ての task が終了してから daemon service の task が終了するようになったみたいです

In November of 2020, we added an update to the ECS scheduler to drain daemon tasks last on a host. This ensures that when an instance is set to drain, ECS will terminate all other tasks before terminating the daemon task.

cf. Improving daemon services in Amazon ECS | Containers

その他の ECS 関連エントリー

発表では auto scaling group で spot instance を使う前提でした。特定の instance type だけだと spot instance が枯渇した場合に困るので、Repro では複数の instance type を利用しています。その方法については次のエントリーを見てもらうと良いと思います。

Launch Templates と Auto Scaling Groups で Spot Fleet Requests みたいなことをする

また、task を安全に終了するにはコンテナの終了順序に気を付けないといけない場合もあると思いますが、依存関係を考慮して終了させる方法については次のエントリーが参考になると思います。

ECS の Enhanced Container Dependency Management でタスク終了時のコンテナの依存関係を考慮する

もっと色々知りたい方

実際にそれなりの規模で ECS をガッツリ利用しているサービスの開発・運用をしてみるのが一番です!!そんなわけで、以下の求人をポチッとな。