3 pontos por GN⁺ 2023-12-20 | 1 comentários | Compartilhar no WhatsApp
  • 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 que DELETE ou UPDATE podem 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 SELECT també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 text com valores separados por vírgula, e o append é feito com SQL CONCAT
    • 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 em gender e depois lê name novamente
    • Se name mudar entre as duas leituras, há violação de Repeatable Read
    • O workload de Monotonic Atomic View usa o value de duas rows
    • O writer incrementa o value da row 0 e depois incrementa a row 1
    • O reader lê a row 0, faz update do noop da 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
  • 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 de 7 e, ao reler, observou [1 2 3 4 5 6 7]
    • No workload direcionado, dentro de uma transação Repeatable Read, name foi lido como "pebble", depois gender foi atualizado para "femme", e ao ler o mesmo name novamente 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 0 na row 0, depois vê o incremento 1 do 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

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 COMMITTED com técnicas de bloqueio como SELECT ... 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_order como ON no 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 DATABASE bem-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

 
GN⁺ 2023-12-20
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

    • Não acho que as pessoas raciocinem bem sobre read committed
      À 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
    • read uncommitted serve para estatísticas agregadas, mas, nesse nível, é melhor mandar os dados para o ClickHouse
    • Consultas de snapshot somente leitura são muito úteis em sistemas reais
    • Se repeatable read realmente funcionasse corretamente, não haveria necessidade de bloquear linhas
  • 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...

    • O palestrante é um developer advocate que trabalha na YugabyteDB, e fico curioso sobre como isso se conecta ao trabalho do Kyle
  • 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;, e id era a chave primária, mas min e max saíram com valores diferentes

  • 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

    • Aurora é um mecanismo de banco de dados completamente diferente, então os problemas de concorrência também são diferentes; por isso, provavelmente não foi abordado aqui
      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
    • Uso bastante MySQL Aurora e, para o nosso caso, o volume de uso é muito alto, mas os padrões de consulta são simples, então não vemos muita diferença
      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 maioria dos sistemas está efetivamente quebrada e segue funcionando contornando isso com correções humanas
  • 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

    • Os detalhes são importantes e é quase impossível diagnosticar só pelo screencast, mas, pela minha experiência, a AWS em geral oferece CloudWatch Metrics com bastante generosidade
      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
    • Posso garantir que nenhum health check da AWS deve ser confiado como alerta primário para uma situação de indisponibilidade
      É 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 frente

  • SELECT ... FOR UPDATE parece ser a resposta para esses problemas
    Se você bloqueia as linhas que vai atualizar, de repente tudo não passa a funcionar como anunciado?

    • Em geral, operações que bloqueiam linhas tendem a “fixar” a existência dos valores, independentemente de repeatable read
      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 UPDATE não é uma leitura consistente; portanto, funciona bem em cenários de concorrência sem bloquear dezenas ou centenas de linhas com um update SQL comum
    • Sim, se você não se importar que o desempenho seja completamente destruído
  • Pela 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

    • Eu gostaria de discordar, mas os primeiros anos de sucesso do MongoDB comprovam bem essa afirmação
    • É por isso que eu disse que o nível de isolamento padrão deveria ser serializable
      [1] https://news.ycombinator.com/item?id=38696421
    • Problemas de isolamento são difíceis demais de raciocinar; por isso, quase tudo abaixo de consistência serializable acaba criando armadilhas de várias formas
      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
    • Pela minha experiência, quase nenhum desenvolvedor sequer considera a consistência em si