Jepsens Bewertung von MySQL 8.0.34
(jepsen.io)- Das standardmäßige Isolationsniveau von MySQL 8.0.34, Repeatable Read, zeigt selbst auf einem einzelnen fehlerfreien Node Verletzungen der Transaktionskonsistenz, die nicht den Erwartungen von ANSI SQL und Adyas PL-2.99 entsprechen
- Mit einer Kombination aus Elles list-append checker, targeted workload und LazyFS wurden MySQL 8.0.34, MariaDB 10.11.3, binlog-Replikationscluster und AWS RDS MySQL Multi-AZ DB Cluster gemeinsam geprüft
- Wie in Kleppmanns Hermitage-Ergebnissen von 2014 wurden G2-item, G-single und lost update reproduziert; außerdem wurden interne Konsistenzverletzungen, non-repeatable reads und Verletzungen von Monotonic Atomic View beobachtet
- Read Uncommitted, Read Committed und Serializable eines einzelnen MySQL schienen jeweils PL-1, PL-2 und PL-3 zu entsprechen, doch AWS RDS MySQL Cluster zeigten selbst unter Serializable G2-item und G-single
- Wer Repeatable Read auf ANSI- oder PL-2.99-Niveau benötigt, kann sich kaum allein auf MySQL Repeatable Read verlassen; nötig sind Serializable oder explizite Sperren wie
SELECT... FOR UPDATE
Bewertete Systeme und Umfang
- MySQL ist eine weit verbreitete relationale Datenbank; in dieser Analyse bezeichnet „MySQL“ MySQL mit der standardmäßigen Storage Engine InnoDB
- Der Fokus liegt auf MySQL als Einzelserver, behandelt werden aber auch Cluster mit einem schreibenden Primary und read-only Secondaries, die binlog-Replikation verwenden
- Getestet wurden:
- MySQL 8.0.34
- MariaDB 10.11.3
- Debian Bookworm
- Das Profil „Multi-AZ DB Cluster“ von AWS RDS Cluster
- Die Arbeit wurde unabhängig und unentgeltlich durchgeführt und folgte der Jepsen ethics policy
SQL-Isolationsniveaus und die Kriterien für Repeatable Read
- ANSI SQL definiert Read Uncommitted, Read Committed, Repeatable Read und Serializable darüber, ob P1 dirty read, P2 non-repeatable read und P3 phantom möglich sind
- 1995 kritisierten Berenson et al. in A Critique of ANSI SQL Isolation Levels die Mehrdeutigkeit und Unvollständigkeit der ANSI-Definitionen
- P1, P2 und P3 lassen Interpretationsspielraum
- Wichtige Phänomene wie P0 dirty write fehlen
- P3 verbietet nur Inserts, die ein Prädikat beeinflussen, behandelt aber keine Updates oder Deletes
- Atul Adyas Paper von 1999 definiert implementierungsunabhängige Isolationsniveaus auf Basis von Abhängigkeitsgraphen zwischen Transaktionen
- PL-1 verbietet G0 write cycle
- PL-2 verbietet G0 und G1
- PL-2.99 verbietet G0, G1 und G2-item und entspricht Repeatable Read
- PL-3 verbietet G0, G1 und G2 und entspricht Serializable
- Jepsen nutzt im Allgemeinen Adyas Formalismus, um Transaktionshistorien und Anomalien zu bestimmen
Konflikt zwischen MySQL-Dokumentation und Repeatable Read
- Die MySQL-Dokumentation erklärt, dass InnoDB alle vier Isolationsniveaus des SQL:1992-Standards bereitstellt
- Das standardmäßige Isolationsniveau Repeatable Read wird so beschrieben, dass consistent reads innerhalb derselben Transaktion den Snapshot lesen, der beim ersten Read festgelegt wurde
- Auch die Dokumentation zu consistent reads erklärt, dass man die Datenbank entsprechend dem Zeitpunkt des ersten Reads sieht
- Eine Anmerkung in derselben Dokumentation besagt jedoch, dass der Snapshot zwar für
SELECTgilt, aber nicht zwingend für DML-Statements;DELETEoderUPDATEkönnen Rows berühren, die von anderen Transaktionen committet wurden - Diese Anmerkung widerspricht dem Umstand, dass ANSI SQL und das MySQL-Referenzhandbuch auch
SELECTals DML ansehen, und schafft Verwirrung darüber, dass Writes unter Repeatable Read Rows beeinflussen können, die sie nicht lesen konnten
Testdesign
- Die Testsuite für MySQL wurde auf Basis der Jepsen testing library 0.3.4 geschrieben
- Der Client verwendet den JDBC-Adapter
mysql-connector-j - Die Tests umfassen fault injection wie process pause, crash, network partition und den Verlust nicht per fsync geschriebener Disk-Writes
- Fast alle Befunde dieser Analyse traten jedoch auf einem einzelnen MySQL-Node im Normalzustand auf
-
Elle list-append workload
- Der zentrale Workload nutzt Elles Elle list-append checker
- Elle leitet write-write-, write-read- und read-write-Abhängigkeiten zwischen Transaktionen ab und weist anhand von Zyklen im Abhängigkeitsgraphen Verletzungen bestimmter Isolationsniveaus nach
- Der list-append workload führt zufällige Transaktionen aus Reads und Appends auf mehreren Listen aus, die über Primary Keys identifiziert werden
- Listen werden als
text-Feld mit kommagetrennten Werten codiert, Appends erfolgen per SQLCONCAT - Durch jüngste Verbesserungen erkennt Elle besser:
- Ableitung von ww/rw-Abhängigkeiten für nicht gelesene Append-Elemente
- Explizite Erkennung von P4 lost update
- Suche nach komplexen Zyklen einschließlich real-time edge und process edge
-
Targeted workload
- Der Non-repeatable-read-Workload zielt auf eine Row der Tabelle
people - Eine Transaktionsserie aktualisiert nur
name, eine andere liestname, aktualisiertgenderund liest danach erneutname - Ändert sich
namezwischen den beiden Reads, ist das eine Verletzung von Repeatable Read - Der Monotonic-Atomic-View-Workload nutzt
valuein zwei Rows - Der Writer erhöht
valuevon Row 0 und danach Row 1 - Der Reader liest Row 0, aktualisiert
noopvon Row 1 und liest danach Row 1 und Row 0 - Wenn man einen Teil der Effekte einer Transaktion sieht, muss man alle Effekte sehen
- Der Non-repeatable-read-Workload zielt auf eine Row der Tabelle
-
LazyFS
- LazyFS ist ein FUSE-Dateisystem, das den Verlust nicht per fsync geschriebener Writes simuliert
- Getestet wurde, indem der MySQL-Prozess beendet, der LazyFS-Cache verworfen und MySQL anschließend neu gestartet wurde
- Dieser Bericht ist der erste öffentliche Jepsen-Bericht mit LazyFS
In MySQL Repeatable Read gefundene Anomalien
-
G2-item
- Adyas PL-2.99 Repeatable Read verbietet G2-item, also Zyklen aus write-write-, write-read- und read-write-Abhängigkeiten ohne Prädikate
- MySQL Repeatable Read erlaubt G2-item wiederholt, selbst auf einem einzelnen fehlerfreien Node
- Das Verhalten, das Kleppmann 2014 in Hermitage berichtete, tritt auch in MySQL 8.0.34 weiterhin auf
- Ein Beispieltest zeigte in 40 Sekunden 214 Zyklen
- Dieses Verhalten ist unter PL-2.99 Repeatable Read verboten; da die ANSI-Definition von P2 jedoch nur den Fall behandelt, dass dieselbe Row zweimal gelesen wird, bleibt nach ANSI-Definition Interpretationsspielraum
-
G-single und read skew
- MySQL Repeatable Read zeigt auch G-single
- G-single ist ein Zyklus aus write-write-, write-read- und read-write-Edges, bei dem read-write-Edges nicht benachbart sind
- Der von Kleppmann 2014 berichtete read skew wurde auch in MySQL 8.0.34 bestätigt
- In einem 60-sekündigen Append-Test traten bei rund 140 Transaktionen pro Sekunde 244 Fälle von G-single und 305 Fälle von G2-item auf
- Da der Append-Test keine Prädikatoperationen verwendet, werden alle als Verletzungen von Repeatable Read klassifiziert
-
Lost update
- P4 lost update ist ein Spezialfall von G-single, bei dem zwei Transaktionen dieselbe Version desselben Keys lesen und beide aktualisieren
- Snapshot Isolation und PL-2.99 Repeatable Read verbieten lost update
- MySQL Repeatable Read erlaubt lost update wiederholt, selbst auf einem einzelnen fehlerfreien Node
- In einem Test fand der neue Checker unter 9.048 erfolgreichen Transaktionen 446 Transaktionen, die an 198 Fällen von lost update beteiligt waren
- Davon wurden nur 47 Fälle als Zyklus sichtbar
- Muster, bei denen ein Wert gelesen und anschließend geschrieben wird, sind unter MySQL Repeatable Read nicht sicher
- Beim Standard-ORM-Muster, ein Objekt zu lesen, im Speicher zu ändern und danach erneut zu speichern, können committete Änderungen stillschweigend verschwinden
- Nutzer müssen explizite Sperren selbst verwenden
-
Non-repeatable read und interne Konsistenzverletzungen
- MySQL Repeatable Read zeigt selbst auf einem einzelnen fehlerfreien Node interne Konsistenzverletzungen
- Im selben Testlauf wiesen 126 von 9.048 committeten Transaktionen interne Konsistenzfehler auf
- In einem Beispiel las eine Transaktion einen Key als
nil, appendete einen Wert und beobachtete beim erneuten Lesen desselben Keys, dass drei weitere Werte hinzugefügt worden waren - In einem anderen Beispiel las sie Key 1096 als
[1 2 3], appendete7und beobachtete beim erneuten Lesen[1 2 3 4 5 6 7] - In einem targeted workload las eine Repeatable-Read-Transaktion
nameals"pebble", aktualisiertegenderauf"femme"und erhielt beim erneuten Lesen desselbennameden Wert"moss" - Ein solches Verhalten widerspricht der ANSI-SQL-Definition von non-repeatable read und der Beschreibung in der MySQL-Dokumentation, nach der der Snapshot „beim ersten Read“ festgelegt wird
-
Verletzung von Monotonic Atomic View
- Monotonic Atomic View ist die Eigenschaft, dass eine Transaktion, die einen Effekt einer anderen Transaktion sieht, alle Effekte dieser Transaktion sehen muss
- MySQL Repeatable Read verletzt dies wiederholt auch auf einem normalen einzelnen Node
- Im Workload erhöht der Writer zunächst Row 0 und danach Row 1
- Der Reader sieht auf Row 0 den alten Wert
0, sieht anschließend auf Row 1 die Erhöhung des Writers1und sieht danach auf Row 0 weiterhin0 - Das ist ein nichtmonotoner Read, bei dem der Effekt auf Row 1 gesehen wurde, der Effekt auf Row 0 aber nicht, und passt nicht zu üblichem Snapshot-Verhalten
Anomalien in AWS RDS MySQL Serializable
- AWS RDS MySQL Cluster verletzen wiederholt Serializability, selbst beim Isolationsniveau „Serializable“
- In einem RDS MySQL Cluster mit dem standardmäßig empfohlenen Production-Profil zeigte der Append-Test G2-item- und G-single-Anomalien
- Die beobachtete Anomalie hatte die Form, dass eine Transaktion, die die Effekte einer anderen Transaktion sah, deren frühere Abhängigkeiten verpasste
- Diese Anomalie wird sowohl als G-single als auch als G2-item klassifiziert und verletzt Snapshot Isolation, Repeatable Read und Serializability
- Einstellungen rund um
replica_preserve_commit_orderbleiben ein Verdachtsfaktor- Ab MySQL 8.0.27 ist
replica_preserve_commit_order=ONder Default - Die RDS-Default-Parameter wählen weiterhin eine Einstellung, die
replica_preserve_commit_order=OFFentspricht - In RDS parameter groups wird der frühere Name dieser Einstellung verwendet:
slave_preserve_commit_order - Wird diese Einstellung auf einen lokalen Testcluster angewendet, werden ähnliche G-single- und G2-item-Fälle beobachtet
- Ab MySQL 8.0.27 ist
Bereiche, die unauffällig wirkten, und LazyFS-Ergebnisse
- Read Uncommitted, Read Committed und Serializable von MySQL 8.0.34 scheinen jeweils PL-1, PL-2 und PL-3 zu erfüllen
- Dieses Ergebnis wurde sowohl auf einem einzelnen Node als auch in kleinen read-only replica-Clustern mit binlog-Replikation beobachtet
- Auch bei process pause, crash und network partition blieb dieses Ergebnis bestehen
- LazyFS fault injection fand mit den MySQL-Standardeinstellungen keine Probleme
- Mit dem Default
innodb_flush_log_at_trx_commit=1traten auch nach process crash und Verlust nicht per fsync geschriebener Daten keine Verluste committeter Transaktionen auf - Bei Umstellung auf
innodb_flush_log_at_trx_commit=0führte MySQL nur alle paar Sekunden einen fsync aus, und Datenverlust wurde beobachtet
Der tatsächliche Charakter von MySQL Repeatable Read
- MySQL Repeatable Read erfüllt PL-2.99 Repeatable Read nicht
- Es zeigt G2-item und write skew
- Es erfüllt auch Snapshot Isolation nicht
- Es zeigt G-single, read skew und lost update
- Es erfüllt auch cursor stability nicht
- Lost update tritt auf
- Read Atomic, Causal Consistency, Consistent View, Prefix Consistency und Parallel Snapshot Isolation werden ebenfalls ausgeschlossen
- Interne Konsistenzverletzungen wurden beobachtet
- MySQL Repeatable Read wirkt etwas stärker als Read Committed
- G0 dirty write, G1a aborted read, G1b intermediate read und G1c cyclic information flow wurden nicht beobachtet
- Die Repeatability einiger Reads bietet stärkere Eigenschaften als Read Committed
- Allerdings ist unklar, welches consistency model MySQL Repeatable Read genau darstellt, und es gibt keine formale Eigenschaftsdefinition
Diskrepanz zwischen Dokumentation und Community-Verständnis
- In der MySQL-Community ist das Verhalten von Repeatable Read nicht ausreichend verstanden
- Mehrere Beiträge glauben, dass MySQL Repeatable Read lost update verhindert, andere berichten, dass es das nicht verhindert, und empfehlen explizite Sperren
- Diverse Internetquellen sagen, dass MySQL Repeatable Read tatsächlich repeatable sei; Jepsens Tests zeigen jedoch Gegenbeispiele
- Auch die Dokumentation von MySQL und MariaDB erklärt, dass Repeatable Read innerhalb derselben Transaktion denselben Snapshot liest
- Ein Satz in der MySQL-Dokumentation zu consistent reads deutet ein Verhalten an, das dieser Beschreibung widerspricht, ist dort aber eher versteckt
Empfehlungen
- Wenn MySQL das aktuelle Verhalten beibehält, sollte klar dokumentiert werden, welches consistency model „Repeatable Read“ tatsächlich bereitstellt
- Eine andere Möglichkeit wäre, das aktuelle Verhalten als Bug zu behandeln und zu beheben
- Jepsen erklärt, dass es begrüßt würde, wenn MySQL und andere Anbieter PL-2.99 Repeatable Read zusichern
- Nutzer, die PL-2.99 oder ANSI Repeatable Read benötigen, sollten bei MySQL Repeatable Read vorsichtig sein
- Praktische Alternativen sind:
- Nutzung des Isolationsniveaus Serializable von MySQL
- Verstärkung von Reads unter
READ COMMITTEDmit Sperrtechniken wieSELECT ... FOR UPDATE
Empfehlungen für RDS-Nutzer
- AWS RDS MySQL Cluster zeigen unter „Serializable“ read skew und G2-item
- Nutzer, die auf Serializability angewiesen sind, sollten in der RDS parameter group
slave_preserve_commit_orderaufONsetzen - Es wird vorgeschlagen, dass AWS den Default ändert oder in der Dokumentation zu known limitations von RDS MySQL klar beschreibt, welche Serializability-Verletzungen möglich sind
Künftige Arbeiten und Standardisierungsaufruf
- MySQL binlog replication wirkte anfällig
- In lokalen Jepsen-Tests wurden mehrere Situationen beobachtet, in denen die Replikation stoppte
- AWS RDS MySQL replication konnte nach nur wenigen Minuten Test vollständig brechen; ein auf dem Primary erfolgreiches
CREATE DATABASEerschien nicht auf dem Secondary, und dieser Zustand erholte sich eine Stunde lang nicht
- Das Befördern eines Secondary zum Primary sowie Replikationstopologien wie Ring oder Star wurden nicht untersucht
- Es läuft Forschung zu allgemeineren predicate tests, um predicate safety zu bewerten
- Die ANSI-SQL-Definitionen der Isolationsniveaus wurden 28 Jahre nach Berenson et al.s Hinweis auf Mehrdeutigkeit und Unvollständigkeit und nach sieben ANSI-/ISO-Revisionen nicht geändert
- Es braucht formalere und portablere Definitionen von Isolationsniveaus, damit ISO/IEC 9075-2 Phänomene wie interne Anomalien, lost update und dirty write klar behandeln kann
1 Kommentare
Meinungen auf Hacker News
repeatable read halte ich schon seit Langem für eine schlechte Idee, selbst wenn die Implementierung perfekt ist.
Selbst wenn es innerhalb der Datenbank korrekt funktioniert, ist es bei komplexen Queries viel zu schwer, darüber zu schlussfolgern.
Meiner Meinung nach sind die einzigen sinnvollen Isolation Levels read committed und serializable.
Entweder geht man konsequent bis hin zu serializable, damit es keine Überraschungen gibt, oder man wählt read committed, wo klar ist: Wenn man innerhalb einer Transaktion eine konsistente Sicht braucht, muss man die Zeilen vor dem Lesen sperren.
read committed ähnelt eher gewöhnlichem multithreaded Code und Speichermanagement, sodass Engineers leichter eine Intuition dafür entwickeln können; serializable ist so strikt, dass es schwer ist, versehentlich überraschende Fehler zu bauen.
Alles dazwischen ist Niemandsland, und alles, was weniger konsistent ist als read committed, ist kaum noch als richtige Datenbank zu betrachten.
Je größer eine Anwendung wird, desto schwieriger wird es, alle Fälle zu verstehen, in denen Locks gesetzt werden und auf Daten zugegriffen wird.
Für Lese-/Schreibtransaktionen ist serializable das einzig vernünftige Isolationsmodell; für reine Lesetransaktionen ist snapshot isolation, bei der man mit einem Datenbank-Snapshot eines bestimmten Zeitpunkts arbeitet, ein gutes Modell.
Die von Spanner angebotenen Modi sind im Grunde auch nur diese beiden: https://cloud.google.com/spanner/docs/transactions
Auf der FOSSDEM 2024 gibt es einen Vortrag, der Isolation Levels und MVCC in SQL-Datenbanken vergleicht.
Behandelt werden Oracle, MySQL, SQL Server, PostgreSQL und YugabyteDB.
https://fosdem.org/2024/schedule/event/fosdem-2024-3600-isol...
Ich frage mich, wie append(a) auf die tatsächlichen SQL-Operationen einer gegebenen Tabelle abgebildet wird.
Wird ein TEXT-Feld wie eine Liste verwendet?
Im MySQL-Modus repeatable read hatte ich auch schon einmal, dass ein einzelnes SELECT, das eine einzelne Zeile auswählt, ein unmögliches Ergebnis zurückgab.
Es war in der Form
SELECT min(value), max(value) FROM table WHERE id = 1;, undidwar der Primärschlüssel, aberminundmaxkamen als unterschiedliche Werte heraus.Zur Einordnung: Das ist kein Problem, das speziell auf CONCAT beschränkt ist. CONCAT wird verwendet, weil sich die Anomalie damit in linearer Zeit statt in exponentieller Zeit herleiten lässt.
Dasselbe Verhalten tritt auch bei gewöhnlichen Lese-/Schreib-Registern auf.
Mir hat gefallen, dass der Artikel auch AWS RDS behandelt hat, aber ich frage mich, ob der Fokus auch auf AWS Aurora MySQL lag.
Für alle, die es nicht kennen: AWS hat eine protokollkompatible Datenbankplattform gebaut, die so tut, als wäre sie MySQL oder PostgreSQL.
Es wäre interessant zu sehen, ob Aurora MySQL dieselben „Eigenschaften“ wie RDS oder MariaDB hat.
Trotzdem wäre es ein sehr interessantes Ziel, und mein Bauchgefühl sagt mir: Da Aurora eine deutlich neuere Datenbank ist, gibt es dort im Vergleich zum alten MySQL vermutlich noch unentdeckte subtile Probleme.
Einen großen Nervfaktor gibt es allerdings.
Engineers von Plaid haben einen guten Artikel geschrieben, der die Unterschiede zusammenfasst: https://plaid.com/blog/exploring-performance-differences-bet...
Der größte Unterschied für mich ist, dass Aurora-Cluster Shared Storage verwenden, wodurch das Isolationsmodell etwas anders ist.
read committed ist nur möglich, wenn man einen clusterweiten Parameter setzt, und read uncommitted ist meiner Ansicht nach nicht möglich.
Ein sehr interessanter Artikel.
Er zeigt gut, wie viele „tatsächlich funktionierende Systeme“ auf einer Grundlage entstehen können, die so viele Konsistenzanomalien aufweist.
Dass RDS-Replikation nach nur fünf Minuten Herumprobieren stehen blieb und es keine Benachrichtigung über fehlgeschlagene Health Checks gab, ist etwas beunruhigend
Allerdings wälzt AWS die Last eher auf die Nutzer ab, die über 150 Metriken durchsehen und die Dokumentation lesen müssen, um die wichtigen zu finden
Außerdem heißt es unter <https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_...>, es gebe in der Konsole eine Tabellenzelle, die den Replikationsstatus anzeigt; in der Konsole muss man diese Spalte aber oft erst selbst einblenden, was nicht gut ist
AWS stützt sich ziemlich stark auf sein sogenanntes „Modell der geteilten Verantwortung“
Man muss das alles selbst innerhalb des Hosts oder Containers erledigen
Der AWS-/Rackspace-Support sagt lediglich: „Was innerhalb eines AWS-Dienstes läuft, verwalten wir nicht, das ist ein Kundenproblem“
Gut finde ich den Teil, dass Jepsen 2022 die Entwicklung von LazyFS bei INESC TEC der Universität Porto in Auftrag gegeben hat
Ein FUSE-Dateisystem, das den Verlust nicht per
fsyncgeschriebener Writes simuliert – ein großartiges Beispiel dafür, wie man den Stand der Technik vorantreibtSELECT ... FOR UPDATEwirkt wie die Antwort auf diese ProblemeWenn man die zu aktualisierende Zeile sperrt, funktioniert dann nicht plötzlich alles wie beworben?
Wenn man einen Datensatz abhängig von den Daten eines anderen Datensatzes aktualisieren möchte, muss man für diesen anderen Datensatz und wahrscheinlich auch für den zu aktualisierenden Datensatz einen Locking Read durchführen
Wenn man einen Datensatz mit einer einzelnen SQL-Abfrage auf Basis eines anderen Datensatzes aktualisiert, sperrt MySQL ohnehin beide
Wenn man etwas auf Basis mehrerer Ziele aktualisieren muss, entstehen meiner Erfahrung nach sehr leicht Deadlocks
Stattdessen ist es besser, etwa einen Locking-Datensatz zu sperren und anschließend auf den gewünschten Daten einen Repeatable Read durchzuführen und zu aktualisieren
Der Zeitpunkt von Repeatable Read wird erst festgelegt, wenn man einen konsistenten Read ausführt
SELECT ... FOR UPDATEist kein konsistenter Read, funktioniert also in Konkurrenzsituationen gut, ohne mit einem normalen SQL-Update Dutzende oder Hunderte von Zeilen zu sperrenMeiner Erfahrung nach berücksichtigen die meisten Entwickler Isolationslevel von vornherein nicht und verwenden einfach die Defaults
Wenn Race Conditions auftreten, heißt es nur: „Hm, komisch“, und man macht weiter
[1] https://news.ycombinator.com/item?id=38696421
Deshalb ist es für die meisten Entwickler besser, sich nicht selbst mit Isolationsleveln beschäftigen zu müssen, und ich denke, dass MySQL und einige andere Datenbanken durchschnittlichen Entwicklern zu wenig Garantien bieten