- Testes do Jepsen confirmaram casos em que clusters Amazon RDS for PostgreSQL Multi-AZ não mantêm Snapshot Isolation, o nível de isolamento mais forte disponível em todos os nós
- A causa principal é que a ordem de visibilidade das transações no primary é determinada por locks em memória, enquanto no secondary segue a ordem do WAL, e essas duas ordens podem divergir
- Mesmo sem injeção de falhas nem failover, usando armazenamento
gp3e instânciasdb.m6id.large, surgem ciclos G-nonadjacent a cada poucos minutos com cerca de 150 write TPS / 1600 read-only TPS - A anomalia corresponde a Long Fork e apareceu em todas as versões testadas, do PostgreSQL 13.15 ao 17.4 com suporte da AWS; Short Fork/Write Skew não foi observado
- Em transações críticas para segurança, leituras em secondary read-only podem enxergar ordens de execução diferentes, então vale considerar usar apenas o writer endpoint ou incluir pelo menos 1 escrita
Atualização sobre a causa do Long Fork
- Sergey Melnik, da AWS, e os participantes dos comentários no HN matashii e Ants Aasma identificaram a causa do Long Fork em clusters PostgreSQL
- O primary do PostgreSQL determina a ordem em que as transações se tornam visíveis por meio de locks em memória
- O secondary torna as transações visíveis conforme a ordem no Write-Ahead Log (WAL)
- Quando a ordem dos locks e a ordem do WAL diferem, primary e secondary podem enxergar ordens aparentes diferentes para as transações
- Esse comportamento já havia sido discutido em uma mensagem da mailing list do PostgreSQL em 2013, e Melnik publicou no blog da AWS um texto explicando transaction visibility em clusters PostgreSQL e read replicas
- O Jepsen recomenda que AWS e PostgreSQL documentem esse problema junto com o trabalho de correção
Níveis de isolamento e arquitetura do RDS for PostgreSQL
- PostgreSQL é um banco de dados SQL open source de uso geral e oferece três níveis de isolamento transacional com MVCC
Read UncommittedeRead Committedfuncionam ambos como Read CommittedRepeatable Readna prática não fornece Repeatable Read, e sim Snapshot IsolationSerializablefornece Serializability
- Amazon RDS for PostgreSQL é um serviço da AWS que oferece clusters PostgreSQL gerenciados
- Automatiza provisionamento, gestão de armazenamento, replicação, backups e upgrades
- Multi-AZ deployments distribuem nós do banco entre várias zonas de disponibilidade para reduzir a chance de falhas correlacionadas
- O RDS usa replicação síncrona para só responder depois que a durabilidade da transação foi garantida no primary e em pelo menos 1 instância secondary
- Para o usuário, são fornecidas duas URLs que falam o protocolo wire do PostgreSQL
- primary endpoint: para transações de leitura e escrita
- reader endpoint: para transações somente leitura
- O primary endpoint suporta todos os níveis de isolamento do PostgreSQL, mas o secondary não suporta Serializable
- O nível de isolamento mais forte disponível em todos os nós é o que o PostgreSQL chama de
Repeatable Read, ou seja, Snapshot Isolation
Desenho do teste
- O Jepsen adaptou a biblioteca de testes para PostgreSQL ao Amazon RDS for PostgreSQL e usou um pequeno programa wrapper
- Em cada rodada de teste, o cluster RDS era provisionado pela API CreateDBCluster da AWS
- armazenamento
gp3 - instâncias
db.m6id.large
- armazenamento
- Um nó EC2 era iniciado para executar os testes, recebendo o main endpoint e o endpoint read-only do cluster RDS
- Não houve injeção de falhas nem failover foi acionado
- A carga principal consistia em transações que manipulavam listas de inteiros únicos
- Cada lista era armazenada em uma única row, codificada em um campo
TEXTcom valores separados por vírgula - As transações liam a lista pela primary key ou adicionavam um inteiro único com
CONCAT
- Cada lista era armazenada em uma única row, codificada em um campo
- Com essa carga, o Elle checker consegue inferir dependências de fluxo de dados entre transações e encontrar ciclos no grafo para verificar vários níveis de isolamento
Observação de ciclos G-nonadjacent
- Mesmo em condições normais e com concorrência moderada, o Amazon RDS for PostgreSQL 17.4 apresentou ciclos G-nonadjacent a cada poucos minutos
- Em uma execução de teste de 2 minutos, foram alcançados cerca de 150 write TPS e 1600 read-only TPS, com um ciclo de 4 transações
- O ciclo de exemplo é composto por quatro transações:
T1,T2,T3,T4T1adicionou9à row 89, formando a lista[4 9], eT2observou issoT3adicionou11à row 90, criando a lista[11]T4adicionou3à row 90 e, ao ler a lista resultante[11, 3], sobrescreveu a versão deT3T2viu a adição deT1na row 89, mas não viu a adição deT3na row 90- Em contrapartida,
T4viu a adição deT3na row 90, mas não percebeu a adição deT1na row 89
- Esse ciclo inclui uma dependência read-write entre transações não adjacentes, portanto é um ciclo G-nonadjacent que viola Snapshot Isolation
- Em
Repeatable Readdo PostgreSQL padrão, esse comportamento não deveria acontecer, e o Jepsen não o observou no PostgreSQL padrão
Por que isso entra em conflito com Snapshot Isolation
- Em Snapshot Isolation, toda transação deve parecer operar sobre um snapshot do banco no timestamp inicial
s - Os efeitos da transação só devem se tornar visíveis a outras transações no timestamp de commit
c - Ao expressar as observações do ciclo de exemplo como relações entre timestamps, surge uma contradição
- Como
T2leu a adição deT1, o início deT2deve ser posterior ao commit deT1:c1 < s2 - Como
T2não viu a adição deT3, entãos2 < c3 - Como
T4sobrescreveu e observouT3, entãoc3 < s4 - Como
T4não viu a adição deT1, entãos4 < c1
- Como
- Todas essas relações não podem ser verdadeiras ao mesmo tempo, o que entra em conflito com o modelo de timestamps de Snapshot Isolation
Long Fork e resultados por versão
- Esse ciclo também é um exemplo de Long Fork
- A primeira e a segunda transação formam um fork de um estado lógico
- A terceira e a quarta transação formam o segundo fork
- Os dois forks atualizam rows diferentes, mas não observam os efeitos um do outro
- Short Fork, ou seja, Write Skew, não foi observado
- O resultado sugere que o Amazon RDS for PostgreSQL pode estar fornecendo Parallel Snapshot Isolation, um pouco mais fraco que Snapshot Isolation
- As anomalias G-nonadjacent apareceram em vários formatos, inclusive conectadas apenas por arestas write-read e envolvendo mais de 4 transações
- O mesmo tipo de anomalia apareceu em todas as versões testadas, da mais antiga com suporte da AWS, PostgreSQL 13.15, até a mais recente, 17.4
Pontos que os usuários devem revisar
- Como existem Long Fork e outros ciclos G-nonadjacent, clusters Amazon RDS for PostgreSQL Multi-AZ não garantem Snapshot Isolation
- Nesse aspecto, os clusters RDS for PostgreSQL Multi-AZ oferecem semântica de segurança mais fraca do que o PostgreSQL de nó único, que em testes anteriores do Jepsen pareceu fornecer Strong Snapshot Isolation
- Usuários podem verificar se a estrutura de suas transações é vulnerável a Long Fork ou validar experimentalmente se as invariantes pretendidas estão sendo mantidas
- Transações de leitura podem ver resultados diferentes umas das outras quanto à ordem de execução das transações
- Como a anomalia parece estar relacionada a consultas no secondary read-only, pode ser possível recuperar Snapshot Isolation das seguintes formas
-
usar apenas o writer endpoint
- incluir pelo menos 1 escrita em todas as transações críticas para segurança
- A verificação do Jepsen é uma abordagem experimental: ela pode provar a existência de bugs, mas não sua ausência
- Este relatório não é resultado de uma investigação profunda do funcionamento do RDS for PostgreSQL, e sim de uma exploração preliminar
-
1 comentários
Opiniões no Hacker News
Eu gostaria que textos no mundo do software fossem mais frequentemente assim: “Amazon RDS for PostgreSQL é um serviço da Amazon Web Services (AWS) que fornece instâncias gerenciadas de bancos de dados PostgreSQL. Mostramos que clusters multi-AZ do Amazon RDS for PostgreSQL violam o isolamento por snapshot, o modelo de consistência mais forte suportado em todos os endpoints...”
É direto, vai ao ponto e sem enfeites, parecido com a forma de compartilhar resultados de pesquisa em outras áreas STEM. Houve uma época em que eu gostava de posts de blog espirituosos explicando coisas com memes, mas agora sinto falta de textos plain e simples
Quando eu escrevia textos técnicos muito profundos, quase não recebia curtidas nem comentários, a ponto de um Staff Engineer dizer que “seria melhor estreitar mais o público-alvo”. Por outro lado, ao testar o Kubecost inicial, escrevi que as recomendações reduziam pouco o custo e poderiam causar problemas de desempenho em contêineres; embora fosse um texto bem técnico tratando de CPU throttling e cgroups, quando coloquei memes as pessoas adoraram
Mais tarde, fiz uma pequena biblioteca externa para Python em C, acessei com ctypes e escrevi um texto mais seco comparando alocação em stack/heap; coloquei memes também e tive resultado parecido. Não gosto dessa tendência, mas, se a ideia é alcançar um público amplo, não sei bem como evitá-la. Jensen não mirou esse público, e uma escrita rigorosa e pura merece aplausos
Não está no título e também não fica muito claro no texto, mas este problema se limita a um recurso relativamente novo do RDS, os clusters multi-AZ. É diferente das instâncias multi-AZ com as quais muita gente está acostumada
Instâncias multi-AZ são um recurso antigo em que o DB primário é replicado de forma síncrona para um DB secundário em outra zona de disponibilidade e, se o primário falha, o RDS faz failover para o secundário
Clusters multi-AZ têm dois secundários, e a transação é replicada de forma síncrona para pelo menos um deles. Eles são mais robustos do que instâncias multi-AZ quando um secundário falha ou perde desempenho, e também permitem acesso somente leitura aos secundários
Porém, clusters multi-AZ provavelmente têm mais mágica adicional internamente que não é funcionalidade nativa do PostgreSQL, e talvez por isso tenham falhado nos testes do Jepsen
Ainda assim, o PostgreSQL tem uma falha que pode permitir um problema parecido com esse padrão. Uma transação não replicada em que o cliente desaparece durante o commit se torna visível imediatamente. No exemplo, se T1 acontece em um líder isolado e a conexão cai durante o commit, T2 também acontece no nó isolado, e T3/T4 acontecem depois no novo líder, dá para ver o mesmo resultado. Mas isso não combina muito bem com a descrição de que não houve fault injection neste teste
Correção: eu não tinha visto o texto dizendo que esse padrão é explicado pela inconsistência na ordem de commits entre a réplica e o nó primário. Meio constrangedor, já que já apresentei uma forma de corrigir esse problema
Boa investigação. Hoje em dia, muitos desenvolvedores de software nem entendem bem transações em si, e muito menos os diversos modelos de transação. Já vi até desenvolvedor CRUD chamado de “desenvolvedor sênior” que não sabia absolutamente nada sobre transações de banco de dados
Na prática, quando há volume de tráfego e o software resolve problemas que não são triviais, transações e modelos de transação são muito importantes para desempenho e para código sem erros
Por exemplo, em um projeto grande, depois de muita análise, mudamos o padrão Read Committed do SQL Server para Read Committed Snapshot Isolation, e a contenção de locks praticamente desapareceu, deixando os usuários muito satisfeitos. Os engenheiros de software daquele projeto usavam muitas transações, mas, até ensinarmos o básico, não sabiam nada sobre modelos de transação ou locks
Como trabalho principalmente no varejo, vejo com frequência sistemas cheios de erros parecidos com condições de corrida, e é uma pena porque esses níveis de isolamento poderiam ajudar bastante nesses pontos
Dito isso, vi esse tipo de caso principalmente em engenheiros de startup; já os desenvolvedores típicos de Oracle/MSSQL em grandes empresas pelo menos tinham o básico correto, então os avalio bem melhor
Algumas vezes na carreira vi essa abordagem dar resultados realmente ruins
Porém, um cuidado nessa transição é que todo código que depende de leituras bloqueantes quebra. Por exemplo, código como
select with existsprecisa ser reescrito com locks explícitos ou por outros meiosEm uma empresa anterior, quando alteramos o comando
pg_dumpdos scripts de backup para começar a usar workers paralelos (flag-j), raramente víamos, durante a restauração, erros que sugeriam inconsistência, como erros de chave duplicada e erros de restrição de chave estrangeiraNa época tentei reportar isso à AWS e à lista de e-mails do PostgreSQL, mas não consegui reproduzir facilmente, então não houve progresso; no fim, desistimos e voltamos para dumps de thread única. Fico curioso se o fenômeno que vi naquela época tem relação com este problema
Lendo este texto, parece que o impacto real é que dados obsoletos podem ser retornados quando uma leitura acontece rapidamente logo após uma escrita na mesma linha. A transação de escrita é marcada como concluída, mas antes que toda a camada distribuída da instância RDS multi-AZ esteja totalmente atualizada; se a mesma linha for lida imediatamente, a linha pode ainda não existir ou a coluna pode não estar totalmente atualizada, retornando o valor anterior
Pelo modelo de snapshots do PostgreSQL, não parece significar que apenas alguns bytes de uma coluna multibyte sejam atualizados e se leia um valor sem sentido
No fim, parece uma condição de corrida que converge com o tempo. Ou será que alguém leu isso como significando que transações posteriores do “long fork” podem nunca ser concluídas, mesmo em condições normais?
A frase “este trabalho foi realizado de forma independente pela Jepsen, sem remuneração” não é algo que um stakeholder de RDBMS queira ver nem em um dia bom. Imagino que alguns e-mails preocupados tenham circulado internamente. Como sempre, respeito ao aphyr
Não está totalmente claro se isso não é um problema em clusters PostgreSQL upstream com múltiplas instâncias. Fico curioso se é correto entender que a AWS faz algo na configuração do cluster, ou adicionou um patch que provoca esse comportamento
Em geral, há várias formas de replicação no PostgreSQL, com resultados diferentes. Por exemplo, há o relatório de Bin Wang sobre Patroni: https://www.binwang.me/2024-12-02-PostgreSQL-High-Availabili...
O que também foi observado aqui é que o PostgreSQL atualmente não fornece um comportamento de snapshot consistente entre o nó primário e as réplicas. Provavelmente a transação somente leitura T2 foi executada em um nó secundário, enquanto as transações de alteração T1/T3/T4 foram executadas no nó primário
Como contexto, o snapshot de um nó PostgreSQL secundário, ao decidir quais transações são visíveis, depende da ordem de persistência das transações, ou seja, da posição dos registros de commit no WAL. Já no nó primário, a ordem de visibilidade é determinada pelo momento em que o backend que aprovou aquela transação recebe pela primeira vez a notificação de que a transação foi totalmente commitada e, depois disso, pelo momento em que marca o commit
Dentro do nó primário e dentro do nó secundário, a ordem de commit entre os backends conectados é consistente, mas a ordem de commit entre primário e secundário pode diferir um pouco. Há trabalho em andamento para melhorar isso, mas ainda está em uma fase muito ativa
A AWS parece ter aplicado patches ao PostgreSQL para replicar em duas instâncias e considerar suficiente quando uma delas confirma a alteração. Quando exatamente essa confirmação ocorre não é informação pública
Pessoalmente, acho que para PostgreSQL é melhor uma replicação em nível de sistema de arquivos, como drbd. As instâncias AWS Multi-AZ do estilo antigo provavelmente usavam esse método. Porém, isso reduz a vazão e não permite leitura na instância secundária
Em especial este ponto: https://youtu.be/fLqJXTOhUg4?t=434
O título enviado pergunta pelo ponto central. RDS for PostgreSQL 17.4 não implementa corretamente o isolamento por snapshot
As discussões podem ficar bem intensas: se o título é duro demais com o sistema, favorável demais, se captura o mais significativo entre a dúzia de problemas encontrados, se é justo segundo o critério da Jepsen de tentar ser uma intermediária honesta dos resultados de segurança de bancos de dados, e como será interpretado daqui a 10 anos, quando as pessoas continuarem linkando, mas ele já não se aplicar mais às versões recentes
Depois de algumas tentativas frustrantes, evitamos esse problema adotando a política de colocar todos os títulos de relatório no formato “Jepsen: ”. Se o HN quiser um texto de link mais explicativo ou mais colorido, claro, pode escolher por conta própria
Ainda assim, isto foi escrito por Kyle Kingsbury, o Chuck Norris das garantias transacionais, então a AWS precisa responder ou esclarecer. Mesmo que pareça se aplicar apenas a uma das duas opções do RDS para PostgreSQL, o cluster multi-AZ. Implantações Multi-AZ podem ter uma ou duas instâncias de banco de dados em standby; aqui se trata da configuração com duas instâncias de banco de dados em standby
A documentação da AWS não faz essa promessa. O manual de 5.494 páginas do RDS também quase só menciona isolation ou serializable na documentação de parâmetros de cada engine
Também não há nada sobre consistência global de leitura em clusters Multi-AZ. Dizem que, por ser replicação semissíncrona, o writer espera a confirmação dos registros de log de um standby, mas dois readers podem estar em snapshots diferentes
[1] - "New Amazon RDS for MySQL & PostgreSQL Multi-AZ Deployment Option: Improved Write Performance & Faster Failover" - https://aws.amazon.com/blogs/aws/amazon-rds-multi-az-db-clus...
[2] - "Amazon RDS Multi-AZ with two readable standbys: Under the hood" - https://aws.amazon.com/blogs/database/amazon-rds-multi-az-wi...
O desenvolvedor assumiu isolamento por snapshot, mas se o Amazon RDS for PostgreSQL na prática oferece apenas isolamento por snapshot paralelo, fico curioso sobre quais bugs de segurança ou bugs em nível de aplicação podem surgir, especialmente em configurações multi-AZ que usam endpoints de réplica de leitura
git push. Você inicia uma transação, lê o estado atual, verifica se ele corresponde ao estado esperado, grava o novo estado e faz o commit junto com o hash do novo estado. Em uma situação azarada, pode surgir um hash de commit que não corresponde a nenhum estado válidoO próprio fato de ser difícil raciocinar sobre isso torna difícil evitar o problema. Por isso, a solução mais simples, para escritas condicionadas a leituras, provavelmente seria algo como “talvez seja possível recuperar o isolamento por snapshot usando apenas o endpoint do writer”
Dito isso, é surpreendente que a abordagem de “usar apenas o endpoint do writer” não tenha sido testada, especialmente em situações de perda de disponibilidade
O User1 comenta, depois o User2 comenta, e então o User1, em uma transação separada, verifica que há apenas 1 comentário e recebe o badge. O User2 também faz a mesma verificação em uma transação separada e, vendo apenas seu próprio 1 comentário, pode receber o badge
Com isolamento por snapshot, isso é impossível. Pelo menos uma das transações separadas teria que ver 2 comentários
O artigo original sobre snapshots paralelos também vale a leitura: https://scispace.com/pdf/transactional-storage-for-geo-repli...
Ao ver a frase “esse fenômeno ocorreu em todas as versões testadas, da 13.15 à 17.4”, fiquei preocupado se atualizar a versão major teria sido uma escolha errada, mas parece que não é o caso. Isso parece mais uma solicitação de recurso ou um bug antigo do que uma regressão