Evaluación de Jepsen sobre MySQL 8.0.34
(jepsen.io)- El nivel de aislamiento predeterminado de MySQL 8.0.34, Repeatable Read, muestra violaciones de consistencia transaccional que no coinciden con las expectativas de ANSI SQL ni de PL-2.99 de Adya, incluso en un solo nodo sano
- Se validaron en conjunto MySQL 8.0.34, MariaDB 10.11.3, un clúster con replicación binlog y AWS RDS MySQL Multi-AZ DB Cluster usando la combinación del checker list-append de Elle, un workload dirigido y LazyFS
- Al igual que en los resultados de Hermitage de Kleppmann en 2014, se reprodujeron G2-item, G-single y lost update, y también se observaron violaciones de consistencia interna, non-repeatable read y Monotonic Atomic View
- Read Uncommitted, Read Committed y Serializable de MySQL individual parecieron cumplir con PL-1, PL-2 y PL-3 respectivamente, pero el clúster AWS RDS MySQL mostró G2-item y G-single incluso en Serializable
- Si se necesita Repeatable Read a nivel ANSI o PL-2.99, no es confiable depender solo de MySQL Repeatable Read; se requiere Serializable o bloqueo explícito como
SELECT... FOR UPDATE
Alcance y sistemas evaluados
- MySQL es una base de datos relacional ampliamente utilizada, y en este análisis “MySQL” se refiere a MySQL usando InnoDB, su motor de almacenamiento predeterminado
- El foco principal es MySQL en un solo servidor, pero también se cubre un clúster con un primary de escritura única y secundarios de solo lectura usando replicación binlog
- Los sistemas probados fueron los siguientes
- MySQL 8.0.34
- MariaDB 10.11.3
- Debian Bookworm
- El perfil “Multi-AZ DB Cluster” de AWS RDS Cluster
- El trabajo se realizó de forma independiente y sin compensación, siguiendo la política ética de Jepsen
Niveles de aislamiento SQL y el criterio de Repeatable Read
- ANSI SQL define Read Uncommitted, Read Committed, Repeatable Read y Serializable según la posibilidad de P1 dirty read, P2 non-repeatable read y P3 phantom
- En 1995, Berenson y otros criticaron la ambigüedad e incompletitud de la definición ANSI en A Critique of ANSI SQL Isolation Levels
- P1, P2 y P3 dejan margen de interpretación
- Faltan fenómenos importantes como P0 dirty write
- P3 solo prohíbe inserts que afectan predicados, pero no trata updates ni deletes
- El artículo de Atul Adya de 1999 define niveles de aislamiento independientes de la implementación con base en un grafo de dependencias entre transacciones
- PL-1 prohíbe G0 write cycle
- PL-2 prohíbe G0 y G1
- PL-2.99 prohíbe G0, G1 y G2-item, y corresponde a Repeatable Read
- PL-3 prohíbe G0, G1 y G2, y corresponde a Serializable
- Jepsen normalmente usa el marco de Adya para clasificar registros transaccionales y anomalías
Conflicto entre la documentación de MySQL y Repeatable Read
- La documentación de MySQL explica que InnoDB ofrece los cuatro niveles de aislamiento del estándar SQL:1992
- El nivel predeterminado, Repeatable Read, dice que las lecturas consistentes dentro de una misma transacción leen el snapshot establecido en la primera lectura
- La documentación de consistent read también dice que la base de datos se observa según el timepoint de la primera lectura
- Sin embargo, una nota en ese mismo documento dice que el snapshot aplica a
SELECT, pero no necesariamente a sentencias DML, y queDELETEoUPDATEpueden tocar rows que otra transacción ya hizo commit - Esa nota entra en conflicto con el hecho de que ANSI SQL y el manual de referencia de MySQL consideran
SELECTtambién como DML, y genera confusión al permitir que escrituras en Repeatable Read afecten rows que la transacción no podía leer
Diseño de pruebas
- El suite de pruebas para MySQL fue escrito sobre la biblioteca de pruebas Jepsen 0.3.4
- Los clientes usan el adaptador JDBC
mysql-connector-j - Las pruebas incluyen inyección de fallas como pausa de procesos, crash, partición de red y pérdida de escrituras a disco no sincronizadas con fsync
- Aun así, casi todos los hallazgos de este análisis ocurrieron en un solo nodo MySQL sano
-
Workload list-append de Elle
- El workload principal usa el checker Elle de list-append
- Elle infiere dependencias write-write, write-read y read-write entre transacciones, y demuestra violaciones de niveles de aislamiento mediante ciclos en el grafo de dependencias
- El workload list-append ejecuta transacciones aleatorias compuestas por lecturas y appends sobre varias listas identificadas por primary key
- Las listas se codifican en un campo
textcon valores separados por comas, y los append se hacen con SQLCONCAT - Mejoras recientes permiten a Elle detectar mejor lo siguiente
- inferencia de dependencias ww/rw sobre elementos append que no fueron leídos
- detección explícita de P4 lost update
- búsqueda de ciclos complejos que incluyen real-time edge y process edge
-
Workload dirigido
- El workload de non-repeatable read se enfoca en una sola row de la tabla
people - Una familia de transacciones actualiza solo
name, y otra leename, actualizagendery luego vuelve a leername - Si
namecambia entre ambas lecturas, hay una violación de Repeatable Read - El workload de Monotonic Atomic View usa el
valuede dos rows - El writer incrementa el
valuede la row 0 y luego incrementa la row 1 - El reader lee la row 0, actualiza el
noopde la row 1 y luego lee la row 1 y la row 0 - Si se ve parte de los efectos de una transacción, se deberían ver todos sus efectos
- El workload de non-repeatable read se enfoca en una sola row de la tabla
-
LazyFS
- LazyFS es un sistema de archivos FUSE que simula pérdida de escrituras no sincronizadas con fsync
- La prueba se realiza matando el proceso de MySQL, descartando la caché de LazyFS y reiniciando MySQL
- Este reporte es el primer informe público de Jepsen que incluye LazyFS
Anomalías encontradas en MySQL Repeatable Read
-
G2-item
- El Repeatable Read PL-2.99 de Adya prohíbe G2-item, un ciclo de dependencias write-write, write-read y read-write que no incluye predicados
- MySQL Repeatable Read permite G2-item repetidamente incluso en un solo nodo sano
- El comportamiento reportado por Kleppmann en 2014 en Hermitage sigue ocurriendo en MySQL 8.0.34
- Una prueba de ejemplo mostró 214 ciclos en 40 segundos
- Este comportamiento está prohibido en PL-2.99 Repeatable Read, pero la definición P2 de ANSI SQL solo trata el caso de leer dos veces la misma row, así que bajo ANSI sigue habiendo margen de interpretación
-
G-single y read skew
- MySQL Repeatable Read también muestra G-single
- G-single es un ciclo compuesto por edges write-write, write-read y read-write, pero donde los edges read-write no son adyacentes entre sí
- El read skew reportado por Kleppmann en 2014 también se confirmó en MySQL 8.0.34
- En una prueba append de 60 segundos, con unas 140 transacciones por segundo, aparecieron 244 casos de G-single y 305 de G2-item
- Como la prueba append no usa operaciones de predicado, todos estos casos se clasifican como violaciones de Repeatable Read
-
Lost update
- P4 lost update es un caso especial de G-single donde dos transacciones leen la misma versión de la misma key y ambas la actualizan
- Snapshot Isolation y PL-2.99 Repeatable Read prohíben lost update
- MySQL Repeatable Read permite lost update repetidamente incluso en un solo nodo sano
- En una prueba, de 9,048 transacciones exitosas, el nuevo checker encontró 446 transacciones involucradas en 198 casos de lost update
- De esos casos, solo 47 aparecieron como ciclos
- El patrón de leer un valor y luego escribirlo no es seguro en MySQL Repeatable Read
- En el patrón estándar de ORM de leer un objeto, modificarlo en memoria y luego guardarlo de nuevo, cambios ya confirmados pueden desaparecer silenciosamente
- Los usuarios deben usar bloqueo explícito por su cuenta
-
Non-repeatable read y violaciones de consistencia interna
- MySQL Repeatable Read muestra violaciones de consistencia interna incluso en un solo nodo sano
- En la misma ejecución de prueba, 126 de 9,048 transacciones confirmadas mostraron errores de consistencia interna
- En un ejemplo, una transacción leyó una key como
nil, hizo append de un valor y, al volver a leer la misma key, observó que se habían agregado otros tres valores - En otro ejemplo, una transacción leyó la key 1096 como
[1 2 3], hizo append de7y al volver a leer observó[1 2 3 4 5 6 7] - En el workload dirigido, una transacción Repeatable Read leyó
namecomo"pebble", actualizógendera"femme"y, al volver a leer el mismoname, obtuvo"moss" - Este comportamiento contradice la definición de non-repeatable read de ANSI SQL y la explicación de la documentación de MySQL sobre “el snapshot establecido en la primera lectura”
-
Violaciones de Monotonic Atomic View
- Monotonic Atomic View es la propiedad según la cual una transacción que ve algún efecto de otra debe ver todos sus efectos
- MySQL Repeatable Read viola esto repetidamente incluso en un solo nodo sano
- En el workload, el writer incrementa primero la row 0 y luego la row 1
- El reader ve primero el valor anterior
0en la row 0, luego ve el incremento1del writer en la row 1 y después vuelve a ver0en la row 0 - Eso significa que vio el efecto en la row 1 pero no el efecto en la row 0: una lectura no monotónica incompatible con el comportamiento típico de un snapshot
Anomalías en AWS RDS MySQL Serializable
- El clúster AWS RDS MySQL viola repetidamente la serializabilidad incluso en el nivel de aislamiento “Serializable”
- En el perfil de producción recomendado por defecto de RDS MySQL, la prueba append mostró anomalías G2-item y G-single
- Las anomalías observadas tuvieron la forma de que una transacción omitiera una dependencia previa de otra, aunque sí veía los efectos de esa transacción
- Esta anomalía se clasifica como G-single y G2-item, y viola Snapshot Isolation, Repeatable Read y Serializability
- La configuración relacionada con
replica_preserve_commit_ordersigue siendo un factor sospechoso- En MySQL 8.0.27 y posteriores,
replica_preserve_commit_order=ONes el valor predeterminado - Los parámetros predeterminados de RDS siguen eligiendo una configuración equivalente a
replica_preserve_commit_order=OFF - En los parameter groups de RDS, se usa el nombre antiguo de esta opción:
slave_preserve_commit_order - Al aplicar esta configuración en un clúster local de prueba, se observaron G-single y G2-item similares
- En MySQL 8.0.27 y posteriores,
Lo que pareció funcionar bien y resultados con LazyFS
- Read Uncommitted, Read Committed y Serializable de MySQL 8.0.34 parecieron satisfacer PL-1, PL-2 y PL-3 respectivamente
- Este resultado se observó tanto en un solo nodo como en un pequeño clúster con réplicas de solo lectura usando replicación binlog
- También se mantuvo ante pausa de procesos, crash y partición de red
- La inyección de fallas con LazyFS no encontró problemas en la configuración predeterminada de MySQL
- Con el valor predeterminado
innodb_flush_log_at_trx_commit=1, no se observó pérdida de transacciones confirmadas tras crash del proceso ni tras pérdida de datos no sincronizados con fsync - Al cambiar a
innodb_flush_log_at_trx_commit=0, MySQL solo hacía fsync aproximadamente una vez cada pocos segundos y sí se observó pérdida de datos
La naturaleza real de MySQL Repeatable Read
- MySQL Repeatable Read no cumple con Repeatable Read PL-2.99
- Muestra G2-item y write skew
- Tampoco cumple con Snapshot Isolation
- Muestra G-single, read skew y lost update
- Tampoco cumple con cursor stability
- Se presentan lost update
- También quedan descartados Read Atomic, Causal Consistency, Consistent View, Prefix Consistency y Parallel Snapshot Isolation
- Se observaron violaciones de consistencia interna
- MySQL Repeatable Read parece ser algo más fuerte que Read Committed
- No se observaron G0 dirty write, G1a aborted read, G1b intermediate read ni G1c cyclic information flow
- La repetibilidad de algunas lecturas ofrece propiedades más fuertes que Read Committed
- Aun así, no está claro qué modelo de consistencia representa exactamente MySQL Repeatable Read, y no existe una definición formal de sus propiedades
Desajuste entre la documentación y la comprensión de la comunidad
- En la comunidad de MySQL, el comportamiento de Repeatable Read no parece estar suficientemente comprendido
- Varias publicaciones creen que MySQL Repeatable Read evita lost update, mientras que otras afirman que no lo hace y recomiendan usar bloqueo explícito
- Muchas fuentes en internet dicen que MySQL Repeatable Read realmente es repeatable, pero las pruebas de Jepsen muestran casos en que no lo es
- La documentación de MySQL y MariaDB también explica que Repeatable Read lee el mismo snapshot dentro de una misma transacción
- Una frase en la documentación de consistent read insinúa un comportamiento que contradice esa explicación, pero ese detalle queda enterrado en el texto
Recomendaciones
- Si MySQL mantiene su comportamiento actual, debería documentar claramente qué modelo de consistencia ofrece en realidad “Repeatable Read”
- La otra opción es tratar el comportamiento actual como un bug y corregirlo
- Jepsen dice que recibiría con gusto que MySQL y otros vendors prometan ofrecer Repeatable Read PL-2.99
- Los usuarios que necesiten PL-2.99 o Repeatable Read según ANSI deben tener cuidado con MySQL Repeatable Read
- Las alternativas prácticas son las siguientes
- usar el nivel de aislamiento Serializable de MySQL
- reforzar las lecturas en
READ COMMITTEDcon técnicas de bloqueo comoSELECT ... FOR UPDATE
Recomendaciones para usuarios de RDS
- El clúster AWS RDS MySQL muestra read skew y G2-item en “Serializable”
- Los usuarios que dependen de la serializabilidad deben configurar
slave_preserve_commit_orderenONen el parameter group de RDS - Se propone que AWS cambie el valor predeterminado o describa claramente en la documentación de known limitations las violaciones permitidas de serializabilidad en RDS MySQL
Trabajo futuro y pedido de estandarización
- La replicación binlog de MySQL pareció frágil
- En pruebas locales de Jepsen se observaron varios casos donde la replicación se detenía
- La replicación de AWS RDS MySQL podía romperse por completo en solo unos minutos de prueba, y un
CREATE DATABASEexitoso en el primary no aparecía en el secondary durante una hora sin recuperarse
- No se exploró la promoción de secundarios a primary ni topologías de replicación como ring o star
- Sigue en curso la investigación de pruebas de predicados más generales para evaluar predicate safety
- La definición de niveles de aislamiento de ANSI SQL no ha cambiado pese a que Berenson y otros señalaron su ambigüedad e incompletitud hace 28 años, y pese a 7 revisiones ANSI/ISO desde entonces
- Se necesita una definición más formal y portable en ISO/IEC 9075-2 para tratar con claridad fenómenos como anomalías internas, lost update y dirty write
1 comentarios
Opiniones de Hacker News
Desde hace tiempo considero que repeatable read es una mala idea, incluso si la implementación es perfecta.
Aunque funcione correctamente dentro de la base de datos, en consultas complejas razonar sobre ello es demasiado difícil.
Creo que los únicos niveles de aislamiento que tienen sentido son read committed y serializable.
Hay que ir hasta el final con serializable para no tener sorpresas, o usar read committed, donde queda claro que si necesitas una vista consistente dentro de una transacción debes bloquear las filas antes de leerlas.
read committed se parece más al código multihilo común y a la gestión de memoria, por lo que a los ingenieros les resulta más fácil desarrollar intuición; serializable es tan estricto que es difícil cometer errores inesperados.
Lo que queda en medio es tierra de nadie, y algo menos consistente que read committed ya difícilmente puede considerarse una base de datos de verdad.
A medida que la aplicación crece, se vuelve muy difícil entender todos los casos de dónde se toman bloqueos y dónde se accede a los datos.
Para transacciones de lectura/escritura, serializable es el único modelo de aislamiento sensato; para transacciones de solo lectura, snapshot isolation, que trabaja con una instantánea de la base de datos en un punto específico, me parece un buen modelo.
Los modos que ofrece Spanner son, en la práctica, solo esos dos: https://cloud.google.com/spanner/docs/transactions
En FOSSDEM 2024 hay una charla que compara los niveles de aislamiento y MVCC en bases de datos SQL.
Cubre Oracle, MySQL, SQL Server, PostgreSQL y YugabyteDB.
https://fosdem.org/2024/schedule/event/fosdem-2024-3600-isol...
Me pregunto cómo se mapea append(a) a una operación SQL real sobre una tabla dada.
¿Se usa un campo TEXT como si fuera una lista?
En el modo repeatable read de MySQL, una sola SELECT que seleccionaba una única fila me llegó a devolver un resultado imposible.
Era de la forma
SELECT min(value), max(value) FROM table WHERE id = 1;, yidera la clave primaria, perominymaxsalieron con valores distintos.Como referencia, este no es un problema específico de CONCAT. La razón para usar CONCAT es que permite razonar sobre la anomalía en tiempo lineal y no en tiempo exponencial.
El mismo tipo de comportamiento aparece también en registros simples de lectura/escritura.
Me gustó que el artículo cubriera AWS RDS, pero me pregunto si también hubo foco en AWS Aurora MySQL.
Para quienes no lo sepan, AWS creó una plataforma de base de datos compatible a nivel de protocolo que finge ser MySQL o PostgreSQL.
Sería interesante ver si Aurora MySQL tiene las mismas “características” que RDS o MariaDB.
Aun así, es un objetivo muy interesante, y como Aurora es una base de datos mucho más nueva, tengo la intuición de que podría tener problemas sutiles aún no descubiertos, más que el viejo MySQL.
Sin embargo, sí hay una molestia importante.
Los ingenieros de Plaid escribieron un buen artículo resumiendo las diferencias: https://plaid.com/blog/exploring-performance-differences-bet...
Para mí, la mayor diferencia es que los clústeres Aurora usan almacenamiento compartido, por lo que el modelo de aislamiento es ligeramente distinto.
read committed solo es posible configurando un parámetro a nivel de todo el clúster, y read uncommitted, según lo veo, no es posible.
Es un artículo muy interesante.
Muestra muy bien cuántos “sistemas que funcionan en la práctica” pueden construirse sobre una base que exhibe tantas anomalías de consistencia.
Me preocupa un poco la parte en la que dicen que, al tocarlo por apenas 5 minutos, la replicación de RDS se detuvo y no hubo ninguna alerta de health check fallido
Eso sí, tiende a trasladarle al usuario la carga de revisar más de 150 métricas y leer la documentación para encontrar las importantes
Además, en <https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_...> dicen que hay una celda en la tabla de la consola que muestra el estado de replicación, pero en la consola a menudo el usuario tiene que activar manualmente esa columna, lo cual no está bien
AWS se apoya bastante en lo que llama el “modelo de responsabilidad compartida”
Hay que hacerlo todo directamente desde dentro del host o del contenedor
El soporte de AWS/Rackspace solo dirá: “lo que corre dentro de un servicio de AWS no lo administramos nosotros, así que es problema del cliente”
Me gustó la parte de que en 2022 Jepsen encargó a INESC TEC de la Universidad de Porto el desarrollo de LazyFS
Un sistema de archivos FUSE que simula la pérdida de escrituras a las que no se les hizo
fsync: es un gran ejemplo de cómo empujar hacia adelante el nivel técnicoSELECT ... FOR UPDATEparece ser la respuesta a estos problemasSi bloqueas las filas que vas a actualizar, ¿no empieza de pronto todo a funcionar como se prometía?
Si quieres actualizar un registro en función de los datos de otro registro, tienes que hacer una lectura con bloqueo sobre ese otro registro, y probablemente también sobre el registro que vas a actualizar
Si actualizas un registro en función de otro con una sola consulta SQL, MySQL de todos modos bloqueará ambos
Si necesitas actualizar algo en función de múltiples objetivos, en mi experiencia es muy fácil que aparezcan deadlocks
En su lugar, conviene bloquear algo como un registro usado para bloqueo, luego hacer repeatable read sobre los datos que quieras y actualizar
El punto temporal de repeatable read no se fija hasta que se realiza una lectura consistente
SELECT ... FOR UPDATEno es una lectura consistente, así que funciona bien en situaciones de concurrencia sin bloquear decenas o cientos de filas con una actualización SQL normalEn mi experiencia, la mayoría de los desarrolladores ni siquiera considera el nivel de aislamiento y usa el valor por defecto
Cuando aparece una condición de carrera, dicen “qué raro” y siguen adelante
[1] https://news.ycombinator.com/item?id=38696421
Por eso es mejor que la mayoría de los desarrolladores no tenga que pensar por su cuenta en el nivel de aislamiento, y creo que MySQL y algunas otras bases de datos ofrecen muy pocas garantías para el desarrollador promedio