Pular para o conteúdo
Escalabilidade no ECS

Escalabilidade no ECS

Escrito pelo Claude Code em 18/10/2026, a partir dos testes de carga e de falha executados no ambiente dev do arglabs-workload entre 16 e 18 de outubro de 2026: nove rodadas, quatro relatórios, mais de 24 milhões de requisições. O módulo é o arglabs-tfmod-ecs-app; o arglabs-workload é o template que todo repositório de app do ARGLabs copia.

Com que velocidade um serviço ECS Fargate adiciona tasks quando a carga dispara, o que cada opção de scaling do módulo muda e o que o próprio app precisa fazer. Todos os números abaixo foram medidos, no mesmo serviço, com os mesmos testes, a partir de um único cliente.

1. Resumo

O scaling rápido é a opção que importa. As políticas que a AWS oferece prontas leem métricas de um minuto que chegam ao CloudWatch cerca de 3 minutos atrasadas, então reagem em 3,5 a 5,5 minutos. Com fast_scaling_metrics_namespace definido e o app publicando três métricas a cada 10 s, um serviço saturado foi de 2 para 10 tasks em 39 s, todas atendendo em menos de 2 minutos; com a opção desligada, o mesmo teste levou 4 min 41 s até o primeiro scale-out e 10 min 29 s até 10 tasks saudáveis. Sob um pico de 600 requisições em andamento, o serviço respondeu 3,7 vezes mais requisições nos mesmos 6 minutos, com mediana de 124 ms contra 602 ms.

Nenhuma opção de scaling conserta um app que não aguenta a carga. O primeiro pico perdeu 1.367 requisições por causa de um listen backlog de 5; o segundo reduziu o throughput pela metade, com 300 threads por task disputando 0,25 vCPU. O app final (um worker, keep-alive, requisições lentas repassadas, um drain gracioso) atende cerca de 450 requisições por segundo por task com 45% menos CPU por requisição, e não perdeu nenhuma requisição em nenhum pico depois disso.

Primeiro scale-out, CPU a 100%
39 s
com scaling rápido; 4 min 41 s sem ele, no mesmo código. O passo de emergência de CPU da primeira rodada levou 3 min 45 s.
Pico de 600 contra 2 tasks
0 erros
1,24 milhão de requisições a 3.443 req/s em 6 minutos. A primeira rodada perdeu 1.367 e serviu 1.316 req/s.
Uma task, 0,25 vCPU
~450 req/s
a 100% de CPU, contra cerca de 300 quando as rodadas de scaling rápido começaram. A CPU por requisição foi de 246 para 136 µs (medida localmente).
De volta de 10 para 2 tasks
34 a 38 min
depois que a carga termina: o scale-in fica com o target tracking, lento por projeto.

2. O ambiente de teste

ItemValor
Serviçoarglabs-workload em dev, us-east-1: ECS Fargate ARM64 (Graviton2), 0,25 vCPU e 512 MB por task, 100% Fargate Spot, 2 a 10 tasks
Tasko app de exemplo (Python, biblioteca padrão) mais dois sidecars: Fluent Bit (logs) e ADOT (traces); o container do app recebe 334 MB
Load balancerALB público, least outstanding requests, health check /healthcheck a cada 5 s com timeout de 2 s
Clienteum Mac no Brasil, cerca de 120 ms até us-east-1 (o piso de toda latência abaixo), um gerador multiprocesso com conexões keep-alive
Observaçãoum coletor a cada 5 s (serviço, tasks, targets, atividades de scaling, alarmes), uma sonda de 1 req/s (o que um usuário vê), métricas do CloudWatch coletadas após cada teste

Os testes, cada um começando com 2 tasks, salvo indicação em contrário:

TesteO que faz
CPUcpuburn em cada task, as novas inclusive, por 8 a 10 minutos, sem carga de requisições
Memóriamemburn=230 (MB) em cada task por 9 minutos
Pico600 requisições em andamento desde o primeiro segundo, por 6 minutos (closed loop: cada conexão envia a próxima requisição quando recebe uma resposta)
Rampa, 10 tasks20 a 600 em andamento com o serviço fixo em 10 tasks: a capacidade
Rampa de taxa fixa100, 200, 400, 600, 900, 1200 e 1600 req/s (open loop: as requisições saem no relógio e saem atrasadas quando todas as conexões estão esperando)
Flood3000 requisições que dormem 40 s, todas de uma vez
Falhas13 formas de matar tasks, uma release que nunca fica pronta, uma release que responde 500, interrupções de Spot

