Pular para o conteúdo
Voltar para o acervo
Engenharia5 min de leituraPasso 8 de 9

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.

· Gabriel Dias
migracaoexpand-contractpostgresdeploy

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.

  1. 1ExpandirColuna nova, aceitando nulo. Nenhum código usa ainda.
  2. 2Escrever nos doisO código novo grava na velha e na nova.
  3. 3Migrar o históricoLote pequeno, com pausa, preenche o passado.
  4. 4ContrairLê só da nova. A velha some num deploy seguinte.
Cada caixa é um deploy. A coluna velha só some na última.

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:

  1. Nunca renomeie. Adicione, migre, remova.
  2. Toda migração é compatível com a versão anterior do código.
  3. 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

Fala comigo

Dúvida sobre o artigo? Me chama no WhatsApp

Sem formulário e sem lista de e-mail. Se você discorda de alguma coisa que eu escrevi, ou quer contar como resolveu aí, a conversa é direta comigo.

Abrir conversa