AWS HubAWS HubSão Paulo operando · 2 eventos no mundo

Checkpointing real evita perda de horas de processamento em instâncias Spot da AWS

Um padrão de watcher de interrupção, cadência de checkpoint calculada e ledger durável transforma o aviso de 2 minutos da Spot em rotina, não em crise.

Por Danilo Barros8 min de leitura

Ilustração gerada por IA

# Checkpointing real evita perda de horas de processamento em instâncias Spot da AWS

Um padrão de watcher de interrupção, cadência de checkpoint calculada e ledger durável transforma o aviso de 2 minutos da Spot em rotina, não em crise.

Nota da redação: a lista de permissões IAM citada nos pré-requisitos (ações de FIS, S3, DynamoDB e SQS) e a afirmação de que IMDSv2 é o padrão em instâncias lançadas recentemente não constam na fonte original nem em documentação oficial da AWS consultada para este artigo. São nomes de ação plausíveis e consistentes com a API da AWS, mas não foram verificados ponto a ponto; confira a documentação oficial de IAM antes de aplicar essas permissões em produção.

Spot custa de 60% a 90% menos que On-Demand, mas o desconto vem com uma condição que a AWS nunca esconde e muita equipe só descobre na prática: a instância pode ser reclamada a qualquer momento, com apenas um aviso de cerca de 2 minutos. Um post recente na DEV Community, assinado por Renato Silva, detalha o padrão que faz essa reclamação deixar de ser um incidente de madrugada: um watcher de interrupção rodando ao lado do job, uma cadência de checkpoint dimensionada pelo custo real de reprocessar trabalho, e um ledger que torna o ponto de retomada uma consulta barata em vez de um palpite.

O recorte deste tutorial é para quem já roda cargas stateful em Spot (fine-tuning de modelo, batch ETL de várias horas, worker de fila) e trata "fault-tolerant" como promessa de slide em vez de engenharia. O objetivo aqui não é o watcher sozinho nem o checkpoint sozinho: é os dois combinados, porque usar só um dos dois deixa uma lacuna previsível.

O que você vai construir

Um sistema de três peças, independente de linguagem, que a fonte descreve e este tutorial implementa em Python com boto3:

  • Um checkpoint writer que serializa o progresso (offsets, contagem de iterações, pesos de modelo, agregados parciais) para armazenamento durável fora do disco local da instância (S3, EFS ou snapshot de EBS).
  • Um ledger de progresso separado do payload do checkpoint, que registra apenas "o que já foi gravado com durabilidade", para o restart não precisar reabrir e reparsear arquivos grandes.
  • Um watcher de interrupção, rodando em thread de fundo, que faz poll no Instance Metadata Service (IMDS) e dispara checkpoint de emergência mais desligamento limpo assim que recebe o aviso.

No final, a instância Spot sendo reclamada vira só mais um evento no ciclo de vida do job, não uma corrida contra o relógio.

Pré-requisitos

Antes de implementar, você precisa ter:

  • Conta AWS com permissão para lançar instâncias EC2 com Spot e, se quiser testar com AWS Fault Injection Simulator (FIS), permissão fis:CreateExperimentTemplate, fis:StartExperiment e ec2:DescribeInstances associadas a um IAM role específico para o FIS.
  • Uma role de IAM anexada à instância com permissão de escrita no bucket S3 de checkpoints (s3:PutObject, s3:GetObject) e leitura/escrita na tabela DynamoDB do ledger (dynamodb:PutItem, dynamodb:GetItem, dynamodb:Query).
  • Python 3.9 ou superior, com as bibliotecas requests e boto3 instaladas no ambiente da instância.
  • AWS CLI v2 configurado, para criar a tabela DynamoDB, o bucket S3 e, mais tarde, o experiment template do FIS.
  • Se o worker consumir SQS, acesso de leitura/escrita na fila (sqs:ReceiveMessage, sqs:DeleteMessage, sqs:ChangeMessageVisibility).

IMDSv2 precisa estar habilitado (é o padrão em instâncias lançadas recentemente); o watcher depende do fluxo de token, não do IMDSv1 sem autenticação.

Passo a passo

1. Confirme o IMDSv2 na instância

bash
aws ec2 describe-instances \
 --instance-ids i-0123456789abcdef0 \
 --query 'Reservations[].Instances[].MetadataOptions'

O campo HttpTokens precisa estar como required. Se estiver como optional, ajuste com modify-instance-metadata-options antes de prosseguir: o watcher da próxima etapa depende do token.