3. Por que as políticas padrão são lentas

O ECS publica CPUUtilization e MemoryUtilization e o ALB publica RequestCountPerTarget como datapoints de um minuto. Nestes testes, o datapoint de um minuto chegou ao alarme cerca de 3 minutos depois do fim daquele minuto. Tudo o que é construído sobre eles herda o atraso:

Política em métricas da AWSO que precisaMedido desde o início da carga
Target tracking em requisições3 datapoints acima do alvo4 min 30 s com 10 req/s contra um alvo de 100 por minuto por task; 5 min 31 s a 5 min 43 s com a carga três vezes um alvo de 1000 desde o primeiro segundo
Target tracking em memória3 datapoints5 min 30 s, depois uma task por vez (4 tasks a 76% contra 70% pedem 5)
Passo de emergência de CPU (enable_emergency_step_scaling)1 datapoint3 min 45 s (primeira rodada), 4 min 41 s (rodada liga/desliga); depois cada passo espera o próximo datapoint atrasado: 2, 4, 8, 10 tasks em 10 minutos
Passo de emergência de contagem de requisições (emergency_request_count_threshold)1 datapoint3 min 45 s a 4 min: não antes do passo de CPU: a métrica do ALB chega tão atrasada quanto a de CPU
Scale-in (toda política de target tracking)15 datapoints abaixo de 90% do alvo10 para 2 tasks em 34 a 38 minutos, uma task a cada 2 minutos aproximadamente

Os dados atrasados também agem depois do fato. Um alarme de emergência de CPU voltou para ALARM 1 min 23 s depois que uma carga havia terminado, com um datapoint de 3 minutos antes, e levou um serviço ocioso de 2 para 4 tasks. Na primeira rodada, um AlarmLow de memória ainda ligado desde antes de um teste de memória fez a política de CPU remover duas tasks enquanto a memória estava a 76%: o Application Auto Scaling faz scale-in quando todas as políticas de target tracking concordam, e uma delas estava lendo o passado.

4. Instrumentando o app

O scaling rápido move o sinal para dentro da task: o app mede a si mesmo e publica três métricas de alta resolução no CloudWatch, e o módulo escala com alarmes de 10 segundos sobre elas.

O contrato

MétricaUnidadeO que é
CpuUtilizationPercentCPU da task inteira (todos os containers, sidecars incluídos) como porcentagem da vCPU da task, a partir do /task/stats do task metadata endpoint, como a métrica do ECS calcula
MemoryUtilizationPercentmemória da task inteira sem o cache de arquivos recuperável (inactive_file), como porcentagem da memória da task
RequestsInFlightCountrequisições sendo atendidas mais as conexões esperando na accept queue do kernel, amostradas a cada segundo e publicadas como um statistic set (contagem de amostras, soma, mínimo, máximo)
  • Namespace e dimensões são o contrato. O módulo injeta METRICS_NAMESPACE (<scope>/<app>), METRICS_INTERVAL (10), METRICS_DIMENSIONS (ClusterName=<cluster>,ServiceName=<service>) e METRICS_BURST_THRESHOLD. Toda task publica sob as mesmas dimensões, então a estatística de um alarme em um período é uma estatística sobre as tasks; os alarmes do módulo usam a mediana (p50).
  • StorageResolution 1, a cada 10 s, a partir de uma thread própria.
  • Publique na hora em um burst. Quando as requisições em andamento chegam a METRICS_BURST_THRESHOLD (o módulo o define a partir de fast_scaling_inflight_threshold), o publicador não espera o fim do seu intervalo, no máximo uma vez a cada 3 s: o alarme vê o pico no período em que ele começa, cerca de 10 s antes.
  • Fique quieto nos primeiros 20 s. Os sidecars e o interpretador começam com um burst de CPU (81% da task no primeiro intervalo): publicado, toda task nova pareceria saturada e todo deployment faria scale-out.
  • Conte a fila. Uma requisição esperando na accept queue é carga que a task ainda não tocou; deixá-la de fora faz uma task saturada parecer ociosa.

