- A Jepsen validou a durabilidade e a consistência do sistema de mensagens distribuídas NATS JetStream em vários cenários de falha.
- Os resultados dos testes mostraram perda de dados e ocorrência de split-brain em cenários de corrupção de arquivo (.blk, snapshot) e simulação de falha de energia.
- O JetStream, por padrão, executa
fsync a cada 2 minutos, o que pode deixar mensagens reconhecidas recentemente sem gravação em disco.
- A perda de dados e inconsistência de réplica pode ocorrer mesmo com crash de apenas um único nó de OS.
- A Jepsen recomenda alterar o padrão do NATS para
fsync=always ou documentar explicitamente o risco de perda de dados.
1. Contexto
- NATS é um sistema de streaming popular para publicação e assinatura de mensagens em fluxos
- O JetStream usa o algoritmo de consenso Raft para replicar dados e garantir entrega pelo menos uma vez (at-least-once)
- O JetStream afirma oferecer consistência linearizável e disponibilidade contínua, mas, pela teoria CAP, os dois requisitos não podem ser satisfeitos simultaneamente
- De acordo com a documentação da NATS, streams de 3 nós suportam a perda de 1 servidor, e streams de 5 nós suportam a perda de 2 servidores
- Uma mensagem é considerada armazenada com sucesso assim que o servidor envia
acknowledge para a requisição de publish
- Para consistência de dados é necessário um conjunto de nós em quórum (quorum), e em clusters de 5 nós, pelo menos 3 devem estar ativos para armazenar novas mensagens
2. Projeto dos testes
- A Jepsen executou os testes com o cliente JNATS 2.24.0 em ambiente de contêineres Debian 12 LXC
- Alguns testes foram feitos no ambiente Antithesis com a imagem oficial Docker da NATS
- Foi configurado um único stream JetStream (replicação 5) e injetaram-se falhas de interrupção de processo, crash, partição de rede, perda de pacotes e corrupção de arquivo
- Foi simulada falta de energia usando o sistema de arquivos LazyFS, para provocar perda de escritas sem
fsync
- Cada processo publica mensagens exclusivas e, ao fim do teste, verifica-se a presença das mensagens acknowledged em todos os nós
- Quando uma mensagem existe apenas em parte dos nós, ela é classificada como divergence (divergência)
3. Principais resultados
3.1 Perda total de dados do NATS 2.10.22 (#6888)
- Foi descoberto que uma simples falha de processo pode causar o desaparecimento de todo o stream JetStream
- Ocorreu o erro
"No matching streams for subject" e ele não foi recuperado por várias horas
- A causa foi inversão de snapshot do líder, exclusão de estado do Raft, etc., corrigida na versão 2.10.23
3.2 Perda de dados em corrupção de arquivo .blk (#7549)
- Quando ocorre erro de 1 bit ou truncamento em arquivos
.blk do JetStream, há perda de centenas de milhares de gravações reconhecidas
- Ex.: 1.367.069 mensagens, 679.153 perdidas
- Mesmo com a corrupção de apenas alguns nós, ocorre grande perda de dados e split-brain
- Ex.: nós
n1, n3, n5 com até 78% de mensagens perdidas
- A NATS está investigando o problema
3.3 Perda total de dados por corrupção de snapshot (#7556)
- Se um arquivo snapshot em
data/jetstream/$SYS/_js_/ for corrompido, o nó considera o stream como órfão (orphaned) e apaga todos os dados
- Mesmo com a corrupção de poucos nós, há indisponibilidade permanente do stream por não conseguir quórum de maioria
- Ex.: corrupção dos nós
n3, n5 → n3 foi eleito líder e excluiu todo o jepsen-stream
- A Jepsen aponta o risco de que um nó corrompido vire líder durante eleição
3.4 Perda de dados com configuração padrão de fsync (#7564)
- O JetStream faz
fsync por padrão apenas a cada 2 minutos, enquanto as mensagens são reconhecidas imediatamente
- Consequentemente, mensagens reconhecidas recentemente podem permanecer sem gravação em disco
- Em falta de energia ou crash de kernel, ocorre perda de mensagens reconhecidas equivalente a dezenas de segundos
- Ex.: 930.005 mensagens, 131.418 perdidas
- Falhas consecutivas de nós individuais podem causar a exclusão completa do stream
- Esse comportamento praticamente não é mencionado na documentação
- A Jepsen recomenda alterar o valor padrão de
fsync para fsync=always ou incluir aviso explícito sobre risco de perda de dados
3.5 Split-brain por crash único de OS (#7567)
- A perda de dados e a divergência de réplica podem ocorrer apenas com falta de energia ou crash de kernel em um único nó
- Em arquitetura líder-seguidor, se alguns nós confirmam uma gravação apenas com commit em memória e falham,
a maioria dos nós perde aquela gravação e avança para um novo estado
- Nos testes houve split-brain persistente após uma única falta de energia
- Perda de mensagens reconhecidas em faixas diferentes por nó foi observada
- A Jepsen cita casos semelhantes do Kafka e reforça que o mesmo risco também existe em sistemas baseados em Raft
4. Discussão e conclusão
- O problema de perda de dados total do 2.10.22 foi resolvido no 2.10.23
- No 2.12.1, ainda ocorrem perda de dados e split-brain por corrupção de arquivos e crash de OS
- A corrupção de arquivos
.blk e snapshot ainda pode causar perda de mensagens em nós específicos ou exclusão total do stream
- O intervalo padrão longo de
fsync mantém risco de perda de dados reconhecidos em falhas simultâneas de múltiplos nós
- A Jepsen sugere definir
fsync=always ou incluir alertas claros no documento
- A afirmação de “sempre disponível” do JetStream é impossível pelo teorema CAP, exigindo ajuste de documentação
- A Jepsen deixa claro que é possível provar a existência de bugs, mas não é possível provar a ausência de segurança
4.1 Papel do LazyFS
- O LazyFS foi usado para simular perda de escritas sem
fsync
- Também permite reproduzir diferentes erros de armazenamento, como escrita parcial (torn write), em simulações de falta de energia
- No estudo relacionado When Amnesia Strikes (VLDB 2024), erros semelhantes também foram reportados em PostgreSQL, Redis, ZooKeeper etc.
4.2 Próximos passos
- Não foram executadas verificações de perda de mensagem por consumidor único, ordem de mensagens e garantias Linearizable/Serializable
- A garantia de exactly-once também é tema de pesquisa futura
- Em adição e remoção de nós foram encontrados erros de documentação e falta de etapa obrigatória de health check (#7545)
- O procedimento seguro para reconfiguração de cluster ainda é incerto
1 comentários
Comentários do Hacker News
Agora fico até pensando se a IA talvez já consiga ler a documentação de um projeto e prever a possibilidade de perda de dados só pelo texto de marketing
As pessoas sempre dizem que “teoria é superestimada” ou que “hackear é melhor do que educação formal”, mas no fim acabam tropeçando no próprio espaço de problemas documentado
Também lidou bem com detalhes sutis de escalabilidade
Mas nunca usei a persistência, e não fazia ideia de que ela podia ser tão frágil assim
Fico surpreso por ser vulnerável até a corrupção de um único bit em arquivo
É uma referência excelente → Jepsen Glossary
Descobri o aphyr.com recentemente e estou animado, parece ter muitos insights
Depois, jepsen.io evoluiu para um projeto profissional e passou a ser operado para valer há cerca de 10 anos
É para melhorar desempenho em benchmark? Em clusters pequenos, esse tipo de configuração costuma ser a causa do problema
Muitas aplicações não exigem durabilidade total, então o lazy fsync pode ser útil
Ainda assim, deixar isso como padrão é discutível
Parece que isso poderia ser resolvido com batching, como no TCP corking
Falhas causadas por lazy fsync quase nunca acontecem ao mesmo tempo na maioria dos nós
Vantagem: oferece streams ilimitados com durabilidade no nível de object storage
Desvantagem: ainda não tem suporte a consumer groups
Se vários nós falharem ao mesmo tempo, pode haver perda de dados já commitados
Isso me lembra o marketing de “web scale” dos primeiros tempos do MongoDB
Acho que o padrão deveria ser sempre a opção mais segura
Isso até foi algo positivo para mim, porque dava para projetar o sistema em cima dessa premissa
Quando usei em 2018, ele tinha bom desempenho e era fácil de operar
Por exemplo, o nível padrão de isolamento de transação do PostgreSQL é read committed
O Redis também faz fsync por padrão a cada 1 segundo
Mesmo no Redis standalone, dá para configurar ack só após fsync, mas por causa do buffering do SO ainda é difícil garantir totalmente
No fim, o importante é entender exatamente o significado do ack
Se insistirem apenas em padrões seguros, o desempenho cai muito e aumenta a carga para o usuário ter que tunar tudo manualmente
Por exemplo, até o nível de isolamento padrão do Postgres é fraco o bastante para permitir race conditions
Referência: post sobre o teste Hermitage
Na era dos SSDs, etapas intermediárias como group-commit desapareceram, e agora o gargalo é o custo da troca de syscall
2 minutos é um intervalo longo demais (também vale considerar a diferença entre fdatasync e fsync)
Melhor simplesmente usar Redpanda
Talvez um batch flush periódico aumentasse a latência, mas mantivesse o throughput
Isso é parecido com agrupar rodadas de Paxos
Quando uma rodada termina, a próxima leva já começa imediatamente