Estratégias de migração de banco de dados sem downtime
1. Fundamentos e Desafios das Migrações sem Downtime
1.1. Definição de migração sem downtime e cenários de aplicação
Migração de banco de dados sem downtime refere-se ao processo de alterar esquemas, dados ou engines de armazenamento enquanto o sistema permanece operacional para usuários finais. Cenários típicos incluem:
- Adição de novas colunas em tabelas com milhões de registros
- Troca de engine de armazenamento (ex: MyISAM para InnoDB)
- Migração entre versões de banco de dados (ex: PostgreSQL 12 para 15)
- Replicação para novo cluster com hardware atualizado
1.2. Riscos comuns
Os principais riscos envolvem:
- Inconsistência de dados: quando escritas concorrentes ocorrem durante a migração
- Degradação de performance: locks prolongados ou replicação atrasada
- Rollback complexo: necessidade de restaurar snapshots completos
1.3. Diferença entre tipos de migração
| Tipo | Exemplo | Complexidade |
|---|---|---|
| Esquema | ALTER TABLE ADD COLUMN | Média |
| Dados | Transformação de valores | Alta |
| Engine | MyISAM → InnoDB | Alta |
2. Estratégia Expand-and-Contract (Mestre-Escravo)
2.1. Fase Expand: adicionar novas colunas/tabelas sem remover as antigas
-- Fase Expand: adicionar nova coluna sem remover a antiga
ALTER TABLE usuarios
ADD COLUMN email_novo VARCHAR(255) NULL;
-- Criar trigger para manter sincronia
CREATE OR REPLACE FUNCTION sync_email()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.email_novo := COALESCE(NEW.email_novo, NEW.email);
ELSIF TG_OP = 'UPDATE' THEN
IF NEW.email_novo IS NULL THEN
NEW.email_novo := NEW.email;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sync_email
BEFORE INSERT OR UPDATE ON usuarios
FOR EACH ROW EXECUTE FUNCTION sync_email();
2.2. Fase Migrate: sincronização bidirecional com CDC
-- Configurar replicação lógica para sincronizar dados
-- No publisher:
CREATE PUBLICATION pub_migracao FOR TABLE usuarios;
-- No subscriber:
CREATE SUBSCRIPTION sub_migracao
CONNECTION 'host=origem dbname=meubanco user=replicador password=senha'
PUBLICATION pub_migracao;
2.3. Fase Contract: remoção gradual dos artefatos legados
-- Fase Contract: após validação, remover coluna antiga
-- Primeiro: verificar se todos os registros estão sincronizados
SELECT COUNT(*) FROM usuarios
WHERE email_novo IS NULL AND email IS NOT NULL;
-- Segundo: remover trigger e coluna antiga
DROP TRIGGER IF EXISTS trg_sync_email ON usuarios;
ALTER TABLE usuarios DROP COLUMN email;
ALTER TABLE usuarios RENAME COLUMN email_novo TO email;
3. Migração de Esquema com Versionamento e Lock Mínimo
3.1. Uso de ferramentas com timeout controlado
-- Exemplo com Flyway (configuração de timeout)
flyway.configure({
connectRetries: 5,
lockRetryCount: 10,
outOfOrder: true,
validateOnMigrate: false
});
-- Migration V1__add_column.sql
ALTER TABLE pedidos
ADD COLUMN status_pagamento VARCHAR(20) DEFAULT 'pendente' NOT NULL;
-- O DEFAULT evita locks longos em tabelas grandes
3.2. Técnicas de alteração sem lock
# Usando gh-ost (GitHub Online Schema Migration)
gh-ost \
--host="127.0.0.1" \
--database="meubanco" \
--table="pedidos" \
--alter="ADD COLUMN desconto DECIMAL(10,2) DEFAULT 0.00" \
--execute \
--max-load=Threads_running=50 \
--critical-load=Threads_running=100 \
--chunk-size=1000 \
--throttle-control-replicas="replica1,replica2"
3.3. Colunas com valor default e nullable
-- Estratégia segura para adicionar coluna sem lock
ALTER TABLE grandes_tabelas
ADD COLUMN nova_coluna VARCHAR(100) NULL;
-- Depois preencher em lotes
UPDATE grandes_tabelas
SET nova_coluna = 'valor_padrao'
WHERE nova_coluna IS NULL
LIMIT 10000;
4. Replicação Lógica e Troca de Engine
4.1. Configuração de replicação lógica
-- PostgreSQL: publicar tabela específica
CREATE PUBLICATION pub_vendas FOR TABLE vendas, itens_venda;
-- Configurar slot de replicação
SELECT pg_create_logical_replication_slot('slot_migracao', 'pgoutput');
-- MySQL: configurar binlog para replicação
[mysqld]
server-id=1
log_bin=mysql-bin
binlog_format=ROW
binlog_row_image=FULL
expire_logs_days=7
4.2. Processo de catch-up e validação
-- Verificar lag de replicação no PostgreSQL
SELECT
slot_name,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes
FROM pg_replication_slots;
-- Validar consistência com pt-table-checksum
pt-table-checksum \
--host=origem \
--replicate=meubanco.checksums \
--tables=vendas \
--chunk-size=1000
4.3. Cutover controlado com proxy
# Configuração HAProxy para cutover gradual
frontend db_frontend
bind *:3306
default_backend db_backend
backend db_backend
balance roundrobin
server db_old 192.168.1.10:3306 weight 100
server db_new 192.168.1.20:3306 weight 0
# Após validação, aumentar peso do novo
# server db_new 192.168.1.20:3306 weight 50
# Finalmente:
# server db_old 192.168.1.10:3306 weight 0
5. Estratégias de Rollback e Pontos de Verificação
5.1. Definição de checkpoints incrementais
-- Criar checkpoint antes da migração
SELECT pg_create_restore_point('checkpoint_migracao_v2');
-- Registrar snapshots lógicos
CREATE TABLE migration_checkpoints (
id SERIAL PRIMARY KEY,
checkpoint_name VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
wal_lsn pg_lsn,
status VARCHAR(20) DEFAULT 'active'
);
INSERT INTO migration_checkpoints (checkpoint_name, wal_lsn)
VALUES ('v2_add_desconto', pg_current_wal_lsn());
5.2. Técnicas de rollback sem perda
-- Rollback usando replicação reversa
-- 1. Parar aplicação de gravação no novo
-- 2. Reverter replicação lógica
ALTER SUBSCRIPTION sub_migracao DISABLE;
-- 3. Restaurar configuração antiga via proxy
-- 4. Verificar consistência
5.3. Testes em staging
# Simulação de falha em staging
# 1. Criar cenário de carga
pgbench -c 50 -T 300 -f benchmark.sql meubanco_staging
# 2. Executar migração paralela
gh-ost --test-on-replica --host=staging --table=pedidos --alter="ADD COLUMN teste INT"
# 3. Verificar se há deadlocks ou perda de dados
6. Monitoramento e Validação Contínua
6.1. Métricas críticas
-- Monitorar latência de replicação
SELECT
application_name,
state,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
-- Taxa de erros durante migração
SELECT
COUNT(*) FILTER (WHERE state = 'error') AS erros,
COUNT(*) AS total_operacoes
FROM migration_log
WHERE timestamp > NOW() - INTERVAL '5 minutes';
6.2. Ferramentas de validação automática
# data-diff: comparar datasets entre origem e destino
data-diff \
--source "postgresql://user:pass@origem/db" \
--target "postgresql://user:pass@destino/db" \
--table "vendas" \
--key "id" \
--threads 4
# pt-table-sync para corrigir divergências
pt-table-sync \
--sync-to-master \
--replicate=meubanco.checksums \
h=destino,u=admin,p=senha
6.3. Estratégias de canary release
-- Roteamento baseado em hash de usuário
CREATE TABLE routing_config (
user_id_hash INT PRIMARY KEY,
db_target VARCHAR(50)
);
-- Migrar 5% dos usuários primeiro
INSERT INTO routing_config
SELECT MOD(id, 100) AS user_id_hash,
CASE WHEN MOD(id, 100) < 5 THEN 'db_novo' ELSE 'db_velho' END
FROM usuarios;
7. Casos Práticos e Padrões Anti-Pattern
7.1. Migração de VARCHAR para TEXT sem downtime
-- Passo 1: Adicionar nova coluna TEXT
ALTER TABLE artigos ADD COLUMN conteudo_text TEXT;
-- Passo 2: Preencher em lotes
DO $$
DECLARE
batch_size INT := 1000;
affected INT;
BEGIN
LOOP
UPDATE artigos
SET conteudo_text = conteudo
WHERE conteudo_text IS NULL
AND conteudo IS NOT NULL
LIMIT batch_size;
GET DIAGNOSTICS affected = ROW_COUNT;
EXIT WHEN affected = 0;
COMMIT;
PERFORM pg_sleep(0.1); -- pausa para reduzir IO
END LOOP;
END $$;
-- Passo 3: Trocar colunas
ALTER TABLE artigos DROP COLUMN conteudo;
ALTER TABLE artigos RENAME COLUMN conteudo_text TO conteudo;
7.2. Troca de engine MyISAM para InnoDB
-- Estratégia com replicação
-- 1. Criar tabela InnoDB
CREATE TABLE logs_innodb LIKE logs;
ALTER TABLE logs_innodb ENGINE=InnoDB;
-- 2. Inserir dados em lotes
INSERT INTO logs_innodb SELECT * FROM logs WHERE id > 0 LIMIT 10000;
-- 3. Sincronizar com trigger
CREATE TRIGGER trg_logs_sync AFTER INSERT ON logs
FOR EACH ROW
BEGIN
INSERT INTO logs_innodb VALUES (NEW.*);
END;
-- 4. Cutover: renomear tabelas
RENAME TABLE logs TO logs_myisam, logs_innodb TO logs;
7.3. Armadilhas comuns
-- ANTI-PATTERN: Renomear coluna diretamente
-- NÃO FAÇA:
ALTER TABLE usuarios RENAME COLUMN email TO email_antigo;
-- FAÇA:
-- 1. Adicionar nova coluna
ALTER TABLE usuarios ADD COLUMN email_novo VARCHAR(255);
-- 2. Copiar dados
UPDATE usuarios SET email_novo = email;
-- 3. Remover coluna antiga
ALTER TABLE usuarios DROP COLUMN email;
ALTER TABLE usuarios RENAME COLUMN email_novo TO email;
-- ANTI-PATTERN: Migrar chave primária
-- NÃO FAÇA:
ALTER TABLE pedidos DROP PRIMARY KEY;
ALTER TABLE pedidos ADD PRIMARY KEY (novo_id);
-- FAÇA: usar replicação lógica com nova tabela
Referências
- Documentação oficial gh-ost — Ferramenta de migração de esquema online do GitHub para MySQL, sem bloqueio de tabelas
- Percona Toolkit: pt-online-schema-change — Ferramenta da Percona para alterar tabelas MySQL sem downtime
- PostgreSQL Logical Replication Documentation — Guia oficial sobre replicação lógica no PostgreSQL para migrações contínuas
- Flyway Database Migrations — Documentação oficial do Flyway para versionamento de esquemas com rollback e validação
- HAProxy Configuration for Database Load Balancing — Guia de configuração do HAProxy para cutover controlado em migrações de banco
- Alembic Migrations Documentation — Tutorial oficial do Alembic para migrações de esquema com suporte a operações online