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