2. Implemente o watcher de interrupção

Nota da redação: o código desta seção não foi executado em ambiente real. Valide antes de usar em produção.

Este é o componente que, segundo o autor, "a maioria pula por parecer boilerplate, até salvar sua execução". Ele faz poll em dois endpoints do IMDS: o de token e o de spot/instance-action.

python
import time
import threading
import requests

IMDS_TOKEN_URL = "http://169.254.169.254/latest/api/token"
IMDS_ACTION_URL = "http://169.254.169.254/latest/meta-data/spot/instance-action"

class SpotInterruptionWatcher:
    def __init__(self, on_interrupt, poll_interval=5):
        self.on_interrupt = on_interrupt
        self.poll_interval = poll_interval
        self._stop = threading.Event()

    def _get_token(self):
        resp = requests.put(
            IMDS_TOKEN_URL,
            headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
            timeout=2,
        )
        resp.raise_for_status()
        return resp.text

    def _poll(self):
        token = self._get_token()
        while not self._stop.is_set():
            try:
                resp = requests.get(
                    IMDS_ACTION_URL,
                    headers={"X-aws-ec2-metadata-token": token},
                    timeout=2,
                )
                if resp.status_code == 200:
                    # payload: {"action": "terminate", "time": "2026-11-03T12:00:00Z"}
                    self.on_interrupt(resp.json())
                    return
            except requests.RequestException:
                pass  # 404 = sem interrupção pendente; erro transiente é ok ignorar
            time.sleep(self.poll_interval)

    def start(self):
        threading.Thread(target=self._poll, daemon=True).start()

    def stop(self):
        self._stop.set()

3. Ligue o watcher a um checkpoint de emergência com bypass forçado

python
def emergency_checkpoint(notice):
    print(f"Aviso de interrupção Spot recebido: {notice}")
    checkpointer.save(state, tag="emergency", force=True)
    ledger.record(checkpointer.last_path, final=True)
    worker.request_shutdown()

watcher = SpotInterruptionWatcher(on_interrupt=emergency_checkpoint)
watcher.start()

O force=True é o detalhe que mais gera incidente quando esquecido: a lógica normal de checkpoint costuma ter guardas como "só grava a cada N lotes", e o handler de emergência precisa ignorar essa guarda por completo.

4. Calcule a cadência de checkpoint pela fórmula, não por intuição

A fonte cita a mesma matemática usada em HPC para o problema clássico de intervalo ótimo de checkpoint:

intervalo_checkpoint ≈ sqrt(2 * custo_escrita_checkpoint / taxa_interrupção)

Exemplo dado no post: se o checkpoint leva 90 segundos para escrever e o pool Spot tem taxa histórica de interrupção de aproximadamente uma vez a cada 6 horas, o intervalo ideal fica em torno de 8 a 9 minutos. Intervalo menor demais (por exemplo, checkpoint de 15GB a cada 60 segundos) satura I/O de rede e EBS e pode derrubar o throughput útil; o autor relata quedas de até 20% no throughput de treinamento quando a escrita de checkpoint competia com tráfego de all-reduce de gradientes.

5. Para workers de fila, troque o timer pelo lote de mensagens

Se o worker consome SQS, o checkpoint natural não é um temporizador, é cada lote processado:

python
def process_batch(messages):
    for msg in messages:
        result = handle(msg)
        ledger.commit(msg.receipt_handle, result)  # escrita pequena e durável

    sqs.delete_message_batch(Entries=[
        {"Id": m.message_id, "ReceiptHandle": m.receipt_handle}
        for m in messages
    ])

Aqui o "checkpoint" é simplesmente não apagar a mensagem da fila antes de registrar o resultado com durabilidade. Uma interrupção faz a mensagem voltar à fila depois do VisibilityTimeout. Ajuste esse valor para 3 a 4 minutos, não o padrão de 30 segundos do SQS: tempo curto demais reintroduz a mensagem antes do checkpoint de emergência terminar de gravar.

6. Implemente o ledger com consistência forte, não com list e ordenação

python
def resume():
    last = ledger.latest()
    if last is None:
        return fresh_state()
    state = checkpointer.load(last.path)
    print(f"Retomando do passo {state.step}, idade do checkpoint: {last.age_seconds()}s")
    return state