O módulo concede cloudwatch:PutMetricData à task role com a condição cloudwatch:namespace igual ao namespace (a API não aceita ARN de recurso). O essencial do que o app de exemplo faz, como um esboço sem tratamento de erros:

import json, os, threading, time, urllib.request
import boto3

NAMESPACE = os.environ.get("METRICS_NAMESPACE", "")
INTERVAL = float(os.environ.get("METRICS_INTERVAL", "10"))
BURST = int(os.environ.get("METRICS_BURST_THRESHOLD") or 0)
DIMS = [{"Name": k, "Value": v} for k, v in (p.split("=", 1)
        for p in os.environ.get("METRICS_DIMENSIONS", "").split(",") if "=" in p)]
in_flight = 0  # the request handler adds 1 on entry and subtracts 1 on exit (under a lock)


def meta(path):
    with urllib.request.urlopen(os.environ["ECS_CONTAINER_METADATA_URI_V4"] + path, timeout=2) as r:
        return json.load(r)


def publish_forever():
    cw = boto3.client("cloudwatch")
    limits = meta("/task")["Limits"]  # {"CPU": 0.25, "Memory": 512}
    started, last, prev, samples = time.monotonic(), time.monotonic(), None, []
    while True:
        time.sleep(1)
        samples.append(in_flight)  # plus the accept queue, if the server has one
        now = time.monotonic()
        burst = BURST and samples[-1] >= BURST and now - last >= 3
        if now - last < INTERVAL and not burst:
            continue
        last = now
        stats = [c for c in meta("/task/stats").values() if c]
        cpu_ns = sum(c["cpu_stats"]["cpu_usage"]["total_usage"] for c in stats)
        mem = sum(c["memory_stats"]["usage"] - c["memory_stats"]["stats"].get(
            "total_inactive_file", c["memory_stats"]["stats"].get("inactive_file", 0)) for c in stats)
        data = [{"MetricName": "RequestsInFlight", "Dimensions": DIMS, "Unit": "Count",
                 "StorageResolution": 1,
                 "StatisticValues": {"SampleCount": len(samples), "Sum": sum(samples),
                                     "Minimum": min(samples), "Maximum": max(samples)}}]
        if prev and now - started >= max(2 * INTERVAL, 20):
            cpu = 100 * (cpu_ns - prev[0]) / ((now - prev[1]) * 1e9) / limits["CPU"]
            for name, value in (("CpuUtilization", cpu),
                                ("MemoryUtilization", 100 * mem / (limits["Memory"] * 2**20))):
                data.append({"MetricName": name, "Dimensions": DIMS, "Unit": "Percent",
                             "StorageResolution": 1, "Value": round(min(value, 100), 2)})
        prev, samples = (cpu_ns, now), []
        cw.put_metric_data(Namespace=NAMESPACE, MetricData=data)


if NAMESPACE:
    threading.Thread(target=publish_forever, daemon=True).start()

Qualquer linguagem serve: o contrato são os três nomes, suas unidades e as dimensões. Um app que não publica nada deixa os alarmes sem dados (notBreaching), então nada acontece e nada quebra; para esse app, mantenha o scaling rápido desligado e o passo de emergência de CPU ligado.

O que custa, a preços de tabela de us-east-1: uma chamada PutMetricData por task a cada 10 s são cerca de 259.000 chamadas por mês, USD 2,60 por task-mês a USD 0,01 por 1.000; mais três métricas customizadas e três alarmes de alta resolução por serviço e região, USD 1,80 por mês. É mais ou menos o que custa uma task Spot de 0,25 vCPU; um intervalo de 30 s divide as chamadas por três, e os alarmes então usam períodos de 30 s.

5. As opções, uma a uma

Inputs do módulo arglabs-tfmod-ecs-app, o padrão, o valor no arglabs-workload (dev) e o que cada um fez nos testes.

Scaling rápido

