Migrando 50+ tenants em produção entre data centers, com zero downtime
TL;DR
- Contexto: SaaS multi-tenant automotivo, 60+ sites em produção, operado por um time de plataforma de uma pessoa.
- Missão: tirar toda a produção de um único data center no exterior e levá-la para dois data centers geo-separados no Brasil (ativo + hot-standby), com a frota prevendo dobrar, e sem janela de manutenção.
- Resultado: 50+ tenants migrados com zero downtime medido: 100% das requisições amostradas atravessaram a virada com resposta 200, e cada etapa foi validada por captura de pacotes.
O problema
Sessenta e tantas revendas dependem desses sites para vender: catálogo, leads, e-commerce. Downtime não é um número num dashboard — é um cliente que abriu o site da loja, não carregou, e foi comprar na concorrência.
A produção inteira rodava num único data center fora do país — usuários no Brasil, servidores num continente diferente, e uma frota com previsão de dobrar de tamanho em pouco tempo. A migração não era só uma mudança de endereço: era a chance de sair de um ponto único de falha para dois data centers geo-separados com failover automático — latência menor, DR de verdade, espaço pra crescer.
A resposta óbvia — "migra de madrugada no domingo" — não servia por dois motivos. Primeiro, gente compra carro de madrugada também. Segundo, um time de plataforma de uma pessoa não pode operar à base de heroísmo: se o plano exige virar a noite torcendo, o plano está errado. Eu precisava de uma migração zero-downtime em etapas pequenas e reversíveis, com rollback barato em cada uma.
A estratégia: o destino fica quente antes de qualquer virada
O princípio de tudo: nenhum tráfego muda de lugar até o destino estar rodando produção-espelho, com dados sincronizados e validados.
- Pre-seed: o destino roda semanas como réplica. O stack completo subiu nos dois DCs novos e passou quase duas semanas "dark": PostgreSQL como standby do cluster Patroni (replicação streaming contínua), membros do MongoDB replica set, réplica Redis atrás do Sentinel. Os dados fluíam em tempo real para o destino — sem um único byte gravado lá.
- Aplicação idêntica por definição. Os serviços foram provisionados no destino pelo mesmo control-plane GitOps — mesmos containers, mesma config versionada. Não existe "esqueci de copiar aquele arquivo" quando o deploy é declarativo.
- Virada em ondas, do descartável ao crítico. Primeiro os serviços internos de plataforma (analytics, dashboards). Depois um canário: um único tenant de baixo risco, flipado sozinho e validado de ponta a ponta. Só então a receita.
- O cutover em si: atômico por desenho. Todos os hostnames de tenant apontam por CNAME para um registro-âncora — virar um registro move 100+ hostnames de uma vez, e o rollback é o mesmo registro de volta, na hora. A promoção do standby do Patroni foi coordenada com a virada, com uma regra dura: lag de replicação zero, ou aborta — e o próprio Patroni recusa a promoção se o lag não for zero.
- Ramp gradual? Avaliado e rejeitado. Servir metade do tráfego de cada DC durante a transição parece mais seguro — mas reabre exatamente o risco que a arquitetura fechou: dois lados achando que podem gravar. Virada atômica com rollback instantâneo > gradualismo com risco de split-brain. Trade-off consciente.
- A herança: o tráfego passou a seguir o banco. Depois da poeira baixar, os tenants foram movidos para trás de um Cloudflare Load Balancer cujo health check só responde 200 no data center onde o PostgreSQL é o líder gravável. Como o Patroni garante um líder único, o roteamento de tráfego segue o failover do banco automaticamente — sem intervenção humana, detecção em ~1–2 min, RPO de segundos.
"Zero downtime" se prova, não se declara
Todo mundo diz que migrou "sem downtime". Quase ninguém instrumenta a afirmação. Aqui a validação foi adversarial — o objetivo era provar que quebrou, e falhar:
- Amostragem HTTP contínua atravessando a virada: 400+ requisições, zero respostas diferentes de 200.
- Captura de pacotes em cada flip (tcpdump na borda): SYNs chegando no destino certo, nenhuma conexão derrubada, nenhum RST órfão.
- Health checks externos de ~280 PoPs da Cloudflare — a visão de quem está fora, do mundo inteiro, não a do servidor sobre si mesmo.
- Dashboards por tenant (taxa de erro, latência p95) antes, durante e depois.
- E o seguro final: o DC de origem ficou vivo, read-only, por semanas — rollback disponível durante todo o soak. Nunca foi acionado. Plano de rollback bom é o que você escreve com cuidado e nunca usa.
Se você não mediu, você não migrou com zero downtime — você migrou com sorte.
O incidente que o drill revelou
Migração no ar não é fim de projeto. Dias depois do cutover, rodei um drill de failover: mandar 100% do tráfego de produção para o standby — de propósito, num dia bom, para provar que o failover automático aguentava um dia ruim.
Não aguentou. O standby estava frio — caches vazios — e derreteu: um único container de site a 138% de CPU, load 168 num host de 24 cores, enquanto o primário, quente, servia o mesmo tráfego a load abaixo de 10. Failback pelo runbook em ~9 minutos, sem perda além do pico. E então, root cause analysis.
A causa-raiz não estava na infraestrutura — estava no comportamento do SSR do Next.js com cache frio, um motor de runaway auto-perpetuado: a renderização fria estoura um timeout interno e vira erro; o framework só cacheia respostas OK, então o cache nunca esquenta; todo request seguinte re-renderiza do zero — e o CPU alimenta o próprio incêndio. Ambiente frio não era um estado transitório que "passa": era uma armadilha estável. É o tipo de causa-raiz que só aparece quando você entende o runtime que opera, não só as VMs embaixo dele.
O fix veio em duas trilhas, documentado em postmortem:
- Infra: um pré-aquecedor por tenant que aquece o destino antes de qualquer virada de tráfego — warm-up virou pré-condição de failover no runbook, não detalhe. Mais índices que faltavam e paralelização de queries no caminho crítico.
- Aplicação: degrade gracioso em vez de erro, cap de concorrência + circuit breaker, single-flight de renders, cache no backend — validado depois sob carga com cache frio: 100% de respostas 200.
Lições
- Rollback planejado vale mais que coragem. Cada etapa tinha caminho de volta barato; por isso nenhuma precisou dele.
- Validação adversarial. Tente provar que falhou. Se não conseguir, aí sim publique o "zero downtime".
- Gradual nem sempre é mais seguro. Ramp por peso reabriria risco de split-brain; a virada atômica com rollback instantâneo era a opção conservadora — mesmo parecendo a ousada.
- Standby "pronto" e standby "quente" são coisas diferentes. Foi um drill que revelou isso — e é exatamente pra isso que drills existem. O incidente virou postmortem, o postmortem virou runbook, e o failover ficou melhor do que a migração o entregou.
Outros cases
Um time de plataforma de uma pessoa só 60+ tenants, dois data centers, um engenheiro — por arquitetura, não heroísmo. 12 anos de homelab: meu stack de IA self-hosted LLM local em GPU, RAG híbrido, e um agente que pentesta o meu próprio SaaS.Sempre aberto a trocar ideia com quem opera infraestrutura de verdade.
LinkedIn