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.
2. O ambiente de teste
| Item | Valor |
|---|---|
| Serviço | arglabs-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 |
| Task | o app de exemplo (Python, biblioteca padrão) mais dois sidecars: Fluent Bit (logs) e ADOT (traces); o container do app recebe 334 MB |
| Load balancer | ALB público, least outstanding requests, health check /healthcheck a cada 5 s com timeout de 2 s |
| Cliente | um 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ção | um 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:
| Teste | O que faz |
|---|---|
| CPU | cpuburn em cada task, as novas inclusive, por 8 a 10 minutos, sem carga de requisições |
| Memória | memburn=230 (MB) em cada task por 9 minutos |
| Pico | 600 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 tasks | 20 a 600 em andamento com o serviço fixo em 10 tasks: a capacidade |
| Rampa de taxa fixa | 100, 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) |
| Flood | 3000 requisições que dormem 40 s, todas de uma vez |
| Falhas | 13 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 AWS | O que precisa | Medido desde o início da carga |
|---|---|---|
| Target tracking em requisições | 3 datapoints acima do alvo | 4 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ória | 3 datapoints | 5 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 datapoint | 3 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 datapoint | 3 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 alvo | 10 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étrica | Unidade | O que é |
|---|---|---|
CpuUtilization | Percent | CPU 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 |
MemoryUtilization | Percent | memória da task inteira sem o cache de arquivos recuperável (inactive_file), como porcentagem da memória da task |
RequestsInFlight | Count | requisiçõ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>) eMETRICS_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). StorageResolution1, 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 defast_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
| Input | Padrão | Workload | O 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_interval | 10 | 10 | Segundos 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_alarm | 3 / 2 | 1 / 1 | Com 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_statistic | p50 | p50 | Uma 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_percent | 100 | 200 | Tasks adicionadas por disparo, como porcentagem das que estão rodando: 200 triplica o serviço (2, 6, 10) |
fast_scaling_saturated_step_percent | 0 (desligado) | 400 | Adicionado 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_threshold | 0 (só CPU e memória) | 8 | Requisições em andamento por task que disparam o terceiro alarme; também o limite de burst do publicador |
fast_scaling_cooldown | 60 | 60 | Segundos 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_tracking | true | true | O 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
| Input | Padrão | Workload | O que fez |
|---|---|---|---|
cpu_target_value / mem_target_value | 70 / 70 | 70 / 70 | Alvos do target tracking, e os limites dos alarmes rápidos de CPU e memória |
request_count_target_value | 0 (desligado) | 1000 | Requisiçõ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_scaling | true | desligado onde o scaling rápido está ligado | Um 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_threshold | 0 (desligado) | 0 | O 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_value | 10 / 60 | 30 / 30 | Entre duas ações de target tracking; o scale-in continua preso aos seus 15 datapoints |
min_capacity_value / max_capacity_value | obrigatório | 2 / 10 | Os 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
| Input | Padrão | Workload | O que fez |
|---|---|---|---|
deregistration_delay | 5 | 5 | Com 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_type | least_outstanding_requests | o mesmo | Manda 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) | false | false | Testado 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_alarms | 0 / [] | 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_threshold | 0 (desligado) | 10 por minuto / 1 s | Com 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.
| Teste | Scaling rápido ligado | Scaling rápido desligado |
|---|---|---|
| CPU a 100%, sem requisições | 2 para 10 em um passo aos 39 s (fast-cpu, saturado +400%); 10 saudáveis em 1 min 57 s | cpu-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/s | fast-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.000 | 2 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 minutos | 10 tasks pedidas aos 43 s: 1.239.639 requisições, 3.443 req/s, p50 / p95 124 / 486 ms | 2 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) | 0 | 0 |
| Task-minutos durante o teste (CPU / rampa / pico) | 92 / 49 / 52 | 43 / 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ção | O que mudou | Primeiro scale-out | 10 tasks pedidas | Erros | req/s | p50 / p95 ms |
|---|---|---|---|---|---|---|
| Rodada 1 | HTTPServer da stdlib: listen backlog 5, HTTP/1.0 | 3 min 30 s | 4 min 35 s | 1.367 (504 aos 10,1 s) | 1.316 | 305 / 666 |
| Rodada 2 | backlog 1024, keep-alive, uma thread por conexão | 3 min 17 s | 7 min 22 s | 0 | 637 | 148 / 2.403 |
| T1 | scaling rápido, 2 de 3 datapoints, passo +50% / +100%; 8 threads por task | 1 min 03 s | 4 min 46 s | 196 (timeouts, 502) | 1.658 | 300 / 863 |
| T1b | + passo +100%, mediana, target tracking nas métricas do app | 1 min 04 s | 4 min 45 s | 308 | 1.406 | 302 / 922 |
| T1c | + 2 requisições por vez, uma fila justa, passo +200% | 1 min 52 s | 5 min 05 s | 0 | 1.126 | 415 / 1.208 |
| T1d | + uma thread worker, requisições lentas em threads próprias, passo saturado +400% | 54 s | 1 min 06 s | 21 (502, interrupção de Spot) | 1.927 | 220 / 1.111 |
| T1e | + JSON compacto, drain no SIGTERM | 57 s | 2 min 50 s | 33 (502, uma task parada) | 2.054 | 204 / 1.113 |
| T1f | + deregistration_delay 5 em dev | 52 s | 1 min 04 s | 0 | 2.050 | 209 / 1.105 |
| Rodada 5 | + keep-alive no worker, uma escrita por resposta, publicação em burst, 1 de 1 | 52 s | 57 s | 0 | 3.621 | 123 / 495 |
| Rodada 7 | o mesmo código em uma stack nova | 49 s | 49 s | 0 | 3.733 | 122 / 494 |
| Liga/desliga, ligado | JSON indentado de novo (o Python 3.14 o codifica em C), alvo de requisições 1000 | 43 s | 43 s | 0 | 3.443 | 124 / 486 |
| Liga/desliga, desligado | o mesmo código com enable_fast_scaling desligado | 4 min 41 s | 5 min 40 s | 0 | 936 | 602 / 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
HTTPServerdo 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_delaydá 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.
HealthyHostCountperde 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
@timestampdentro 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
TargetResponseTimedo 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).