InputPadrãoWorkloadO que fez
fast_scaling_metrics_namespace"" (desligado)<scope>/<app> (enable_fast_scaling por ambiente)Liga tudo o que vem abaixo: as variáveis de ambiente, a permissão IAM, uma step policy por métrica e o target tracking nas métricas do app. Primeiro scale-out 32 a 52 s depois que uma carga saturante começou, contra 3,5 a 5,5 minutos
fast_scaling_metrics_interval1010Segundos entre publicações; períodos de alarme de 10 s até 10, 30 s até 30, 60 s acima
fast_scaling_evaluation_periods / fast_scaling_datapoints_to_alarm3 / 21 / 1Com 2 de 3, o Application Auto Scaling agrega os três datapoints, os baixos inclusive, e um agregado abaixo do limite não encontrou nenhum passo (“No step adjustment found for metric value”): o primeiro scale-out esperou um minuto. Agora o módulo tem passos abaixo do limite também. 1 de 1 dispara no primeiro datapoint de 10 s acima do limite; o CloudWatch avalia um período cerca de 17 s depois que ele termina
fast_scaling_statisticp50p50Uma task nova lê baixo até receber tráfego e puxou um Average para baixo do limite enquanto as tasks com carga continuavam saturadas; a mediana continua lendo elas
fast_scaling_step_percent100200Tasks adicionadas por disparo, como porcentagem das que estão rodando: 200 triplica o serviço (2, 6, 10)
fast_scaling_saturated_step_percent0 (desligado)400Adicionado no lugar, bem acima dos limites (CPU ou memória 20 pontos acima do alvo, o dobro do limite de requisições em andamento): 2 para 10 em um passo na CPU e em um pico, em 36 a 49 s
fast_scaling_inflight_threshold0 (só CPU e memória)8Requisições em andamento por task que disparam o terceiro alarme; também o limite de burst do publicador
fast_scaling_cooldown6060Segundos entre dois scale-outs rápidos: tasks novas levam cerca de um minuto para receber tráfego, e um cooldown menor multiplicaria o serviço de novo antes disso
fast_scaling_target_trackingtruetrueO target tracking de CPU e memória (scale-in incluído) lê as métricas do app em vez das do ECS. Nas do ECS, um AlarmLow de memória ainda ligado desde antes de um pico removeu uma task 2 min 20 s depois do início dele

Políticas em métricas da AWS

InputPadrãoWorkloadO que fez
cpu_target_value / mem_target_value70 / 7070 / 70Alvos do target tracking, e os limites dos alarmes rápidos de CPU e memória
request_count_target_value0 (desligado)1000Requisições por minuto por task. 100 levou o serviço a 10 tasks com 10 req/s, que 2 tasks atendiam com menos de 5% de CPU, e o manteve ali por meia hora. 6000 (100 req/s, um terço de uma task) deixou uma carga de 10 req/s em 2 tasks. Dimensione a partir de uma task medida, não de um chute
enable_emergency_step_scalingtruedesligado onde o scaling rápido está ligadoUm passo de CPU em um datapoint do ECS, +50%, +100% acima de 85%: 3 min 45 s a 4 min 41 s para disparar, e dispara de novo depois que a carga acabou. Com o scaling rápido ligado, não acrescenta nada além de scale-outs atrasados
emergency_request_count_threshold0 (desligado)0O mesmo passo nas requisições por task do ALB: disparou 3 min 45 s a 4 min dentro de um pico, não antes do de CPU
scale_out_cooldown_value / scale_in_cooldown_value10 / 6030 / 30Entre duas ações de target tracking; o scale-in continua preso aos seus 15 datapoints
min_capacity_value / max_capacity_valueobrigatório2 / 10Os passos rápidos vão direto ao máximo em um pico saturante: max_capacity é o teto de custo

Em volta do scaling: load balancer e deployments

InputPadrãoWorkloadO que fez
deregistration_delay55Com 0, uma task saindo no meio de um pico (uma interrupção real de Spot, depois um stop-task) cortou as requisições em andamento nela: 21 e 33 502s. Com 5 s, e o app esvaziando sua fila no SIGTERM, 0
load_balancing_algorithm_typeleast_outstanding_requestso mesmoManda mais tráfego para um target que falha rápido (um processo morto responde 502 na hora e não tem requisições pendentes): metade das requisições falhou nos 7 a 10 s antes de o health check removê-lo. Evita bem uma task travada ou lenta (1 requisição falha)
load_balancing_anomaly_mitigation (com weighted_random)falsefalseTestado em dev: reduziu em 87% os erros de uma task respondendo 500 com health check verde, mas exige 3 targets saudáveis, e com 10 tasks uma task saturada manteve sua fatia de 10% e metade das requisições em andamento: 15% e 31% menos throughput a 400 e 600 em andamento. De volta ao least outstanding requests
deployment_bake_time_in_minutes + deployment_module_alarms0 / []5 / ["http_5xx", "target_response_time"]O ECS só verifica os alarmes de deployment durante o bake time, que é 0 em ROLLING por padrão: uma release respondendo 500 terminou seu deployment 2 min 45 s antes de o alarme de 5xx disparar. Com 5 minutos, fez rollback sozinha, 3 min 23 s depois do primeiro erro. Agora o módulo envia o bake time em ROLLING quando há alarmes
http_5xx_threshold / target_response_time_threshold0 (desligado)10 por minuto / 1 sCom 0, failrate=1 respondeu 500 a toda requisição com todos os targets saudáveis e nenhum alarme. Com eles ligados, o alarme de 5xx disparou 2 min 48 s a 3 min 40 s depois dos primeiros erros

