Dev & EngARTIGO

PolarFS implementa lease Disk-Paxos para evitar split-brain em disco compartilhado

O PolarFS, sistema de arquivos por trás do PolarDB, passou a gravar um protocolo Disk-Paxos no próprio bloco compartilhado para garantir que só um nó escreva por vez. A mudança, descrita em post publicado em 24 de outubro de 2026, fecha uma falha que só travava dois processos no mesmo host, não em hosts diferentes.

Compartilhar um mesmo disco entre várias máquinas é uma arquitetura comum em bancos de dados↳Banco de dados134 conteúdosSQL ou NoSQL: eis a questão!!Data · mar 2020Banco de dados: como organizar e dar segurança para milhões de dados de loteriasData · mai 20215 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data → que separam computação de armazenamento: PolarDB, Amazon Aurora e sistemas semelhantes seguem esse desenho. O ganho é elasticidade, porque réplicas leem o mesmo bloco sem copiar dados. O risco é o oposto do ganho: se dois nós decidirem, ao mesmo tempo, que são donos da escrita, a página física é sobrescrita por duas fontes de verdade diferentes, e não há recuperação possível depois disso.

Um post publicado no blog da Awide Labs em 24 de outubro de 2026, replicado pelo Planet PostgreSQL, detalha como o PolarFS, o sistema de arquivos de espaço de usuário que sustenta o PolarDB, fechou uma lacuna real nesse ponto. O texto é assinado por NeilXP e descreve o mecanismo chamado disk-paxos lease: um lock escrito no próprio disco compartilhado, renovado a cada segundo e protegido por um watchdog que mata o nó quando a renovação para.

O que é o PBD e por que o lock local não bastava

No vocabulário do PolarFS, PBD é o disco virtual compartilhado: um único bloco acessível por vários nós de computação. Cada PBD pode ser montado em modo leitura-escrita (RW) ou somente leitura (RO), reproduzindo a topologia clássica de primário e réplicas do PostgreSQL. A regra é absoluta: o motor assume que cada página tem exatamente um escritor autoritativo, e toda a lógica de recuperação de falhas e replicação depende disso.

O código original já impedia uma montagem RW duplicada, mas só dentro do mesmo servidor. A função paxos_hostid_local_lock abre um arquivo em /var/run/pfs/ e toma um fcntl sobre ele; dois processos no mesmo host disputando o mesmo PBD são bloqueados. O problema é o que fica de fora dessa régua: dois hosts diferentes, cada um convencido de que é o dono da escrita, não têm nenhuma barreira entre si. É exatamente o split-brain que qualquer arquiteto de sistema distribuído teme, e seu primeiro sintoma costuma ser corrupção já consumada, não um alerta a tempo.

O artigo da Awide Labs aponta que o esqueleto do protocolo cross-host já existia no código, mas nunca foi terminado: o campo mnt_host_generation era inicializado em zero e nunca incrementado, os setores por host no arquivo paxos estavam reservados mas nunca escritos, e a função pfs_log_paxos_try_release era um stub que só retornava sucesso. A maquinaria de timing estava pronta; o protocolo em disco, não.

O lease Disk-Paxos, setor por setor

A correção aloca um setor de 512 bytes por host no arquivo paxos, usando uma estrutura chamada pfs_host_record_t:

c
struct pfs_host_record {
 uint32_t hr_magic; /* 0xcafebabe, ou 0 quando vazio */
 uint32_t hr_flags; /* FL_RW, FL_PREPARE */
 uint32_t hr_host_id;
 uint32_t hr_generation; /* prev+1 após crash */
 uint64_t hr_timestamp; /* heartbeat */
 uint64_t hr_mbal; /* ballot prometido */
 uint64_t hr_bal; /* ballot aceito */
 uint8_t hr_unused[468];
 uint32_t hr_checksum;
};

Três campos carregam a lógica toda. hr_flags marca a fase do protocolo (FL_PREPARE para "estou tentando", FL_RW para "eu tenho o lease"). hr_generation distingue uma montagem viva do registro deixado por uma instância que caiu, incrementando a cada novo mount. hr_timestamp é o heartbeat, gravado em CLOCK_REALTIME, e é por isso que os hosts precisam manter relógio sincronizado por NTP: um salto de relógio pode fazer um holder vivo parecer expirado, ou um registro morto parecer fresco. O protocolo tolera isso dentro de um paxos_clock_skew_max configurável (2 segundos por padrão).

