Pular para o conteúdo

Migrar cargas para instâncias spot

Avançado16 min de leituracloud

Instâncias spot custam uma fração do preço sob demanda porque o provedor pode retomá-las com aviso curto (na ordem de dois minutos na AWS, trinta segundos no GCP). A troca é explícita: você paga menos e assume a interrupção. Este guia mostra como assumir isso sem transformar economia em incidente.

Boas candidatas — toleram morrer e voltar:

  • Workers de fila com mensagens idempotentes.
  • Runners de CI e jobs de build.
  • Processamento em lote com ponto de retomada.
  • Ambientes de desenvolvimento e homologação.
  • Parte da capacidade de serviços web sem estado, acima de uma base garantida.

Nunca em spot: banco de dados, control plane do cluster, serviço com estado em memória sem replicação, job longo sem retomada, e a última réplica de qualquer coisa.

O teste rápido: se esta instância sumir em dois minutos, o que o usuário percebe? Se a resposta for “nada” ou “alguns segundos a mais”, pode ir.

Interrupção costuma atingir um tipo de instância específico numa zona específica. Pedir “10 instâncias m5.large na zona A” é o jeito mais fácil de ser interrompido em bloco.

# Karpenter: deixe o provisionador escolher entre muitas famílias
apiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: spot-diversificado }
spec:
template:
spec:
requirements:
- { key: karpenter.sh/capacity-type, operator: In, values: [spot] }
- { key: kubernetes.io/arch, operator: In, values: [amd64, arm64] }
- { key: karpenter.k8s.aws/instance-family, operator: In, values: [m6i, m6a, m7i, c6i, c6a] }
- { key: topology.kubernetes.io/zone, operator: In, values: [sa-east-1a, sa-east-1b, sa-east-1c] }
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
limits: { cpu: 200 } # teto explícito: seu limite de fatura

Quanto mais famílias e zonas aceitáveis, menor a chance de interrupção simultânea.

A base do serviço em capacidade garantida; o excedente em spot.

# ASG / Node group com política mista
mixedInstancesPolicy:
instancesDistribution:
onDemandBaseCapacity: 2 # sempre 2 sob demanda
onDemandPercentageAboveBaseCapacity: 20 # acima disso, 80% spot
spotAllocationStrategy: price-capacity-optimized

price-capacity-optimized escolhe pools com capacidade disponível, e não apenas os mais baratos — o que reduz bastante a taxa de interrupção na prática.

Distribua as réplicas para não concentrar tudo no mesmo tipo de capacidade:

topologySpreadConstraints:
- maxSkew: 1
topologyKey: karpenter.sh/capacity-type
whenUnsatisfiable: ScheduleAnyway
labelSelector: { matchLabels: { app: loja } }

O aviso só ajuda se alguém agir. Em Kubernetes, use um handler que drena o nó ao receber a notificação (o Karpenter e o NTH fazem isso), e garanta que a aplicação encerre com graça:

spec:
terminationGracePeriodSeconds: 60 # menor que a janela de aviso
containers:
- name: worker
lifecycle:
preStop:
exec: { command: ["sh","-c","sleep 10"] } # sai do endpoint antes de encerrar
# impeça que a drenagem derrube o serviço inteiro
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: loja, namespace: prod }
spec:
minAvailable: 2
selector: { matchLabels: { app: loja } }

Fora do Kubernetes, o padrão é o mesmo: escute o endpoint de metadados que anuncia a interrupção, pare de aceitar trabalho novo, finalize o que está em andamento, e devolva a mensagem para a fila se não der tempo.

Spot expõe o que já era frágil. Duas exigências:

  • Idempotência: reprocessar a mesma mensagem não pode duplicar efeito.
  • Ponto de retomada: job longo salva progresso; ao voltar, continua de onde parou.

Em fila, garanta que o visibility timeout seja maior que o tempo de processamento — assim a mensagem volta para a fila se o worker morrer, sem ser processada duas vezes ao mesmo tempo.

Janela do terminal
# proporção de capacidade em spot, no cluster
kubectl get nodes -L karpenter.sh/capacity-type
# custo por tipo de capacidade
aws ce get-cost-and-usage --time-period Start=2026-08-01,End=2026-09-01 \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=DIMENSION,Key=PURCHASE_TYPE

Acompanhe também a frequência de interrupção e o efeito nas métricas de sintoma. Se a latência p95 piora a cada interrupção, o problema não é o spot: é a drenagem ou o tempo de partida da aplicação.

Sintoma Causa provável
Interrupções frequentes Pouca diversificação de tipo e zona
Pods Pending após interrupção Sem capacidade sob demanda de reserva, ou limites baixos
Erros 5xx a cada interrupção Sem PDB, sem preStop, ou grace period curto
Mensagens processadas duas vezes Falta de idempotência; visibility timeout curto
Economia menor que a esperada Base sob demanda alta demais, ou pouca carga em spot
Job longo nunca termina Sem ponto de retomada — não é carga para spot
  • Cargas classificadas por tolerância a interrupção.
  • Diversificação de famílias, arquiteturas e zonas.
  • Base sob demanda garantida para o tráfego crítico.
  • Handler de interrupção drenando o nó.
  • PodDisruptionBudget, preStop e grace period configurados.
  • Trabalho idempotente e retomável.
  • Economia e taxa de interrupção medidas, com métricas de sintoma estáveis.