6. Scaling rápido ligado e desligado, medido

Os mesmos três testes, executados com enable_fast_scaling ligado e desligado em dev, com a mesma imagem do app e a mesma configuração: só a opção mudou entre as duas rodadas.

Até o primeiro scale-outDo primeiro scale-out a 10 tasks saudáveis
CPU a 100%, scaling rápido ligado
primeiro scale-out em 0:39
1:57
CPU a 100%, scaling rápido desligado
primeiro scale-out em 4:41
10:29
Rampa de 100 a 1600 req/s, scaling rápido ligado
primeiro scale-out em 3:47
5:21
Rampa de 100 a 1600 req/s, scaling rápido desligado
primeiro scale-out em 5:43
6:41
Pico de 600 em andamento, scaling rápido ligado
primeiro scale-out em 0:43
2:18
Pico de 600 em andamento, scaling rápido desligado
primeiro scale-out em 4:41
6:25
Minutos desde o início da carga até 10 tasks saudáveis; o primeiro segmento termina no primeiro scale-out. Na rampa, o primeiro scale-out também depende de quando a carga cruza um limite: a CPU passou de 70% no passo de 600 req/s. Passe o mouse sobre um segmento para ver seu valor.
TesteScaling rápido ligadoScaling rápido desligado
CPU a 100%, sem requisições2 para 10 em um passo aos 39 s (fast-cpu, saturado +400%); 10 saudáveis em 1 min 57 scpu-emergency aos 4 min 41 s (4 tasks), 6 min 41 s (8) e 9 min 41 s (10); 10 saudáveis em 10 min 29 s
Rampa de taxa fixa, 100 a 1600 req/sfast-cpu aos 3 min 47 s (o passo de 600 req/s); p95 125 ms em todos os passos menos o de 900 (199 ms); 8 requisições acima de 1 s em 432.0002 tasks até o target tracking em requisições levar a 10 aos 5 min 43 s: a 1200 req/s p50 507 ms, p95 1.027 ms, e o cliente até 17,8 s atrasado; 5.940 requisições acima de 1 s
Pico de 600 por 6 minutos10 tasks pedidas aos 43 s: 1.239.639 requisições, 3.443 req/s, p50 / p95 124 / 486 ms2 tasks por 4 min 41 s, 10 pedidas aos 5 min 40 s: 336.868 requisições, 936 req/s, p50 / p95 602 / 1.092 ms, 9,4% acima de 1 s
Erros (carga, sonda, 5xx do ALB)00
Task-minutos durante o teste (CPU / rampa / pico)92 / 49 / 5243 / 37 / 15

Nada falhou com o scaling rápido desligado: este app enfileira em vez de recusar, então a falta de tasks aparece como latência e throughput, não como erros, desde que fique abaixo do timeout do cliente. Um app com timeouts mais apertados, ou que recuse conexões, transformaria esses minutos em erros. O preço do scaling rápido é a capacidade usada mais cedo: as mesmas 10 tasks, minutos antes. A mesma opção medida duas vezes, em duas versões do app, variou até 11 s no primeiro scale-out e 33 s para 10 tasks saudáveis: ruído bem abaixo da diferença acima.

7. Dentro da task: o que o app precisou mudar

O pico de 600 contra 2 tasks, o mesmo teste após cada mudança. As duas primeiras linhas são as primeiras rodadas, sem scaling rápido; a partir de T1, o scaling rápido está ligado.

