Migração de esquema sem downtime
Por que renomear coluna quebra produção, e o padrão de quatro passos que transforma migração em tarefa de terça-feira.
Existe um princípio que explica praticamente todo incidente de migração de banco, e ele cabe em uma frase:
Durante o deploy, as duas versões do código rodam ao mesmo tempo contra o mesmo banco.
Sempre. Rolling update, blue-green, canário: em algum momento existe código velho e código novo vivos simultaneamente, e os dois falam com o mesmo esquema.
Logo, toda alteração de esquema precisa ser compatível com a versão anterior do código. Sem exceção.
O que isso proíbe
Renomear coluna. Quebra a versão antiga no instante em que roda.
Remover coluna que ainda é usada. Mesmo problema.
Adicionar coluna NOT NULL sem valor padrão. A versão antiga não sabe preencher, e o INSERT dela falha.
Mudar o tipo de uma coluna de forma incompatível. A versão antiga escreve um valor que não cabe mais.
Adicionar restrição que o dado existente viola. Falha na hora ou impede escrita legítima.
O padrão expand-contract
Também chamado de parallel change. Quatro passos, e cada um é um deploy independente e reversível.
- 1ExpandirColuna nova, aceitando nulo. Nenhum código usa ainda.
- 2Escrever nos doisO código novo grava na velha e na nova.
- 3Migrar o históricoLote pequeno, com pausa, preenche o passado.
- 4ContrairLê só da nova. A velha some num deploy seguinte.
Passo 1: Expandir
Adicione a estrutura nova sem tocar na velha. Coluna nova, aceitando nulo, sem restrição. Nada quebra, porque nenhum código usa ainda.
Passo 2: Escrever nos dois
A versão nova do código passa a gravar na coluna velha e na nova. Deploy. Agora todo dado novo existe nos dois lugares, e a versão antiga continua funcionando lendo a velha.
Passo 3: Migrar o histórico
Um processo em lote preenche a coluna nova para os registros antigos. Em lotes pequenos, com pausa entre eles, para não travar a tabela nem inundar o log de replicação.
Passo 4: Contrair
Quando tudo estiver preenchido e nenhuma versão antiga estiver mais no ar, o código passa a ler só da nova. Num deploy seguinte, não no mesmo, você remove a coluna velha.
Quatro deploys em vez de um. Parece burocrático. É o que transforma migração em tarefa de terça à tarde, em vez de janela de madrugada com o time inteiro acordado.
E cada passo é reversível sozinho, o que é a propriedade que mais importa quando algo dá errado às onze da noite.
O que dá errado no volume
Uma migração que roda em milissegundos numa tabela vazia pode travar a tabela por vinte minutos em produção, com dez milhões de linhas.
Os casos clássicos, em Postgres:
CREATE INDEX trava escrita. Use CREATE INDEX CONCURRENTLY. É mais lento, não trava, e pode
falhar deixando um índice inválido que você precisa remover manualmente. Conheça esse comportamento
antes da noite do deploy.
ALTER TABLE que reescreve a tabela. Mudar tipo de coluna geralmente reescreve tudo, com lock
exclusivo. Em versões modernas, adicionar coluna com valor padrão não reescreve mais, mas confira a
sua versão.
Adicionar chave estrangeira valida a tabela inteira sob lock. A saída é adicionar como
NOT VALID e depois rodar VALIDATE CONSTRAINT, que usa um lock mais fraco.
UPDATE em massa gera volume enorme de WAL, atrasa réplicas e pode encher disco. Sempre em lotes,
com pausa, monitorando o lag.
O lock que espera. Em Postgres, um ALTER que precisa de lock exclusivo entra na fila, e enquanto
ele espera, ele bloqueia todas as queries que chegam depois. Uma transação longa segurando lock leve
pode, indiretamente, travar a tabela inteira. Use lock_timeout curto e tente de novo, em vez de
deixar o comando pendurado.
Teste a migração com volume
Isso é o que quase ninguém faz e o que mais evita surpresa.
Rode a migração numa cópia com volume parecido com o de produção, nem que seja uma vez, antes do deploy grande. Cronometre. Se levar vinte minutos, você acabou de descobrir que precisa de outra abordagem, e descobriu numa terça-feira, não numa madrugada.
E teste a migração para trás também. Muita ferramenta gera o script de reversão automaticamente e ninguém nunca executa. No dia em que precisar, ele não funciona.
Migração de dado versus migração de esquema
Vale separar as duas.
Mudança de esquema é rápida ou lenta dependendo do comando, e é reversível na maioria dos casos.
Mudança de dado (recalcular um campo, normalizar valores, reprocessar histórico) é sempre demorada e frequentemente irreversível. Trate como processo em lote com estado: registre o que já foi processado, permita retomar, e faça em lotes idempotentes.
O caso do renomeio de tabela
O mesmo princípio, num nível acima. Você quer renomear usuarios para contas.
Expandir: crie a nova tabela e um gatilho que replica as escritas de uma para a outra. Ou uma view com o nome antigo apontando para a nova, se o seu banco permitir escrita em view.
Migrar: copie o histórico em lotes.
Contrair: mova as leituras, depois as escritas, depois remova a antiga.
Mesma coreografia, mais peças. E a pergunta que vale fazer antes: esse renomeio entrega valor suficiente para justificar quatro deploys? Às vezes a resposta honesta é não.
Resumindo
Três regras que evitam a maioria dos incidentes:
- Nunca renomeie. Adicione, migre, remova.
- Toda migração é compatível com a versão anterior do código.
- Teste com volume antes.
E uma quarta, que é mais cultural: migração não é apêndice da tarefa. Ela é parte do desenho, e merece o mesmo cuidado que o código.
Leia depois disto
- IA AplicadaPasso 10O que é um LLM de verdade: token, embedding e atençãoSem misticismo e sem matemática: o que o modelo faz, por que ele alucina, e como isso muda as suas decisões de arquitetura.Ler artigo
- EngenhariaPasso 9Observabilidade: métrica, log e trace, e o que cada um respondeOs três pilares não são intercambiáveis. Cada um responde uma pergunta diferente, e usar o errado é o motivo de investigações que duram horas.Ler artigo
- EngenhariaPasso 7Os cinco padrões de resiliência que evitam queda em cascataComo uma dependência secundária lenta derruba o sistema inteiro em noventa segundos, e os cinco padrões que impedem isso.Ler artigo