Avaliação do MySQL 8.0.34 pelo Jepsen
(jepsen.io)- O nível de isolamento padrão do MySQL 8.0.34, Repeatable Read, mostra violações de consistência transacional que não correspondem às expectativas do ANSI SQL nem do PL-2.99 de Adya, mesmo em um único nó saudável
- A combinação do checker list-append do Elle, workload direcionado e LazyFS foi usada para validar em conjunto o MySQL 8.0.34, MariaDB 10.11.3, clusters com replicação por binlog e o AWS RDS MySQL Multi-AZ DB Cluster
- Assim como nos resultados do Hermitage de 2014 de Kleppmann, foram reproduzidos G2-item, G-single e lost update, além de terem sido observadas violações de consistência interna, non-repeatable read e Monotonic Atomic View
- Em um MySQL único, Read Uncommitted, Read Committed e Serializable pareceram estar de acordo com PL-1, PL-2 e PL-3, respectivamente, mas o cluster AWS RDS MySQL mostrou G2-item e G-single até em Serializable
- Se for necessário Repeatable Read no nível ANSI ou PL-2.99, é difícil confiar apenas no Repeatable Read do MySQL; são necessários Serializable ou bloqueios explícitos como
SELECT... FOR UPDATE
Alvo da avaliação e escopo
- MySQL é um banco de dados relacional amplamente usado, e nesta análise “MySQL” se refere ao MySQL usando o mecanismo de armazenamento padrão, InnoDB
- O foco é o MySQL em servidor único, mas também são abordados clusters com primary único de escrita e secondaries somente leitura usando replicação por binlog
- Os alvos de teste foram os seguintes
- MySQL 8.0.34
- MariaDB 10.11.3
- Debian Bookworm
- Perfil “Multi-AZ DB Cluster” do RDS Cluster da AWS
- O trabalho foi realizado de forma independente, sem remuneração, e seguiu a política de ética do Jepsen
Níveis de isolamento SQL e o critério para Repeatable Read
- O ANSI SQL define Read Uncommitted, Read Committed, Repeatable Read e Serializable pela possibilidade de P1 dirty read, P2 non-repeatable read e P3 phantom
- Em 1995, Berenson e outros criticaram a ambiguidade e a incompletude da definição ANSI em A Critique of ANSI SQL Isolation Levels
- P1, P2 e P3 admitem interpretações diferentes
- Fenômenos importantes como P0 dirty write ficaram de fora
- P3 proíbe apenas inserts que afetam predicados, sem tratar de updates ou deletes
- O artigo de 1999 de Atul Adya define níveis de isolamento independentes de implementação com base em grafos de dependência entre transações
- PL-1 proíbe o ciclo de escrita G0
- PL-2 proíbe G0 e G1
- PL-2.99 proíbe G0, G1 e G2-item, correspondendo a Repeatable Read
- PL-3 proíbe G0, G1 e G2, correspondendo a Serializable
- O Jepsen normalmente usa o formalismo de Adya para determinar históricos transacionais e anomalias
Conflito entre a documentação do MySQL e o Repeatable Read
- A documentação do MySQL explica que o InnoDB oferece todos os quatro níveis de isolamento do padrão SQL:1992
- O nível padrão, Repeatable Read, é descrito como lendo, em consistent reads dentro da mesma transação, o snapshot definido na primeira leitura
- A documentação de consistent read também diz que o banco é visto conforme o timepoint da primeira leitura
- Porém, um comentário no mesmo documento diz que o snapshot se aplica a
SELECT, mas não necessariamente a instruções DML, e queDELETEouUPDATEpodem atingir rows commitadas por outras transações - Esse comentário entra em conflito com o fato de que o ANSI SQL e o manual de referência do MySQL tratam
SELECTtambém como DML, e gera confusão ao sugerir que, em Repeatable Read, uma escrita pode afetar rows que a leitura não podia ver
Projeto dos testes
- A suíte de testes para MySQL foi escrita com base na biblioteca de testes Jepsen 0.3.4
- Os clientes usam o adaptador JDBC
mysql-connector-j - Os testes incluem fault injection como pause de processo, crash, partição de rede e perda de escritas em disco não sincronizadas com fsync
- Ainda assim, quase todas as descobertas desta análise ocorreram em um único nó MySQL saudável
-
Workload list-append do Elle
- O workload principal usa o checker list-append do Elle
- O Elle infere dependências write-write, write-read e read-write entre transações e prova violações de níveis específicos de isolamento por meio de ciclos no grafo de dependência
- O workload list-append executa transações aleatórias compostas por reads e appends sobre várias listas identificadas por primary key
- As listas são codificadas em um campo
textcom valores separados por vírgula, e o append é feito com SQLCONCAT - Melhorias recentes fizeram o Elle detectar melhor o seguinte
- inferência de dependências ww/rw para elementos appended que não foram lidos
- detecção explícita de P4 lost update
- busca de ciclos complexos com real-time edge e process edge
-
Workload direcionado
- O workload de non-repeatable read usa uma row da tabela
people - Uma família de transações faz update apenas em
name, e outra lêname, faz update emgendere depois lênamenovamente - Se
namemudar entre as duas leituras, há violação de Repeatable Read - O workload de Monotonic Atomic View usa o
valuede duas rows - O writer incrementa o
valueda row 0 e depois incrementa a row 1 - O reader lê a row 0, faz update do
noopda row 1 e depois lê a row 1 e a row 0 - Se viu parte dos efeitos de uma transação, deve ver todos os seus efeitos
- O workload de non-repeatable read usa uma row da tabela
-
LazyFS
- LazyFS é um sistema de arquivos FUSE que simula perda de escritas não sincronizadas com fsync
- O teste é feito matando o processo do MySQL, descartando o cache do LazyFS e reiniciando o MySQL
- Este relatório é o primeiro relatório público do Jepsen a incluir o LazyFS
Anomalias encontradas no MySQL Repeatable Read
-
G2-item
- O Repeatable Read PL-2.99 de Adya proíbe G2-item, um ciclo de dependências write-write, write-read e read-write sem predicados
- O Repeatable Read do MySQL permite G2-item repetidamente mesmo em um único nó saudável
- O comportamento relatado por Kleppmann no Hermitage em 2014 continua ocorrendo no MySQL 8.0.34
- Um teste de exemplo mostrou 214 ciclos em 40 segundos
- Esse comportamento é proibido em Repeatable Read PL-2.99, mas na definição ANSI ainda há margem de interpretação porque a definição de P2 trata apenas do caso de ler a mesma row duas vezes
-
G-single e read skew
- O Repeatable Read do MySQL também apresenta G-single
- G-single é um ciclo composto por arestas write-write, write-read e read-write, mas em que as arestas read-write não são adjacentes
- O read skew relatado por Kleppmann em 2014 também foi confirmado no MySQL 8.0.34
- Em um teste de append de 60 segundos, com cerca de 140 transações por segundo, apareceram 244 casos de G-single e 305 de G2-item
- Como o teste de append não usa operações de predicado, todos são classificados como violações de Repeatable Read
-
Lost update
- P4 lost update é um caso especial de G-single em que duas transações leem a mesma versão da mesma key e ambas fazem update
- Snapshot Isolation e Repeatable Read PL-2.99 proíbem lost update
- O Repeatable Read do MySQL permite lost update repetidamente mesmo em um único nó saudável
- Em um teste, entre 9.048 transações bem-sucedidas, o novo checker encontrou 446 transações envolvidas em 198 casos de lost update
- Desses, apenas 47 apareceram como ciclos
- O padrão de ler um valor e depois escrever nele não é seguro em Repeatable Read do MySQL
- No padrão comum de ORM em que se lê um objeto, modifica-se na memória e depois salva novamente, mudanças já commitadas podem desaparecer silenciosamente
- O usuário precisa usar bloqueios explícitos manualmente
-
Non-repeatable read e violações de consistência interna
- O Repeatable Read do MySQL mostra violações de consistência interna mesmo em um único nó saudável
- Na mesma execução de teste, 126 das 9.048 transações commitadas mostraram erros de consistência interna
- Em um exemplo, uma transação leu uma key como
nil, fez append de um valor e, ao ler a mesma key novamente, observou mais três valores adicionados - Em outro exemplo, uma transação leu a key 1096 como
[1 2 3], fez append de7e, ao reler, observou[1 2 3 4 5 6 7] - No workload direcionado, dentro de uma transação Repeatable Read,
namefoi lido como"pebble", depoisgenderfoi atualizado para"femme", e ao ler o mesmonamenovamente o valor retornado foi"moss" - Esse comportamento contradiz a definição ANSI de non-repeatable read e a descrição da documentação do MySQL de “snapshot definido na primeira leitura”
-
Violação de Monotonic Atomic View
- Monotonic Atomic View é a propriedade segundo a qual uma transação que viu algum efeito de outra deve ver todos os seus efeitos
- O Repeatable Read do MySQL viola isso repetidamente mesmo em um único nó saudável
- No workload, o writer incrementa a row 0 e depois a row 1
- O reader vê o valor anterior
0na row 0, depois vê o incremento1do writer na row 1 e, ao ler a row 0 novamente, ainda vê0 - Isso é uma leitura não monotônica: o efeito da row 1 foi visto, mas o da row 0 não, o que não condiz com o comportamento normal de snapshot
Anomalias no AWS RDS MySQL Serializable
- O cluster AWS RDS MySQL viola repetidamente a Serializability mesmo no nível de isolamento “Serializable”
- No perfil de produção recomendado por padrão do RDS MySQL cluster, o teste de append mostrou anomalias G2-item e G-single
- As anomalias observadas tinham a forma de uma transação deixar de perceber uma dependência anterior de outra transação, embora tenha visto os efeitos de uma transação intermediária
- Essa anomalia é classificada tanto como G-single quanto como G2-item, violando Snapshot Isolation, Repeatable Read e Serializability
- Uma suspeita remanescente é a configuração relacionada a
replica_preserve_commit_order- No MySQL 8.0.27 ou superior,
replica_preserve_commit_order=ONé o valor padrão - Os parâmetros padrão do RDS ainda escolhem uma configuração equivalente a
replica_preserve_commit_order=OFF - No parameter group do RDS, é usado o nome antigo dessa configuração,
slave_preserve_commit_order - Ao aplicar essa configuração a um cluster local de testes, observaram-se G-single e G2-item semelhantes
- No MySQL 8.0.27 ou superior,
O que pareceu funcionar e resultados com LazyFS
- Read Uncommitted, Read Committed e Serializable do MySQL 8.0.34 pareceram satisfazer PL-1, PL-2 e PL-3, respectivamente
- Esse resultado foi observado tanto em nó único quanto em pequenos clusters com réplicas somente leitura usando replicação por binlog
- O mesmo resultado se manteve com pause de processo, crash e partição de rede
- A fault injection com LazyFS não encontrou problemas na configuração padrão do MySQL
- Com o valor padrão
innodb_flush_log_at_trx_commit=1, não apareceu perda de transações commitadas mesmo após crash de processo e perda de dados não sincronizados com fsync - Ao mudar para
innodb_flush_log_at_trx_commit=0, o MySQL passou a fazer fsync apenas uma vez a cada poucos segundos, e houve perda de dados
A natureza real do MySQL Repeatable Read
- O Repeatable Read do MySQL não satisfaz o Repeatable Read PL-2.99
- Mostra G2-item e write skew
- Também não satisfaz Snapshot Isolation
- Mostra G-single, read skew e lost update
- Também não satisfaz cursor stability
- Ocorre lost update
- Read Atomic, Causal Consistency, Consistent View, Prefix Consistency e Parallel Snapshot Isolation também são descartados
- Foram observadas violações de consistência interna
- O Repeatable Read do MySQL parece ser um pouco mais forte que Read Committed
- Não foram observados G0 dirty write, G1a aborted read, G1b intermediate read nem G1c cyclic information flow
- A repetibilidade de algumas leituras oferece propriedades mais fortes do que Read Committed
- Ainda assim, não está claro exatamente qual consistency model o MySQL Repeatable Read oferece, e não há uma definição formal de suas propriedades
Desalinhamento entre documentação e entendimento da comunidade
- Na comunidade MySQL, o comportamento do Repeatable Read ainda não é plenamente compreendido
- Vários textos acreditam que o Repeatable Read do MySQL evita lost update, enquanto outros relatam que não evita e recomendam bloqueios explícitos
- Diversos materiais na internet dizem que o Repeatable Read do MySQL é de fato repeatable, mas os testes do Jepsen mostram casos em que não é
- A documentação do MySQL e do MariaDB também explica que o Repeatable Read lê o mesmo snapshot dentro da mesma transação
- Uma frase na documentação de consistent read do MySQL sugere um comportamento que entra em conflito com essa explicação, mas esse ponto fica escondido no texto
Recomendações
- Se o MySQL mantiver o comportamento atual, deveria documentar com clareza qual consistency model “Repeatable Read” realmente fornece
- A outra opção é tratar o comportamento atual como bug e corrigi-lo
- O Jepsen afirma que receberia com satisfação a promessa do MySQL e de outros fornecedores de oferecer Repeatable Read PL-2.99
- Usuários que precisam de PL-2.99 ou de ANSI Repeatable Read devem tomar cuidado com o Repeatable Read do MySQL
- Alternativas práticas incluem
- usar o nível de isolamento Serializable do MySQL
- fortalecer leituras em
READ COMMITTEDcom técnicas de bloqueio comoSELECT ... FOR UPDATE
Recomendações para usuários de RDS
- O cluster AWS RDS MySQL mostra read skew e G2-item em “Serializable”
- Usuários que dependem de Serializability devem definir
slave_preserve_commit_ordercomoONno parameter group do RDS - Foi sugerido que a AWS mude o valor padrão ou explique claramente as violações permitidas de Serializability na documentação de known limitations do RDS MySQL
Trabalhos futuros e pedido de padronização
- A replicação por binlog do MySQL pareceu frágil
- Nos testes locais do Jepsen, foram observadas várias situações em que a replicação parava
- A replicação do AWS RDS MySQL podia quebrar completamente com apenas alguns minutos de teste, e houve um caso em que um
CREATE DATABASEbem-sucedido no primary não apareceu no secondary nem após 1 hora de recuperação
- Não foram exploradas promoção de secondary para primary nem topologias de replicação como ring ou star
- Está em andamento uma pesquisa de testes de predicado mais gerais para avaliar segurança de predicados
- A definição dos níveis de isolamento do ANSI SQL não mudou, embora Berenson e outros tenham apontado sua ambiguidade e incompletude há 28 anos e tenham ocorrido 7 revisões ANSI·ISO desde então
- É necessária uma definição mais formal e portável dos níveis de isolamento em ISO/IEC 9075-2, para tratar com clareza fenômenos como anomalias internas, lost update e dirty write
1 comentários
Opiniões do Hacker News
Há muito tempo vejo repeatable read como uma má ideia, mesmo que a implementação seja perfeita
Mesmo que funcione corretamente dentro do banco de dados, em consultas complexas é difícil demais raciocinar sobre isso
Para mim, os únicos níveis de isolamento que fazem sentido são read committed e serializable
Ou se vai até o fim com serializable, para não haver surpresas, ou se usa read committed, em que fica claro que, se você precisa de uma visão consistente dentro de uma transação, deve bloquear as linhas antes de ler
read committed é mais próximo de código multithread comum e de gerenciamento de memória, então é mais fácil para engenheiros terem uma intuição; serializable é tão rigoroso que é difícil criar erros inesperados
O meio-termo é terra de ninguém, e algo menos consistente que read committed já não dá para considerar um banco de dados de verdade
À medida que a aplicação cresce, fica muito difícil entender todos os casos de onde locks são adquiridos e onde os dados são acessados
Para transações de leitura/escrita, serializable é o único modelo de isolamento sensato; para transações somente leitura, snapshot isolation, lidando com um snapshot do banco de dados em um ponto no tempo, é um bom modelo
Os modos oferecidos pelo Spanner, na prática, também são apenas esses dois: https://cloud.google.com/spanner/docs/transactions
Há uma palestra no FOSSDEM 2024 comparando níveis de isolamento e MVCC em bancos de dados SQL
Ela cobre Oracle, MySQL, SQL Server, PostgreSQL e YugabyteDB
https://fosdem.org/2024/schedule/event/fosdem-2024-3600-isol...
Fico curioso sobre como append(a) é mapeado para uma operação SQL real em uma dada tabela
É usar um campo TEXT como se fosse uma lista?
No modo repeatable read do MySQL, também já vi um único SELECT selecionando uma única linha retornar um resultado impossível
Era no formato
SELECT min(value), max(value) FROM table WHERE id = 1;, eidera a chave primária, masminemaxsaíram com valores diferentesSó para constar, isso não é um problema específico de CONCAT. O motivo de usar CONCAT é que ele permite raciocinar sobre as anomalias em tempo linear, não em tempo exponencial
O mesmo tipo de comportamento também aparece em registradores comuns de leitura/escrita
Gostei do texto e de ele tratar de AWS RDS, mas fico curioso se também houve foco no AWS Aurora MySQL
Para quem não sabe, a AWS criou uma plataforma de banco de dados compatível em protocolo que finge ser MySQL ou PostgreSQL
Seria interessante ver se o Aurora MySQL tem as mesmas “características” que RDS ou MariaDB
Ainda assim, é um alvo muito interessante, e, como Aurora é um banco de dados bem mais novo, tenho a intuição de que pode haver problemas sutis ainda não descobertos, mais do que no MySQL antigo
Porém há uma grande irritação
Os engenheiros da Plaid escreveram um bom texto resumindo as diferenças: https://plaid.com/blog/exploring-performance-differences-bet...
Para mim, a maior diferença é que um cluster Aurora usa armazenamento compartilhado, então o modelo de isolamento é um pouco diferente
read committed só é possível definindo um parâmetro para o cluster inteiro, e read uncommitted, pelo que vejo, não é possível
Texto muito interessante
Mostra bem quantos “sistemas que funcionam de verdade” podem ser construídos sobre uma base que exibe tantas anomalias de consistência
A parte em que, ao mexer por apenas 5 minutos, a replicação do RDS parou e não houve nenhum alerta de health check com falha é um pouco preocupante
Só que ela tende a jogar para o usuário o ônus de vasculhar mais de 150 métricas e ler a documentação para descobrir o que importa
Além disso, em <https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_...> eles dizem que há uma célula de tabela no console mostrando o status da replicação, mas muitas vezes no console o usuário precisa ativar manualmente a exibição dessa coluna, o que não é bom
Eles se apoiam bastante no “modelo de responsabilidade compartilhada” de que a AWS fala
É preciso fazer tudo por conta própria dentro do host ou do contêiner
O suporte da AWS/Rackspace apenas diz: “o que roda dentro de um serviço da AWS não é gerenciado por nós, então é problema do cliente”
Gostei da parte em que, em 2022, a Jepsen encomendou o desenvolvimento do LazyFS ao INESC TEC da Universidade do Porto
Um sistema de arquivos FUSE para simular perda de escritas que não passaram por
fsync: é um ótimo exemplo de empurrar o nível técnico para a frenteSELECT ... FOR UPDATEparece ser a resposta para esses problemasSe você bloqueia as linhas que vai atualizar, de repente tudo não passa a funcionar como anunciado?
Se você quiser atualizar um registro com base nos dados de outro registro, precisa fazer uma leitura com bloqueio nesse outro registro e, provavelmente, também no registro que será atualizado
Se você atualizar um registro com base em outro registro em uma única consulta SQL, o MySQL vai bloquear os dois de qualquer forma
Se precisar atualizar algo com base em vários alvos, pela minha experiência, deadlocks surgem com muita facilidade
Em vez disso, é melhor bloquear algo como um registro de bloqueio e então fazer repeatable read nos dados desejados e atualizar
O ponto no tempo do repeatable read não é definido até que uma leitura consistente seja realizada
SELECT ... FOR UPDATEnão é uma leitura consistente; portanto, funciona bem em cenários de concorrência sem bloquear dezenas ou centenas de linhas com um update SQL comumPela minha experiência, a maioria dos desenvolvedores nem considera níveis de isolamento para começo de conversa e usa o padrão
Quando aparece uma condição de corrida, pensam “ué, que estranho” e seguem em frente
[1] https://news.ycombinator.com/item?id=38696421
Então acho que, para a maioria dos desenvolvedores, é melhor não precisar pensar diretamente em níveis de isolamento, e MySQL e alguns outros bancos de dados oferecem garantias insuficientes para o desenvolvedor médio