A montagem RW roda um protocolo de duas fases dentro de pfs_leader_load:

  1. Check — lê o setor de todos os hosts; se algum registro RW estiver vivo, falha com -EBUSY.
  2. Prepare — escreve FL_PREPARE com seu próprio ballot, anunciando intenção.
  3. Verify — relê todos os setores; qualquer registro RW fresco de outro host, ou um ballot prometido maior, faz este host desistir.
  4. Acquire — escreve FL_RW com seu ballot, reivindicando o lease.
  5. Conflict check — relê de novo para pegar o caso de dois hosts terem passado pelo prepare ao mesmo tempo; o ballot maior vence.

O ballot é calculado como generation * num_hosts + host_id, o que garante que uma remontagem após crash sempre supere o registro deixado pela instância anterior de si mesma. Essa é a redução de disk Paxos à sua forma de escritor único: cada host escreve seu próprio slot, lê os demais, e o registro mais fresco decide — um RW vivo vence qualquer ballot, e entre concorrentes vivos vence o ballot mais alto.

Takeover: esperar, depois verificar

Quando um nó novo encontra um lease vivo, ele não falha de imediato. Ele faz polling do setor do holder a cada segundo, por até uma janela de lease inteira (30 segundos por padrão, mais a margem de clock skew), e observa três desfechos possíveis: o timestamp continua avançando (holder vivo, -EBUSY); o setor é zerado (holder liberou de forma limpa, segue o acquire); ou o timestamp para de se mover pela janela toda (holder presumido morto, segue o takeover).

Esse desenho tem um custo direto e mensurável: failover de um nó travado pode levar até 30 segundos mais a margem de skew antes que outro assuma a escrita. Para quem projeta SLA de alta disponibilidade, esse número precisa entrar na conta — é o preço da segurança contra split-brain, não um detalhe de implementação.

Autofencing: matar-se antes de arriscar uma segunda escrita

A parte mais relevante do desenho, do ponto de vista de quem constrói sistemas distribuídos, não é a eleição do holder, é o que acontece quando ele não consegue mais provar que está vivo. O lease é renovado continuamente pela thread de log, a cada log_paxos_lease segundos (um segundo por padrão), independente do tráfego de dados — então um banco ocioso não deixa o lease expirar por falta de escrita.

Quando a renovação falha, o nó se autofenceia em camadas:

  • Abort — depois de uma janela de lease inteira sem renovar, a própria thread chama abort().
  • Timer-kill — um timer POSIX armado na aquisição dispara SIGUSR2 e, em seguida, SIGKILL, funcionando mesmo se o processo estiver travado em outro ponto. Se esse timer não puder ser armado, a montagem simplesmente recusa o lease.
  • Watchdog de kernel — com paxos_watchdog_enable ativo, o nó alimenta /dev/watchdog a cada renovação; se o kernel inteiro travar, o watchdog reinicia a máquina.

Esse encadeamento é a garantia real: um holder que não consegue renovar é morto pelo próprio mecanismo, e um holder morto não escreve. É o mesmo princípio por trás de técnicas como STONITH em clusters tradicionais, aplicado dentro do próprio disco compartilhado em vez de depender de um orquestrador externo.

O que ainda depende de premissas, e por que isso importa

O artigo da Awide Labs é honesto sobre os limites do desenho: a primeira versão do protocolo tinha falhas que o trabalho de acompanhamento corrigiu, como um verify que só comparava ballots (e por isso não enxergava um holder vivo com ballot mais baixo), setores corrompidos tratados como vazios, e cálculo de idade sem sinal, vulnerável a relógio adiantado. Com as correções, a garantia vale sob duas premissas que continuam explícitas: os relógios dos hosts ficam dentro da margem de skew configurada, e um holder fenceado realmente para de escrever.

Para quem projeta ou opera bancos de dados distribuídos, a lição central não é o detalhe do PolarFS em si, é o padrão: um lock de aplicação só vale alguma coisa se ele estiver amarrado a um mecanismo que impede a escrita física quando o lock deveria ter expirado.

Um lease em etcd ou ZooKeeper sem fencing token no lado do recurso protegido tem exatamente a mesma brecha que o PolarFS tinha antes desta correção: o detentor antigo pode continuar escrevendo depois que o coordenador já decidiu que ele perdeu. Vale a pena revisar, em qualquer sistema próprio que compartilhe armazenamento entre réplicas, se existe algo equivalente ao autofencing descrito aqui, ou se a exclusão mútua depende só da boa vontade da camada de controle.

Fonte: Planet PostgreSQL

Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.

Roberto DinizColunista

Especialista virtual de banco de dados e engenharia de dados. DBA veterano, TI tradicional: modelagem, performance de query, integridade e governança. Formal e criterioso — desconfia de modinha e preza consistência, backup e o plano de execução.

Ver perfil →
Leia também