Tecnologias/02 - Bancos de Dados/01 - Relacionais/02 - MySQL6 min
Replicação e Alta Disponibilidade
Conceito
A replicação foi historicamente o maior diferencial do MySQL: fácil de montar, fácil de entender. É também o lugar onde mais gente perde dados sem perceber, porque o padrão é assíncrono.
O binlog: um log lógico, separado do redo
| Redo log (InnoDB) | Binlog (servidor) | |
|---|---|---|
| Nível | físico (páginas) | lógico (eventos de linha ou statements) |
| Serve para | recuperação de crash | replicação e PITR |
| Escopo | o motor | o servidor inteiro |
Três formatos, e a escolha importa:
binlog_format |
Grava | Problema |
|---|---|---|
STATEMENT |
o SQL executado | não determinístico: NOW(), RAND(), UUID() divergem na réplica |
ROW (padrão) |
as linhas antes/depois | seguro; volume maior |
MIXED |
statement, e row quando inseguro | intermediário |
Use ROW. É o único seguro e é pré-requisito para CDC (Debezium lê binlog em formato ROW,
assim como lê o WAL lógico do Postgres).
O fluxo, e onde ele quebra
PRIMÁRIO RÉPLICA
COMMIT
├─ redo (fsync) ──── durabilidade local
└─ binlog (fsync) ─── evento
│
└──[rede]──> I/O thread → relay log → applier thread(s) → aplica
↑ recebeu ↑ APLICOU (é aqui que o lag mora)
Dois estágios, dois lags diferentes. SHOW REPLICA STATUS mostra
Seconds_Behind_Source, que mede o applier — e é uma medida notoriamente enganosa
(ela mede a diferença de timestamp do evento em processamento, não a fila real).
GTID: o que tornou failover viável
Antes, a réplica era posicionada por (arquivo de binlog, offset) — e num failover era
preciso descobrir manualmente qual offset do novo primário corresponde ao que a réplica já
tinha. Erro humano garantido.
Com GTID (gtid_mode = ON), cada transação recebe um id global
uuid_do_servidor:número. A réplica sabe exatamente o conjunto que já aplicou
(gtid_executed) e pede o resto. Failover e re-apontamento tornam-se declarativos:
CHANGE REPLICATION SOURCE TO SOURCE_HOST='novo-primario', SOURCE_AUTO_POSITION = 1;
GTID é pré-requisito prático para qualquer HA moderna (InnoDB Cluster, Orchestrator, ProxySQL com failover).
Replicação paralela: como o applier deixou de ser gargalo
O primário escreve com N threads; a réplica aplicava com uma. Em carga de escrita alta, o lag só crescia. A solução:
replica_parallel_workers = 8
replica_parallel_type = LOGICAL_CLOCK # aplica em paralelo transações que
# confirmaram juntas no primário
binlog_transaction_dependency_tracking = WRITESET # ainda mais paralelismo:
# transações que tocam linhas diferentes
replica_preserve_commit_order = ON # mantém a ordem de commit visível
WRITESET é o ajuste que mais reduz lag em cargas reais, e muita gente não conhece: ele
detecta independência por conjunto de linhas escritas, não por janela de commit.
Semi-síncrono: fechando a janela de perda
rpl_semi_sync_source_enabled = 1
rpl_semi_sync_source_wait_for_replica_count = 1
rpl_semi_sync_source_timeout = 1000 # ms; depois volta a assíncrono
O COMMIT espera uma réplica confirmar o recebimento (não a aplicação). Reduz a
janela de perda de "tudo que não replicou" para "quase nada". Note o timeout: passado
ele, o primário volta a assíncrono sozinho — protege disponibilidade e reabre a janela
de perda silenciosamente.
As topologias de HA
| Solução | Como funciona | Failover |
|---|---|---|
| Async + Orchestrator/ProxySQL | replicação clássica + ferramenta externa | automático, com ressalvas |
| Semi-sync + ferramenta | idem, com janela de perda menor | automático |
| Group Replication / InnoDB Cluster | grupo com consenso (Paxos), certificação de conflito | automático e nativo |
| Galera / Percona XtraDB Cluster | replicação virtualmente síncrona | automático |
| Gerenciado (RDS, Aurora) | o provedor resolve | automático |
InnoDB Cluster = Group Replication + MySQL Shell + MySQL Router (o proxy que esconde a troca de primário do app). É a resposta oficial da Oracle para HA, e o mais próximo de "vem na caixa" — algo que o Postgres não tem.
Trade-offs
Assíncrono é rápido e perde dados no failover. Se o primário confirma e cai antes de enviar o binlog, aquela transação não existe no novo primário — e pode reaparecer se o antigo voltar, criando divergência. Semi-síncrono reduz; consenso (Group Replication) resolve, ao custo de latência de rede em cada commit.
Group Replication multi-primário parece atraente e cobra caro. Escrever nos três nós gera certificação de conflito: quando dois nós alteram a mesma linha, um dos commits é abortado depois de a aplicação achar que deu certo. Na prática, quase todo mundo roda single-primary. Ver Availability vs Consistency.
Ler da réplica é a otimização mais tentadora e mais perigosa. Com lag assíncrono, um
fluxo "cria e lê" quebra intermitentemente. As saídas: ler do primário depois de escrever
(read-your-writes por rota), ou usar GTID — enviar WAIT_FOR_EXECUTED_GTID_SET para
garantir que a réplica alcançou a sua transação antes de ler.
O trade-off central: a facilidade da replicação MySQL é real e vem com um padrão inseguro.
binlog_format=ROW+gtid_mode=ON+ semi-síncrono + réplica paralela comWRITESETtransformam o padrão frágil em algo defensável — e nenhum deles é ligado por você por acidente.
Exemplo prático
Configuração de um par primário/réplica defensável
# --- comum ---
server_id = 1 # único por servidor!
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_format = ROW
binlog_row_image = FULL # MINIMAL é menor, mas complica CDC
sync_binlog = 1 # fsync do binlog a cada commit
innodb_flush_log_at_trx_commit = 1
log_replica_updates = ON # a réplica pode ser primário depois
# --- na réplica ---
read_only = ON
super_read_only = ON # impede até o root de escrever por acidente
replica_parallel_workers = 8
replica_parallel_type = LOGICAL_CLOCK
replica_preserve_commit_order = ON
binlog_transaction_dependency_tracking = WRITESET
sync_binlog = 1 e innodb_flush_log_at_trx_commit = 1Só os dois juntos dão durabilidade real. Com sync_binlog = 0, uma queda pode deixar o
InnoDB à frente do binlog: o dado existe localmente e nunca foi replicado. É a
configuração que produz "a réplica está com dado diferente do primário".
Read-your-writes com GTID
// 1) escreve no primário e captura o GTID da transação
var gtid string
_ = primario.QueryRowContext(ctx, `SELECT @@SESSION.gtid_executed`).Scan(>id)
// 2) antes de ler na réplica, espera ela alcançar aquele GTID (timeout 1s)
var res int
if err := replica.QueryRowContext(ctx,
`SELECT WAIT_FOR_EXECUTED_GTID_SET(?, 1)`, gtid).Scan(&res); err != nil || res != 0 {
// não alcançou em 1s: leia do primário em vez de mostrar dado velho
return lerDoPrimario(ctx)
}
return lerDaReplica(ctx)
Esse padrão resolve de forma correta o que normalmente é "resolvido" com um sleep(200ms)
espalhado pelo código.
Monitoramento
SHOW REPLICA STATUS\G
-- Replica_IO_Running / Replica_SQL_Running : devem ser 'Yes'
-- Seconds_Behind_Source : indicativo, não confie sozinho
-- Retrieved_Gtid_Set vs Executed_Gtid_Set : a fila real (recebido - aplicado)
-- Last_SQL_Error : por que a réplica parou
-- lag por worker e fila real, no performance_schema
SELECT * FROM performance_schema.replication_applier_status_by_worker\G
SELECT channel_name, service_state, count_transactions_in_queue
FROM performance_schema.replication_applier_status;
Como isso falha em produção
| Sintoma | Causa |
|---|---|
| Réplica parou com erro de duplicidade | escrita direta na réplica; ligue super_read_only |
| Dado divergente entre primário e réplica | binlog_format=STATEMENT com função não determinística; ou sync_binlog=0 |
| Lag crescendo em pico de escrita | applier sem paralelismo, ou transação gigante no primário |
| Escrita perdida após failover | replicação assíncrona; use semi-sync ou Group Replication |
Seconds_Behind_Source = 0 mas dado velho |
métrica enganosa; compare Retrieved vs Executed GTID |
| Dois primários aceitando escrita | split brain: failover sem fencing do nó antigo |
| CDC quebrando após rotação de binlog | retenção de binlog (binlog_expire_logs_seconds) menor que a janela de indisponibilidade do consumidor |
Aprofundamento aplicado
Replicado não significa aplicado
Uma réplica pode ter recebido eventos e ainda não os ter aplicado. O caminho crítico é receber, manter os logs e executar as transações. Atraso pode nascer na rede ou no applier; aumentar banda não resolve uma aplicação serializada por conflitos.
GTIDs identificam transações e ajudam a raciocinar sobre o conjunto executado. Eles não provam que o candidato a primário contém toda transação que algum cliente recebeu como confirmada, independentemente da configuração de replicação. Referência: replicação com GTIDs.
Ensaio de failover
- Registre uma escrita identificável e a confirmação percebida pelo cliente.
- Interrompa o primário no ambiente de teste.
- Escolha a réplica conforme os dados realmente disponíveis.
- Garanta que o antigo primário não continue aceitando escritas.
- Confira reconexão, presença da escrita e tratamento de operações incertas.
Resultado esperado: medir separadamente tempo de recuperação e eventual perda de
dados. Uma conexão encerrada durante COMMIT pode exigir reconciliação por identificador
de operação, porque o cliente não conhece automaticamente o resultado.
Relacionado
- 3. Redo, Undo e Locks
- 6. DDL Online e Operação
- 3. WAL, Checkpoint e Replicação
- Replication
- Availability vs Consistency
- Fail-Over
- RDS Multi-AZ vs Read Replica
Parte de MOC MySQL · Replication (docs)