- Repeatable Read, le niveau d’isolation par défaut de MySQL 8.0.34, présente des violations de cohérence transactionnelle incompatibles avec les attentes d’ANSI SQL et du PL-2.99 d’Adya, même sur un seul nœud sain
- L’évaluation combine le vérificateur list-append d’Elle, des workloads ciblés et LazyFS pour tester MySQL 8.0.34, MariaDB 10.11.3, des clusters à réplication binlog et un cluster AWS RDS MySQL Multi-AZ DB
- Comme dans les résultats Hermitage de Kleppmann en 2014, G2-item, G-single et des lost updates ont été reproduits ; des violations de cohérence interne, des non-repeatable reads et des violations de Monotonic Atomic View ont aussi été observés
- En MySQL mononœud, Read Uncommitted, Read Committed et Serializable semblaient respectivement conformes à PL-1, PL-2 et PL-3, mais le cluster AWS RDS MySQL présentait G2-item et G-single même en Serializable
- Si vous avez besoin d’un Repeatable Read de niveau ANSI ou PL-2.99, il est difficile de se fier au seul Repeatable Read de MySQL : il faut recourir à Serializable ou à des verrous explicites comme
SELECT... FOR UPDATE
Périmètre et sujets de l’évaluation
- MySQL est une base de données relationnelle très utilisée ; dans cette analyse, « MySQL » désigne MySQL avec son moteur de stockage par défaut, InnoDB
- L’accent porte sur MySQL en serveur unique, mais l’analyse couvre aussi des clusters avec un primary unique en écriture et des secondaries en lecture seule utilisant la réplication binlog
- Les cibles de test sont les suivantes
- MySQL 8.0.34
- MariaDB 10.11.3
- Debian Bookworm
- Le profil « Multi-AZ DB Cluster » d’AWS RDS Cluster
- Le travail a été réalisé indépendamment, sans rémunération, conformément à la politique d’éthique de Jepsen
Niveaux d’isolation SQL et critères de Repeatable Read
- ANSI SQL définit Read Uncommitted, Read Committed, Repeatable Read et Serializable selon la possibilité des phénomènes P1 dirty read, P2 non-repeatable read et P3 phantom
- En 1995, Berenson et al. ont critiqué l’ambiguïté et le caractère incomplet de la définition ANSI dans A Critique of ANSI SQL Isolation Levels
- P1, P2 et P3 laissent place à l’interprétation
- Des phénomènes importants comme P0 dirty write sont absents
- P3 interdit seulement les insertions qui affectent un prédicat, mais ne traite pas les updates ni les deletes
- La thèse de 1999 d’Atul Adya définit des niveaux d’isolation indépendants de l’implémentation à partir d’un graphe de dépendances entre transactions
- PL-1 interdit les cycles d’écriture G0
- PL-2 interdit G0 et G1
- PL-2.99 interdit G0, G1 et G2-item, et correspond à Repeatable Read
- PL-3 interdit G0, G1 et G2, et correspond à Serializable
- Jepsen utilise généralement le formalisme d’Adya pour analyser les historiques de transactions et les anomalies
Conflit entre la documentation MySQL et Repeatable Read
- La documentation MySQL explique qu’InnoDB fournit les quatre niveaux d’isolation de la norme SQL:1992
- Repeatable Read, le niveau d’isolation par défaut, est décrit comme faisant lire aux consistent reads, au sein d’une même transaction, le snapshot établi lors de la première lecture
- La documentation sur les consistent reads explique également que la base de données est vue selon le timepoint de la première lecture
- Mais une note de cette même documentation indique que le snapshot s’applique aux
SELECT, sans nécessairement s’appliquer aux instructions DML, et que des lignes commit par d’autres transactions peuvent être touchées par unDELETEou unUPDATE - Cette note entre en conflit avec le fait qu’ANSI SQL et le manuel de référence MySQL considèrent aussi
SELECTcomme du DML, et crée une confusion : en Repeatable Read, des écritures peuvent affecter des lignes qui n’étaient pas visibles en lecture
Conception des tests
- La suite de tests pour MySQL est écrite sur la base de la bibliothèque de test Jepsen 0.3.4
- Le client utilise l’adaptateur JDBC
mysql-connector-j - Les tests incluent de l’injection de fautes : process pause, crash, network partition et perte d’écritures disque non fsyncées
- Cependant, presque toutes les observations de cette analyse apparaissent sur un seul nœud MySQL sain
-
Workload list-append d’Elle
- Le workload principal utilise le vérificateur list-append d’Elle
- Elle infère les dépendances write-write, write-read et read-write entre transactions, et prouve des violations de niveaux d’isolation spécifiques via les cycles du graphe de dépendances
- Le workload list-append exécute des transactions aléatoires composées de reads et d’appends sur plusieurs listes identifiées par clé primaire
- Les listes sont encodées dans un champ
textcontenant des valeurs séparées par des virgules, et l’append est traité avec leCONCATSQL - Des améliorations récentes permettent à Elle de mieux détecter :
- l’inférence de dépendances ww/rw sur des éléments append non lus
- la détection explicite de P4 lost update
- la recherche de cycles complexes incluant real-time edges et process edges
-
Workloads ciblés
- Le workload non-repeatable read cible une ligne de la table
people - Une famille de transactions ne met à jour que
name, tandis qu’une autre litname, met à jourgender, puis relitname - Si
namechange entre les deux lectures, c’est une violation de Repeatable Read - Le workload Monotonic Atomic View utilise la
valuede deux lignes - Le writer incrémente la
valuede la ligne 0, puis celle de la ligne 1 - Le reader lit la ligne 0, met à jour le
noopde la ligne 1, puis lit la ligne 1 et la ligne 0 - Si une transaction voit une partie des effets d’une transaction, elle doit voir tous ses effets
- Le workload non-repeatable read cible une ligne de la table
-
LazyFS
- LazyFS est un système de fichiers FUSE qui simule la perte d’écritures non fsyncées
- Le test consiste à tuer le process MySQL, vider le cache LazyFS, puis redémarrer MySQL
- Ce rapport est le premier rapport Jepsen public incluant LazyFS
Anomalies découvertes dans MySQL Repeatable Read
-
G2-item
- Le Repeatable Read PL-2.99 d’Adya interdit G2-item, un cycle de dépendances write-write, write-read et read-write n’impliquant pas de prédicat
- MySQL Repeatable Read autorise G2-item de façon répétée, même sur un seul nœud sain
- Le comportement signalé par Kleppmann en 2014 dans Hermitage se produit toujours avec MySQL 8.0.34
- Un test d’exemple a montré 214 cycles en 40 secondes
- Ce comportement est interdit en Repeatable Read PL-2.99, mais la définition ANSI de P2 ne couvrant que le cas où la même ligne est lue deux fois, il reste une marge d’interprétation dans la définition ANSI
-
G-single et read skew
- MySQL Repeatable Read présente aussi G-single
- G-single est un cycle composé d’arêtes write-write, write-read et read-write, mais dans lequel les arêtes read-write ne sont pas adjacentes
- Le read skew rapporté par Kleppmann en 2014 est confirmé dans MySQL 8.0.34
- Dans un test append de 60 secondes, à environ 140 transactions par seconde, 244 G-single et 305 G2-item sont apparus
- Le test append n’utilise pas d’opérations sur prédicats ; tous ces cas sont donc classés comme violations de Repeatable Read
-
Lost update
- P4 lost update est un cas particulier de G-single où deux transactions lisent la même version de la même clé et la mettent toutes deux à jour
- Snapshot Isolation et Repeatable Read PL-2.99 interdisent les lost updates
- MySQL Repeatable Read autorise de façon répétée les lost updates, même sur un seul nœud sain
- Dans un test, sur 9 048 transactions réussies, le nouveau checker a trouvé 446 transactions impliquées dans 198 cas de lost update
- Parmi celles-ci, seulement 47 cas apparaissaient sous forme de cycles
- Le schéma consistant à lire une valeur puis à l’écrire n’est pas sûr avec MySQL Repeatable Read
- Dans le pattern ORM standard consistant à lire un objet, le modifier en mémoire puis le réenregistrer, des modifications commit peuvent disparaître silencieusement
- Les utilisateurs doivent employer eux-mêmes des verrous explicites
-
Non-repeatable read et violations de cohérence interne
- MySQL Repeatable Read présente des violations de cohérence interne même sur un seul nœud sain
- Dans le même run de test, 126 transactions commit sur 9 048 ont montré des erreurs de cohérence interne
- Dans un exemple, une transaction lit une clé comme
nil, ajoute une valeur, puis relit la même clé et observe que trois autres valeurs ont été ajoutées - Dans un autre exemple, après avoir lu la clé 1096 comme
[1 2 3]et append7, une seconde lecture observe[1 2 3 4 5 6 7] - Dans le workload ciblé, au sein d’une transaction Repeatable Read,
nameest lu comme"pebble",genderest mis à jour en"femme", puis une nouvelle lecture du mêmenamerenvoie"moss" - Ce comportement contredit la définition ANSI des non-repeatable reads et la description de la documentation MySQL selon laquelle le snapshot est « établi lors de la première lecture »
-
Violations de Monotonic Atomic View
- Monotonic Atomic View est la propriété selon laquelle une transaction qui observe un effet d’une autre transaction doit en observer tous les effets
- MySQL Repeatable Read la viole de façon répétée, même sur un seul nœud sain
- Dans le workload, le writer incrémente la ligne 0, puis la ligne 1
- Le reader voit d’abord l’ancienne valeur
0sur la ligne 0, puis l’incrément1du writer sur la ligne 1, puis à nouveau0sur la ligne 0 - Il s’agit d’une lecture non monotone qui voit l’effet sur la ligne 1, mais pas celui sur la ligne 0, ce qui ne correspond pas au comportement attendu d’un snapshot classique
Anomalies de Serializable sur AWS RDS MySQL
- Les clusters AWS RDS MySQL violent de façon répétée la serializability même au niveau d’isolation « Serializable »
- Sur un cluster RDS MySQL avec le profil de production recommandé par défaut, le test append montre des anomalies G2-item et G-single
- Les anomalies observées prenaient la forme d’une transaction manquant des dépendances antérieures d’une autre transaction dont elle voyait les effets
- Cette anomalie est classée à la fois comme G-single et G2-item, et viole Snapshot Isolation, Repeatable Read et Serializability
- Les réglages liés à
replica_preserve_commit_orderrestent un facteur suspect- Depuis MySQL 8.0.27,
replica_preserve_commit_order=ONest la valeur par défaut - Les paramètres RDS par défaut choisissent toujours un réglage équivalent à
replica_preserve_commit_order=OFF - Dans les parameter groups RDS, l’ancien nom de ce réglage,
slave_preserve_commit_order, est utilisé - Lorsque ce réglage est appliqué à un cluster de test local, des G-single et G2-item similaires sont observés
- Depuis MySQL 8.0.27,
Points semblant fonctionner correctement et résultats LazyFS
- Read Uncommitted, Read Committed et Serializable dans MySQL 8.0.34 semblent satisfaire respectivement PL-1, PL-2 et PL-3
- Ce résultat a été observé à la fois sur un nœud unique et sur un petit cluster avec réplicas read-only utilisant la réplication binlog
- Ces résultats se maintiennent aussi lors de process pauses, crashes et network partitions
- L’injection de fautes LazyFS n’a pas mis en évidence de problème avec la configuration par défaut de MySQL
- Avec la valeur par défaut
innodb_flush_log_at_trx_commit=1, aucune perte de transaction commit n’est apparue après un crash de process et une perte de données non fsyncées - En passant à
innodb_flush_log_at_trx_commit=0, MySQL ne faisait un fsync qu’une fois toutes les quelques secondes, et une perte de données a été observée
Nature réelle de MySQL Repeatable Read
- MySQL Repeatable Read ne satisfait pas Repeatable Read PL-2.99
- Il présente G2-item et write skew
- Il ne satisfait pas non plus Snapshot Isolation
- Il présente G-single, read skew et lost update
- Il ne satisfait pas non plus cursor stability
- Des lost updates se produisent
- Read Atomic, Causal Consistency, Consistent View, Prefix Consistency et Parallel Snapshot Isolation sont également exclus
- Des violations de cohérence interne ont été observées
- MySQL Repeatable Read semble toutefois un peu plus fort que Read Committed
- G0 dirty write, G1a aborted read, G1b intermediate read et G1c cyclic information flow n’ont pas été observés
- La repeatability de certaines lectures fournit des propriétés plus fortes que Read Committed
- Cependant, il n’est pas clair quel consistency model décrit exactement MySQL Repeatable Read, et aucune propriété formelle n’est définie officiellement
Décalage entre documentation et compréhension de la communauté
- Le comportement de Repeatable Read reste insuffisamment compris dans la communauté MySQL
- Plusieurs articles pensent que MySQL Repeatable Read empêche les lost updates, tandis que d’autres disent qu’il ne les empêche pas et recommandent des verrous explicites
- De nombreuses ressources en ligne affirment que MySQL Repeatable Read est effectivement repeatable, mais les tests de Jepsen montrent des contre-exemples
- La documentation de MySQL et de MariaDB explique aussi que Repeatable Read lit le même snapshot au sein d’une même transaction
- Une phrase de la documentation MySQL sur les consistent reads laisse entendre un comportement contradictoire avec cette description, mais cette information est noyée dans la documentation
Recommandations
- Si MySQL conserve son comportement actuel, il devrait documenter clairement le consistency model réellement fourni par « Repeatable Read »
- Une autre option consiste à traiter le comportement actuel comme un bug et à le corriger
- Jepsen indique qu’il accueillerait favorablement l’engagement de MySQL et d’autres fournisseurs à fournir Repeatable Read PL-2.99
- Les utilisateurs qui ont besoin de PL-2.99 ou d’ANSI Repeatable Read doivent faire preuve de prudence avec MySQL Repeatable Read
- Les alternatives pratiques sont les suivantes
- Utiliser le niveau d’isolation Serializable de MySQL
- Renforcer les lectures en
READ COMMITTEDavec des techniques de verrouillage commeSELECT ... FOR UPDATE
Recommandations pour les utilisateurs RDS
- Les clusters AWS RDS MySQL présentent du read skew et G2-item en « Serializable »
- Les utilisateurs qui dépendent de la serializability doivent définir
slave_preserve_commit_ordersurONdans le parameter group RDS - Il est suggéré qu’AWS modifie la valeur par défaut, ou documente clairement les violations de Serializability autorisées dans les known limitations de RDS MySQL
Travaux futurs et appel à la standardisation
- La réplication binlog de MySQL semblait fragile
- Des tests Jepsen locaux ont observé plusieurs situations où la réplication s’arrêtait
- La réplication AWS RDS MySQL pouvait être complètement cassée après seulement quelques minutes de test, et une situation où un
CREATE DATABASEréussi sur le primary n’apparaissait pas sur le secondary ne s’est pas rétablie pendant une heure
- La promotion d’un secondary en primary, ainsi que les topologies de réplication de type ring ou star, n’ont pas été explorées
- Des travaux sont en cours sur des tests de prédicats plus généraux afin d’évaluer la predicate safety
- Les définitions ANSI des niveaux d’isolation SQL n’ont pas changé, malgré les 28 ans écoulés depuis que Berenson et al. en ont souligné l’ambiguïté et l’incomplétude, et malgré sept révisions ANSI·ISO
- Des définitions de niveaux d’isolation plus formelles et portables sont nécessaires pour qu’ISO/IEC 9075-2 puisse traiter clairement des phénomènes comme les anomalies internes, les lost updates et les dirty writes
1 commentaires
Avis sur Hacker News
J’estime depuis longtemps que repeatable read est une mauvaise idée, même si l’implémentation est parfaite
Même si cela fonctionne correctement dans la base de données, le raisonnement devient trop difficile avec des requêtes complexes
À mes yeux, les seuls niveaux d’isolation qui aient du sens sont read committed et serializable
Il faut soit aller jusqu’au bout avec serializable pour éviter toute surprise, soit choisir un read committed où il est clair qu’il faut verrouiller les lignes avant de lire si l’on a besoin d’une vue cohérente dans la transaction
read committed se rapproche du code multithread classique et de la gestion mémoire, donc les ingénieurs peuvent plus facilement se forger une intuition, et serializable est si strict qu’il laisse peu de place aux erreurs inattendues
Entre les deux, c’est un no man’s land, et tout ce qui est moins cohérent que read committed ne mérite plus vraiment le nom de base de données
Plus l’application grossit, plus il devient très difficile de comprendre tous les cas où des verrous sont pris et où les données sont accédées
Pour les transactions lecture/écriture, serializable est à mes yeux le seul modèle d’isolation sensé, et pour les transactions en lecture seule, snapshot isolation, qui manipule un instantané de la base à un moment donné, est un bon modèle
D’ailleurs, ce sont pratiquement les deux seuls modes fournis par Spanner : https://cloud.google.com/spanner/docs/transactions
Il y a une présentation à FOSSDEM 2024 qui compare les niveaux d’isolation et MVCC des bases SQL
Elle couvre Oracle, MySQL, SQL Server, PostgreSQL et YugabyteDB
https://fosdem.org/2024/schedule/event/fosdem-2024-3600-isol...
Je me demande comment append(a) se mappe exactement sur une opération SQL réelle pour la table donnée
Est-ce qu’un champ TEXT est utilisé comme une liste ?
Il m’est aussi arrivé de voir un simple SELECT sur une seule ligne renvoyer un résultat impossible en mode repeatable read de MySQL
C’était de la forme
SELECT min(value), max(value) FROM table WHERE id = 1;, avecidcomme clé primaire, etminetmaxrenvoyaient des valeurs différentesÀ noter que ce n’est pas un problème propre à CONCAT. Si CONCAT est utilisé, c’est parce que cela permet de raisonner sur les anomalies en temps linéaire plutôt qu’en temps exponentiel
Le même type de comportement apparaît aussi avec des registres lecture/écriture ordinaires
J’ai apprécié l’article et le fait qu’il traite d’AWS RDS, mais je me demande s’il s’intéressait aussi à AWS Aurora MySQL
Pour ceux qui ne le savent pas, AWS a créé une plateforme de base de données compatible protocole qui se fait passer pour MySQL ou PostgreSQL
Il serait intéressant de voir si Aurora MySQL présente les mêmes « caractéristiques » que RDS ou MariaDB
Cela reste néanmoins une cible très intéressante, et comme Aurora est une base bien plus récente, j’ai l’intuition qu’elle peut encore cacher des problèmes subtils qui n’ont pas été découverts, là où l’ancien MySQL est déjà mieux connu
Il y a quand même un gros point irritant
Des ingénieurs de Plaid ont écrit un bon article qui résume bien les différences : https://plaid.com/blog/exploring-performance-differences-bet...
Pour moi, la plus grande différence est que le modèle d’isolation est légèrement différent parce que le cluster Aurora utilise un stockage partagé
read committed n’est possible qu’en configurant un paramètre au niveau de tout le cluster, et read uncommitted me semble impossible
Article très intéressant
Il montre bien combien de « systèmes qui fonctionnent en pratique » peuvent être construits sur une base qui présente autant d’anomalies de cohérence
Le passage indiquant qu’en seulement 5 minutes de manipulation, la réplication RDS s’est arrêtée, sans même une alerte de health check en échec, est assez inquiétant
En revanche, cela reporte sur l’utilisateur la charge de fouiller parmi plus de 150 métriques et de lire la documentation pour trouver celles qui comptent
De plus, <https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_...> mentionne une cellule de tableau dans la console censée afficher l’état de la réplication, mais dans la console il faut souvent activer soi-même l’affichage de cette colonne, ce qui n’est pas idéal
AWS s’appuie énormément sur ce qu’il appelle le « modèle de responsabilité partagée »
Il faut tout faire soi-même depuis l’intérieur de l’hôte ou du conteneur
Le support AWS/Rackspace se contente de dire que ce qui s’exécute à l’intérieur d’un service AWS n’est pas géré par eux, donc c’est un problème client
J’aime le passage disant qu’en 2022 Jepsen a demandé à INESC TEC de l’université de Porto de développer LazyFS
Un système de fichiers FUSE qui simule la perte d’écritures non
fsync, c’est un excellent exemple de technologie qui fait avancer l’état de l’artSELECT ... FOR UPDATEdonne l’impression d’être la réponse à ce genre de problèmesSi on verrouille la ligne à mettre à jour, est-ce que soudain tout ne se met pas à fonctionner comme annoncé ?
Si vous voulez mettre à jour un enregistrement à partir des données d’un autre, il faut faire une lecture avec verrouillage sur cet autre enregistrement, et probablement aussi sur celui à mettre à jour
Si vous mettez à jour un enregistrement à partir d’un autre avec une seule requête SQL, MySQL verrouillera de toute façon les deux
Si vous devez mettre à jour quelque chose à partir de plusieurs cibles, d’après mon expérience les interblocages arrivent très facilement
Il vaut mieux à la place verrouiller quelque chose comme un enregistrement de verrouillage, puis faire une repeatable read sur les données voulues et effectuer la mise à jour
Le point de référence temporel de repeatable read n’est pas fixé tant qu’on n’a pas effectué de lecture cohérente
SELECT ... FOR UPDATEn’est pas une lecture cohérente, donc cela fonctionne bien dans des situations concurrentes sans verrouiller des dizaines ou des centaines de lignes via une mise à jour SQL classiqueD’après mon expérience, la plupart des développeurs ne réfléchissent même pas au niveau d’isolation et gardent simplement la valeur par défaut
Quand une condition de concurrence se produit, ils se disent juste « tiens, c’est bizarre » et passent à autre chose
[1] https://news.ycombinator.com/item?id=38696421
C’est pourquoi, à mon avis, il vaut mieux que la plupart des développeurs n’aient pas à se soucier eux-mêmes du niveau d’isolation, et que MySQL ainsi que certaines autres bases de données offrent trop peu de garanties au développeur moyen