ExecuçãoO que mudouPrimeiro scale-out10 tasks pedidasErrosreq/sp50 / p95 ms
Rodada 1HTTPServer da stdlib: listen backlog 5, HTTP/1.03 min 30 s4 min 35 s1.367 (504 aos 10,1 s)1.316305 / 666
Rodada 2backlog 1024, keep-alive, uma thread por conexão3 min 17 s7 min 22 s0637148 / 2.403
T1scaling rápido, 2 de 3 datapoints, passo +50% / +100%; 8 threads por task1 min 03 s4 min 46 s196 (timeouts, 502)1.658300 / 863
T1b+ passo +100%, mediana, target tracking nas métricas do app1 min 04 s4 min 45 s3081.406302 / 922
T1c+ 2 requisições por vez, uma fila justa, passo +200%1 min 52 s5 min 05 s01.126415 / 1.208
T1d+ uma thread worker, requisições lentas em threads próprias, passo saturado +400%54 s1 min 06 s21 (502, interrupção de Spot)1.927220 / 1.111
T1e+ JSON compacto, drain no SIGTERM57 s2 min 50 s33 (502, uma task parada)2.054204 / 1.113
T1f+ deregistration_delay 5 em dev52 s1 min 04 s02.050209 / 1.105
Rodada 5+ keep-alive no worker, uma escrita por resposta, publicação em burst, 1 de 152 s57 s03.621123 / 495
Rodada 7o mesmo código em uma stack nova49 s49 s03.733122 / 494
Liga/desliga, ligadoJSON indentado de novo (o Python 3.14 o codifica em C), alvo de requisições 100043 s43 s03.443124 / 486
Liga/desliga, desligadoo mesmo código com enable_fast_scaling desligado4 min 41 s5 min 40 s0936602 / 1.092

O que cada passo ensinou, para qualquer app atrás de um ALB:

  • Verifique o listen backlog e o keep-alive antes de um teste de carga. O HTTPServer do Python escuta com backlog de 5 e responde HTTP/1.0, então o ALB abre uma conexão por requisição; a cerca de 470 conexões novas por segundo por task a fila estourou, o kernel descartou SYNs e o ALB respondeu 504 após seu timeout de conexão de 10 s. Nenhum target chegou a ficar não saudável.
  • Concorrência ilimitada é pior que uma fila. Com backlog de 1024, o ALB colocou todas as 600 requisições dentro das 2 tasks: cerca de 300 threads Python por task em 0,25 vCPU disputaram o interpretador, cada requisição custou o dobro de CPU, e o health check de 2 s deu timeout no meio do pico. O antigo backlog de 5 tinha sido um controle de admissão por acidente.
  • Mantenha a thread que aceita alimentada. Com 8 threads worker, a que aceitava conexões esperava atrás delas e as conexões novas ficavam até 10 s sem ser aceitas. Uma thread worker, com as requisições que dormem repassadas a threads próprias, resolveu.
  • Desligue o Nagle com keep-alive. Cabeçalhos e corpo em duas escritas esperavam o ACK atrasado do ALB: 40 ms em toda resposta. TCP_NODELAY, depois uma escrita por resposta.
  • Faça drain no SIGTERM, e deixe o ALB fazer drain. Deploys, scale-in e interrupções de Spot param tasks com SIGTERM; o app continua aceitando até a fila do kernel esvaziar, e deregistration_delay dá 5 s às requisições em andamento.
  • Meça a CPU por requisição onde ela roda. Um profile local no Python 3.9 culpava o JSON indentado por 30% de uma requisição; no Python 3.14 e posteriores, o da imagem, indent= também usa o encoder em C e custa cerca de 0,5 µs. Uma imagem base sem versão fixada muda a CPU por requisição sem nenhuma mudança de código.
  • Cuidado com os limites de file descriptors de APIs antigas. select.select() falha acima de 1.024 descriptors: 3000 requisições lentas fizeram toda requisição falhar, health checks inclusive. poll() não tem esse limite.

Capacidade com o serviço fixo em 10 tasks, req/s a 300, 400 e 600 em andamento: 2.390, 3.083 e 3.537 na primeira rodada; 2.404, 3.205 e 4.679 com o código final. Abaixo de 300 em andamento, o throughput é a concorrência dividida pela latência de rede, igual em todas as versões.

