rochasolutions
Back-end

Migração de Banco de Dados: Como Fazer Sem Downtime

Danilo Rocha5 min de leitura

O que é migração de banco de dados sem downtime

Migração de banco de dados sem downtime é o processo de alterar o esquema ou mover dados de uma tabela em produção sem tirar a aplicação do ar nem bloquear as leituras e escritas em andamento. A técnica mais usada para isso é o padrão expand and contract: primeiro se adiciona a nova estrutura ao lado da antiga, depois o tráfego migra aos poucos, e só no final a estrutura antiga é removida.

Isso importa porque uma migração direta, do tipo renomear a coluna e seguir em frente, trava a tabela pelo tempo que o banco leva para reescrever cada linha. Numa tabela de alguns milhares de registros isso passa despercebido. Numa tabela hipotética de dez milhões de linhas, o mesmo comando pode segurar um lock exclusivo por minutos, e toda consulta que depende daquela tabela fica na fila esperando.

Por que uma migração direta trava a aplicação em produção

Uma migração direta trava a aplicação porque a maioria dos comandos DDL, como ALTER TABLE para mudar o tipo de uma coluna ou adicionar uma constraint, pede um lock exclusivo na tabela inteira enquanto reescreve o arquivo de dados. Enquanto esse lock está ativo, nenhuma outra transação consegue ler nem escrever naquela tabela.

O efeito prático depende do tamanho da tabela e da versão do banco. PostgreSQL, a partir da versão 11, tornou ADD COLUMN com valor padrão uma operação rápida, sem reescrita da tabela toda. Mas ALTER COLUMN para trocar o tipo de uma coluna, ou adicionar uma constraint NOT NULL sem validação prévia, ainda segura o lock pelo tempo da operação inteira. Se isso coincidir com o horário de pico, a fila de requisições cresce até expirar antes de ser atendida.

Como fazer migração de banco de dados sem downtime

O padrão expand and contract quebra a mudança em etapas pequenas, cada uma reversível, em vez de uma alteração única e irreversível.

  1. Adicione a nova coluna, tabela ou índice sem remover nada da estrutura antiga. Um índice, por exemplo, deve nascer com CREATE INDEX CONCURRENTLY para não bloquear escritas durante a construção.
  2. Ajuste a aplicação para escrever nos dois lugares ao mesmo tempo, na estrutura antiga e na nova. Essa etapa é o dual write, coberto em detalhe mais abaixo.
  3. Rode um script de backfill que copia os dados existentes da estrutura antiga para a nova, em lotes pequenos, para não competir por recursos com o tráfego real.
  4. Ajuste a aplicação para ler da estrutura nova, mantendo a escrita dupla como rede de segurança.
  5. Confirme, com uma consulta de comparação, que os dois conjuntos de dados batem.
  6. Remova a escrita na estrutura antiga e, só depois de um período de observação sem incidentes, remova a estrutura antiga em si.

Cada etapa pode ser revertida sozinha. Se o passo 4 revelar um problema, basta voltar a ler da estrutura antiga sem mexer em mais nada.

Vale acompanhar cada etapa com observabilidade de aplicação de verdade, não só o log de erro padrão. Latência da query de backfill, número de linhas processadas por minuto e taxa de divergência entre as duas estruturas dizem se o passo 3 está indo bem antes de você chegar ao passo 4 e trocar a leitura de lugar.

Quando usar dual write e quando evitar

Dual write é a prática de escrever o mesmo dado em duas estruturas ao mesmo tempo durante a transição. Funciona bem quando a lógica de escrita é simples e as duas estruturas conseguem ficar consistentes dentro da mesma transação.

O problema aparece quando a escrita dupla depende de duas chamadas separadas, por exemplo um INSERT no banco relacional seguido de uma chamada de rede para outro serviço. Se a segunda chamada falhar depois que a primeira já foi confirmada, os dois lados divergem sem que ninguém perceba na hora. Nesse cenário, vale considerar um processo assíncrono de sincronização, com fila e reprocessamento, em vez de duas escritas síncronas dentro da mesma requisição.

Erros comuns que causam downtime mesmo com o padrão correto

Um erro comum é adicionar uma constraint NOT NULL direto, sem o passo intermediário de validação. PostgreSQL permite adicionar a constraint como NOT VALID primeiro, o que não trava a tabela, e validar os dados existentes depois com VALIDATE CONSTRAINT, que trava por muito menos tempo porque só confirma o que já foi verificado.

Outro erro é esquecer o índice na coluna certa antes do backfill. Um script que copia dez milhões de linhas fazendo uma busca sem índice na tabela de destino demora horas em vez de minutos, e nesse tempo todo a escrita dupla continua competindo por lock com o próprio backfill.

O terceiro erro é rodar a migração inteira numa transação só. Uma transação longa mantém locks pelo tempo inteiro de execução e, se falhar no meio, desfaz o trabalho já feito. Quebrar o backfill em lotes de alguns milhares de linhas, cada um na sua própria transação, limita o dano de qualquer falha a um lote só.

O objetivo não é migrar mais rápido

O objetivo do padrão expand and contract não é migrar mais rápido. É migrar em etapas pequenas o suficiente para que cada uma seja reversível e observável, trocando uma mudança arriscada por várias mudanças pequenas e chatas. Isso custa mais linhas de script e mais dias de calendário do que um ALTER TABLE direto, mas evita a versão mais cara de qualquer migração: a que derruba a aplicação no meio do expediente.

Se sua aplicação tem uma migração de esquema pendente e o time não quer arriscar o horário de pico, fale com a gente para planejar as etapas.