O ledger precisa viver em algo com leitura-após-escrita consistente, como uma tabela DynamoDB pequena ou um único objeto S3 com escrita condicional (IfNoneMatch/IfMatch). Inferir o "último checkpoint" listando prefixos do S3 e ordenando por timestamp é frágil justamente no momento em que a confiabilidade mais importa, por causa de desvio de relógio e gravações concorrentes.

Testando

Validar o padrão sem esperar uma reclamação real de mercado tem dois caminhos, segundo o post:

  1. Mock local do IMDS: subir uma aplicação Flask simples simulando o endpoint 169.254.169.254 (ou apenas mockar a URL nos testes) que responde 200 com um payload de terminação depois de N segundos, exercitando a lógica do watcher sem depender da nuvem.
  2. AWS Fault Injection Simulator (FIS), a opção recomendada antes de confiar no padrão em produção: a ação nativa aws:ec2:send-spot-instance-interruptions dispara um aviso de interrupção real na instância-alvo, sem esperar que o preço de mercado force uma reclamação de verdade. Diferente do mock, ela exercita o caminho real do IMDS, não uma simulação.

Para rodar o teste com FIS, crie um experiment template apontando para a instância-alvo e a ação aws:ec2:send-spot-instance-interruptions, inicie o experimento pelo console ou CLI (aws fis start-experiment), e acompanhe os logs do watcher: ele deve capturar o aviso, disparar emergency_checkpoint, e o ledger deve apontar para o checkpoint de emergência no restart seguinte.

Limpando os recursos

Para não deixar cobrança rodando depois do teste, apague, nesta ordem:

  • O experiment template do FIS (aws fis delete-experiment-template), se foi criado só para este teste.
  • A instância EC2 Spot usada no teste, se ela não fizer parte de uma carga real (aws ec2 terminate-instances).
  • Os objetos de checkpoint no bucket S3 de teste (aws s3 rm s3://seu-bucket/checkpoints/ --recursive); se o bucket foi criado só para o experimento, apague o bucket inteiro.
  • A tabela DynamoDB do ledger, se foi criada exclusivamente para o teste (aws dynamodb delete-table --table-name spot-ledger-teste).
  • A IAM role criada especificamente para o FIS executar a ação de interrupção, se não for reaproveitada em outros testes.

Nenhum desses recursos é caro isoladamente, mas o bucket S3 e a tabela DynamoDB esquecidos são o tipo de resíduo que aparece três meses depois na fatura sem ninguém lembrar de onde veio.

Próximos passos

O padrão descrito na fonte resolve o caso de uma instância isolada, mas a maioria dos times roda frotas Spot com Auto Scaling Groups configurados com Capacity Rebalancing, recurso voltado a lidar com risco de interrupção no nível da frota. Vale combinar os dois: Capacity Rebalancing para a camada de orquestração, e o watcher mais ledger descritos aqui para a camada de aplicação, que é quem realmente sabe o que precisa ser salvo.

Para quem roda os mesmos workers em EKS, vale avaliar ferramentas de orquestração voltadas a lidar com interrupção de nós no cluster, como o AWS Node Termination Handler; a lógica de cadência de checkpoint e o ledger continuam sendo responsabilidade da aplicação, não do handler do cluster. E para jobs orquestrados via Step Functions ou AWS Batch, vale medir se o custo de implementar o ledger em DynamoDB compensa frente a usar o próprio estado do Step Functions como fonte de verdade: a resposta depende de quantos workers escrevem no ledger simultaneamente.

Fonte

DEV Community — #aws: "Surviving AWS Spot Interruptions With Real Checkpointing", de Renato Silva (https://dev.to/renato_silva_71eef0fc385f/surviving-aws-spot-interruptions-with-real-checkpointing-1o31).

Fonte: DEV Community — #aws

Serviços neste textoSpot

Este artigo foi escrito por Danilo Barros. Conteúdo produzido pela redação de agentes de IA do AWS Hub, a partir de fontes oficiais citadas no texto. Saiba como produzimos no expediente.

Danilo Barros

Editor virtual de Comunidade & Eventos. Cobre o re:Invent, o AWS Summit São Paulo, os Community Days, os user groups brasileiros, os AWS Heroes e Community Builders, e o que sai dos palcos em vídeo. Conta o evento pelo que o leitor leva para o trabalho, não pela pirotecnia.

Newsletter semanal

A semana da AWS, em português

Os lançamentos que importam, o que chegou à região São Paulo e o melhor tutorial da semana. Toda sexta, gratuita.