8. Armadilhas

  • Um alarme de step ainda em ALARM dispara de novo. Definir o desired count à mão depois de um teste foi desfeito por um alarme que não tinha voltado para OK. Espere os alarmes antes.
  • Datapoints atrasados escalam o próximo teste. Com o scaling rápido desligado, espere cerca de 4 minutos depois de uma carga antes de medir qualquer outra coisa, ou a carga anterior dispara o scaling.
  • Alarmes de deployment também veem o passado. Um deploy iniciado logo depois de um flood de requisições de 40 s sofreu rollback: o alarme de tempo de resposta pegou o flood dois minutos atrasado, dentro do bake time. Faça o deploy quando os alarmes estiverem OK.
  • HealthyHostCount perde quedas curtas. O ALB o publica com 2 amostras por minuto: 40 s sem nenhum target saudável nunca apareceu na métrica, um minuto inteiro apareceu.
  • Os logs atrasam quando a CPU está saturada. O Fluent Bit divide os 0,25 vCPU da task: só 13% das linhas chegaram ao CloudWatch no próprio minuto, o resto até 5 minutos depois. Nenhuma se perdeu; filtre pelo @timestamp dentro do JSON, não pelo do CloudWatch.
  • Uma task nova leva cerca de um minuto. Cerca de 19 s da criação até o download da imagem, 30 a 60 s de downloads (a imagem do Fluent Bit tem 113 MB compactada), depois dois health checks: imagens menores são o próximo ganho.
  • Interrupções de Spot são baratas quando a task faz drain. Cinco reais durante os testes. A que ocorreu no meio de um pico, antes do drain de 5 s, cortou 21 requisições em andamento; as outras quatro não custaram nenhuma: a substituta começou um segundo depois do aviso e ficou saudável em 40 a 50 s.

9. A configuração que resultou disso

Os inputs do módulo como o arglabs-workload os define em dev, por meio de seus locals:

module "ecs_app" {
  source = "git::https://bitbucket.org/arglabs/arglabs-tfmod-ecs-app.git//terraform?ref=main"
  # ...
  min_capacity_value         = 2
  max_capacity_value         = 10
  cpu_target_value           = 70
  mem_target_value           = 70
  request_count_target_value = 1000 # requests a minute per task: size it from a measured task

  # Fast scaling: the app publishes CpuUtilization, MemoryUtilization and RequestsInFlight
  fast_scaling_metrics_namespace      = "aio/workload"
  fast_scaling_metrics_interval       = 10
  fast_scaling_evaluation_periods     = 1
  fast_scaling_datapoints_to_alarm    = 1
  fast_scaling_statistic              = "p50"
  fast_scaling_inflight_threshold     = 8
  fast_scaling_step_percent           = 200
  fast_scaling_saturated_step_percent = 400
  fast_scaling_cooldown               = 60
  fast_scaling_target_tracking        = true

  # The steps on late metrics only where fast scaling is off
  enable_emergency_step_scaling     = false
  emergency_request_count_threshold = 0

  # Draining, and deployments that roll back on errors
  deregistration_delay            = 5
  deployment_bake_time_in_minutes = 5
  deployment_module_alarms        = ["http_5xx", "target_response_time"]
  http_5xx_threshold              = 10
  target_response_time_threshold  = 1
}

Para um app que não consegue publicar as métricas: deixe fast_scaling_metrics_namespace vazio, mantenha enable_emergency_step_scaling ligado e defina request_count_target_value a partir de uma task medida; espere de 4 a 6 minutos até o primeiro scale-out.

10. Método e limites

  • Um cliente, no Brasil, a cerca de 120 ms do serviço: o throughput medido no cliente inclui essa latência; o TargetResponseTime do ALB deu o lado do servidor.
  • Fargate Spot em Graviton2, dias e horas diferentes: os números da rampa da primeira rodada vieram em parte do hardware daquele dia. Sua imagem, executada de novo nas tasks posteriores, fez 2.308, 2.540 e 2.584 req/s a 300, 400 e 600 em andamento, contra 2.390, 3.083 e 3.537 naquele dia.
  • Uma execução por configuração na maioria dos testes; a variação entre execuções do mesmo código está no texto onde foi medida.
  • O scale-in não foi medido depois da primeira rodada: entre os testes o desired count voltava a 2 à mão, quando os alarmes de scaling estavam OK.
  • Só dev: stg e prd rodam o mesmo código com outros valores (o prd mantém 2 tasks on-demand antes do Spot).
Última atualização em