3 puntos por GN⁺ 2023-12-20 | 1 comentarios | Compartir por WhatsApp
  • 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 que DELETE o UPDATE pueden 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 SELECT tambié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 text con valores separados por comas, y los append se hacen con SQL CONCAT
    • 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 lee name, actualiza gender y luego vuelve a leer name
    • Si name cambia entre ambas lecturas, hay una violación de Repeatable Read
    • El workload de Monotonic Atomic View usa el value de dos rows
    • El writer incrementa el value de la row 0 y luego incrementa la row 1
    • El reader lee la row 0, actualiza el noop de 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
  • 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 de 7 y al volver a leer observó [1 2 3 4 5 6 7]
    • En el workload dirigido, una transacción Repeatable Read leyó name como "pebble", actualizó gender a "femme" y, al volver a leer el mismo name, 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 0 en la row 0, luego ve el incremento 1 del writer en la row 1 y después vuelve a ver 0 en 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_order sigue siendo un factor sospechoso
    • En MySQL 8.0.27 y posteriores, replica_preserve_commit_order=ON es 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

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 COMMITTED con técnicas de bloqueo como SELECT ... 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_order en ON en 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 DATABASE exitoso 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

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

    • No creo que la gente razone bien sobre read committed.
      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
    • read uncommitted está bien para estadísticas agregadas, pero para eso es mejor enviar los datos a ClickHouse.
    • Las consultas de snapshot de solo lectura son muy útiles en sistemas reales.
    • Si repeatable read realmente funcionara bien, no haría falta bloquear filas.
  • 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...

    • El ponente es un developer advocate que trabaja en YugabyteDB, y me da curiosidad cómo se conecta esto con el trabajo de Kyle.
  • 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;, y id era la clave primaria, pero min y max salieron con valores distintos.

  • 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.

    • Aurora es un motor de base de datos completamente distinto, así que sus problemas de concurrencia también son distintos; por eso probablemente no se cubrió aquí.
      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.
    • Uso bastante MySQL Aurora y, para nuestro caso, aunque el volumen de uso es muy alto, los patrones de consulta son simples, así que no se notan grandes diferencias.
      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.

    • La mayoría de los sistemas están rotos en la práctica y siguen funcionando a base de correcciones humanas.
  • 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

    • Los detalles importan, y con solo el screencast es casi imposible diagnosticar el problema, pero en mi experiencia AWS por lo general ofrece CloudWatch Metrics bastante abundantes
      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”
    • Puedo asegurar que no se debería confiar en ningún health check de AWS como alerta primaria ante una caída
      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écnico

  • SELECT ... FOR UPDATE parece ser la respuesta a estos problemas
    Si bloqueas las filas que vas a actualizar, ¿no empieza de pronto todo a funcionar como se prometía?

    • En general, las operaciones que bloquean filas tienden a “fijar” la existencia de los valores, independientemente de repeatable read
      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 UPDATE no es una lectura consistente, así que funciona bien en situaciones de concurrencia sin bloquear decenas o cientos de filas con una actualización SQL normal
    • Sí, si no te importa que el rendimiento quede completamente destruido
  • En 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

    • Me gustaría refutarlo, pero los exitosos primeros años de MongoDB lo demuestran bastante bien
    • Por eso dije que el nivel de aislamiento por defecto debería ser serializable
      [1] https://news.ycombinator.com/item?id=38696421
    • Los problemas de aislamiento son demasiado difíciles de razonar, así que casi todo lo que esté por debajo de la consistencia serializable termina pasándote factura de varias maneras
      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
    • En mi experiencia, casi ningún desarrollador considera siquiera la consistencia en sí