- En pruebas de Jepsen se confirmó que los clústeres Amazon RDS for PostgreSQL Multi-AZ presentan casos en los que no respetan Snapshot Isolation, el nivel de aislamiento más fuerte disponible en todos los nodos
- La causa principal es que el orden de visibilidad de las transacciones en el primary se define con bloqueos en memoria, mientras que en los secondary sigue el orden del WAL, y ambos órdenes pueden desalinearse
- Incluso sin inyección de fallas ni failover, usando almacenamiento
gp3e instanciasdb.m6id.large, aparecen ciclos G-nonadjacent cada pocos minutos con alrededor de 150 write TPS / 1600 read-only TPS - La anomalía corresponde a Long Fork y apareció en todas las versiones probadas, desde PostgreSQL 13.15 hasta 17.4 soportadas por AWS; no se observaron Short Fork/Write Skew
- En transacciones críticas para la seguridad, usar un secondary de solo lectura puede mostrar un orden de ejecución distinto, por lo que conviene evaluar usar solo el writer endpoint o incluir al menos 1 write
Actualización sobre la causa de Long Fork
- Sergey Melnik de AWS y los participantes de HN matashii y Ants Aasma identificaron la causa de Long Fork en clústeres PostgreSQL
- El primary de PostgreSQL decide el orden en que las transacciones se vuelven visibles mediante bloqueos en memoria
- Los secondary hacen visibles las transacciones según el orden dentro del Write-Ahead Log (WAL)
- Si el orden de los bloqueos y el del WAL difieren, primary y secondary pueden ver en distinto orden aparente las transacciones
- Este comportamiento ya se había tratado en una publicación de la mailing list de PostgreSQL en 2013, y Melnik escribió en el blog de AWS un artículo sobre la visibilidad de transacciones en clústeres PostgreSQL y read replicas
- Jepsen recomienda que AWS y PostgreSQL documenten este problema junto con el trabajo para corregirlo
Niveles de aislamiento y arquitectura de RDS for PostgreSQL
- PostgreSQL es una base de datos SQL open source de propósito general que, mediante MVCC, ofrece tres niveles de aislamiento transaccional
Read UncommittedyRead Committedoperan ambos como Read CommittedRepeatable Readen realidad no ofrece Repeatable Read, sino Snapshot IsolationSerializableofrece Serializability
- Amazon RDS for PostgreSQL es un servicio de AWS que ofrece clústeres PostgreSQL administrados
- Automatiza aprovisionamiento, gestión de almacenamiento, replicación, respaldos, upgrades, etc.
- Los deployments Multi-AZ distribuyen nodos de base de datos entre varias zonas de disponibilidad para reducir la probabilidad de fallas correlacionadas
- RDS usa replicación síncrona para responder solo después de garantizar la durabilidad de la transacción tanto en el primary como en al menos una instancia secondary
- A los usuarios se les entregan dos URL que hablan el protocolo wire de PostgreSQL
- primary endpoint: para transacciones de lectura-escritura
- reader endpoint: para transacciones de solo lectura
- El primary endpoint soporta todos los niveles de aislamiento de PostgreSQL, pero los secondary no soportan Serializable
- El nivel de aislamiento más fuerte que puede usarse en todos los nodos es Snapshot Isolation, al que PostgreSQL llama
Repeatable Read
Diseño de la prueba
- Jepsen adaptó su biblioteca de pruebas para PostgreSQL a Amazon RDS for PostgreSQL y usó un pequeño wrapper program
- En cada ronda de prueba, aprovisionó un clúster RDS con la API CreateDBCluster de AWS
- almacenamiento
gp3 - instancias
db.m6id.large
- almacenamiento
- Se levantó un nodo EC2 para ejecutar las pruebas y se le dieron el main endpoint y el endpoint de solo lectura del clúster RDS
- No se hizo inyección de fallas ni se disparó failover
- La carga principal consistió en transacciones sobre listas de enteros únicos
- Cada lista se guardó en una sola row y se codificó en un campo
TEXTcon valores separados por comas - Las transacciones leen listas por primary key o agregan enteros únicos a la lista con
CONCAT
- Cada lista se guardó en una sola row y se codificó en un campo
- Con esta carga, el Elle checker puede inferir dependencias de flujo de datos entre transacciones y encontrar ciclos en el grafo para validar varios niveles de aislamiento
Observación de ciclos G-nonadjacent
- Aun en condiciones normales y con concurrencia moderada, Amazon RDS for PostgreSQL 17.4 muestra ciclos G-nonadjacent cada pocos minutos
- Una ejecución de prueba de 2 minutos logró cerca de 150 write TPS y 1600 read-only TPS, e incluyó un ciclo de 4 transacciones
- Un ciclo de ejemplo está compuesto por cuatro transacciones:
T1,T2,T3,T4T1agrega9a la row 89 y crea la lista[4 9], queT2observaT3agrega11a la row 90 y crea la lista[11]T4agrega3a la row 90 y, al leer la lista resultante[11, 3], sobrescribe la versión deT3T2observa en la row 89 el append deT1, pero no ve en la row 90 el append deT3- En cambio,
T4observa en la row 90 el append deT3, pero se pierde el append deT1en la row 89
- Este ciclo incluye una dependencia read-write entre transacciones no adyacentes, por lo que es un ciclo G-nonadjacent y una violación de Snapshot Isolation
- En
Repeatable Readde PostgreSQL estándar este comportamiento no debería ocurrir, y Jepsen no lo observó en PostgreSQL estándar
Por qué entra en conflicto con Snapshot Isolation
- En Snapshot Isolation, toda transacción debe parecer ejecutarse sobre un snapshot de la base de datos tomado en el timestamp inicial
s - Los efectos de la transacción se vuelven visibles para otras transacciones en un timestamp de commit posterior
c - Si se expresan las observaciones del ciclo de ejemplo como relaciones entre timestamps, aparece una contradicción
- Como
T2leyó el append deT1, el inicio deT2debe ser posterior al commit deT1:c1 < s2 - Como
T2no observó el append deT3, se cumples2 < c3 - Como
T4sobrescribió y observó aT3, se cumplec3 < s4 - Como
T4no observó el append deT1, se cumples4 < c1
- Como
- Todas esas relaciones no pueden cumplirse al mismo tiempo, lo que entra en conflicto con el modelo temporal de Snapshot Isolation
Long Fork y resultados por versión
- El ciclo también es un ejemplo de Long Fork
- La primera y la segunda transacción forman un fork de un mismo estado lógico
- La tercera y la cuarta transacción forman un segundo fork
- Ambos forks actualizan rows distintas, pero no alcanzan a observar los efectos entre sí
- No se observó Short Fork, es decir, Write Skew
- Esto sugiere que Amazon RDS for PostgreSQL podría estar ofreciendo Parallel Snapshot Isolation, un nivel apenas más débil que Snapshot Isolation
- Las anomalías G-nonadjacent aparecieron en varias formas, incluyendo casos conectados solo por edges write-read y casos con más de 4 transacciones
- El mismo tipo de anomalía apareció en todas las versiones probadas, desde PostgreSQL 13.15, la más antigua soportada por AWS, hasta la versión más reciente 17.4
Puntos que los usuarios deberían revisar
- Dado que existen Long Fork y otros ciclos G-nonadjacent, los clústeres Amazon RDS for PostgreSQL Multi-AZ no garantizan Snapshot Isolation
- En ese sentido, estos clústeres ofrecen una semántica de seguridad más débil que PostgreSQL de nodo único, que en pruebas previas de Jepsen parecía ofrecer Strong Snapshot Isolation
- Los usuarios pueden revisar si la estructura de sus transacciones es vulnerable a Long Fork, o validar experimentalmente si se conservan las invariantes deseadas
- Las transacciones de lectura pueden ver resultados distintos sobre el orden de ejecución de otras transacciones
- Como la anomalía parece estar relacionada con consultas a secondary de solo lectura, Snapshot Isolation podría recuperarse de estas formas
-
usar solo el writer endpoint
- incluir al menos 1 write en todas las transacciones críticas para la seguridad
- La validación de Jepsen es un enfoque experimental; puede demostrar la existencia de bugs, pero no su ausencia
- Este informe no es el resultado de una investigación detallada del funcionamiento interno de RDS for PostgreSQL, sino de una exploración preliminar
-
1 comentarios
Opiniones de Hacker News
Ojalá los textos del mundo del software fueran más seguido así: “Amazon RDS for PostgreSQL es un servicio de Amazon Web Services (AWS) que ofrece instancias administradas de bases de datos PostgreSQL. Mostramos que los clústeres multi-AZ de Amazon RDS for PostgreSQL violan el aislamiento de instantáneas, el modelo de consistencia más fuerte admitido en todos los endpoints…”
Es directo, va al punto y no tiene adornos, parecido a la forma en que se comparten resultados de investigación en otros campos STEM. Hubo una época en que me gustaban los posts ingeniosos explicados con memes, pero ahora extraño los textos sencillos y simples.
Cuando escribía posts técnicos muy profundos, casi no recibían likes ni comentarios, al punto de que un Staff Engineer me dijo que “sería mejor acotar más el público objetivo”. En cambio, mientras probaba una versión temprana de Kubecost, escribí que sus recomendaciones ahorraban poco costo y podían generar problemas de rendimiento en contenedores; aunque era un texto bastante técnico sobre CPU throttling y cgroups, al meterle memes a la gente le encantó.
Más adelante hice una pequeña biblioteca externa para Python en C, accediéndola con ctypes y comparando asignación en stack/heap; también le puse memes a ese texto más seco y el resultado fue parecido. No me gusta esta tendencia, pero tampoco sé bien cómo evitarla si se quiere llegar a un público amplio. Jensen no apuntaba a ese público, y una escritura rigurosa y pura merece aplausos.
Aunque no está en el título y tampoco queda clarísimo en el texto, este problema se limita a una funcionalidad relativamente nueva de RDS: los clústeres multi-AZ. No es lo mismo que las instancias multi-AZ con las que mucha gente está familiarizada.
Las instancias multi-AZ son una funcionalidad antigua en la que la base de datos principal se replica de forma síncrona a una base de datos secundaria en otra zona de disponibilidad, y si la principal falla, RDS hace failover a la secundaria.
En los clústeres multi-AZ hay dos secundarias y las transacciones se replican de forma síncrona al menos a una de ellas. Son más robustos que las instancias multi-AZ cuando una secundaria falla o tiene bajo rendimiento, y además permiten acceso de solo lectura a las secundarias.
Dicho eso, los clústeres multi-AZ probablemente tengan más magia adicional interna que no forma parte de las funciones básicas de PostgreSQL, y quizá por eso fallaron en las pruebas de Jepsen.
Aun así, PostgreSQL todavía tiene un defecto que permite un problema parecido a este patrón. Una transacción no replicada cuyo cliente desaparece durante el commit se vuelve visible de inmediato. En el ejemplo, si T1 ocurre en un líder aislado y la conexión se corta durante el commit, T2 también ocurre en el nodo aislado y T3/T4 ocurren después en el nuevo líder, se puede ver el mismo resultado. Pero eso no encaja muy bien con la explicación de que en esta prueba no hicieron fault injection.
Edición: no había visto el texto que explica este patrón por una discrepancia en el orden de commits entre la réplica y el nodo primario. Es un poco vergonzoso, porque alguna vez presenté una forma de corregir este problema.
Buena investigación. Hoy en día muchos desarrolladores de software ni siquiera entienden bien las transacciones en sí, y mucho menos los distintos modelos de transacción. Incluso he visto desarrolladores CRUD llamados “desarrolladores sénior” que no saben nada de transacciones de bases de datos
En la práctica, cuando hay cierto volumen de tráfico y el software resuelve problemas que no son triviales, las transacciones y los modelos de transacción son muy importantes para el rendimiento y para escribir código sin errores
Por ejemplo, en un proyecto grande, después de mucho análisis, se cambió el valor predeterminado de SQL Server, Read Committed, a Read Committed Snapshot Isolation, y la contención por bloqueos desapareció en gran medida, dejando muy satisfechos a los usuarios. Los ingenieros de software de ese proyecto usaban mucho las transacciones, pero hasta que se les enseñaron los fundamentos no sabían nada de modelos de transacción ni de bloqueos
Como trabajo principalmente en retail, suelo ver sistemas llenos de errores parecidos a condiciones de carrera, y da más lástima porque estos niveles de aislamiento podrían ayudar mucho en esos casos
Dicho eso, estos casos los vi sobre todo en ingenieros de startups; a los desarrolladores típicos de Oracle/MSSQL en empresas grandes los valoro bastante, porque al menos tienen bien cubiertos los fundamentos
Varias veces en mi carrera vi que este enfoque producía resultados realmente malos
Eso sí, al hacer esa transición hay que tener cuidado: se rompe todo el código que depende de lecturas bloqueantes. Por ejemplo, código como
select with existsdebe reescribirse con bloqueos explícitos u otro métodoEn una empresa anterior, cuando cambiamos el comando
pg_dumpdel script de backups para empezar a usar workers en paralelo (flag-j), durante la restauración veíamos rara vez errores que sugerían inconsistencias, como errores de clave duplicada y errores de restricciones de clave foráneaEn ese momento intenté reportarlo a AWS y a la lista de correo de PostgreSQL, pero como no era fácil reproducirlo no hubo avance, y al final nos rendimos y volvimos a dumps de un solo hilo. Me pregunto si el fenómeno que vi entonces está relacionado con este problema
Al leer este artículo, parece que el impacto real es que, cuando ocurre una lectura rápidamente después de una escritura sobre la misma fila, se pueden devolver datos obsoletos. La transacción de escritura aparece como completada, pero antes de que toda la capa distribuida de la instancia RDS multi-AZ se haya actualizado por completo, si se lee la misma fila de inmediato, es posible que la fila aún no exista o que las columnas no se hayan actualizado por completo, por lo que salga el valor anterior
Por la forma en que funcionan los snapshots de PostgreSQL, no parece significar que se actualicen solo algunos bytes de un tipo de columna multibyte y se lea un valor sin sentido
Al final parece una condición de carrera que converge con el tiempo. O me pregunto si alguien lo leyó como que las transacciones posteriores del “long fork” podrían no completarse nunca incluso en situaciones normales
La frase “este trabajo fue realizado de forma independiente por Jepsen, sin compensación” no es algo que ninguna parte interesada en un RDBMS quiera ver ni en un buen día. Me imagino que internamente habrán circulado varios correos con preocupación. Como siempre, mis respetos a aphyr
No está del todo claro si esto no es un problema en un clúster PostgreSQL upstream multi-instancia. Me pregunto si es correcto entender que AWS hace algo en la configuración del clúster o agregó un parche que provoca este comportamiento.
En general, la replicación de PostgreSQL puede hacerse de varias maneras y los resultados también varían. Por ejemplo, está el informe de Bin Wang sobre Patroni: https://www.binwang.me/2024-12-02-PostgreSQL-High-Availabili...
Lo que también se descubrió aquí es que PostgreSQL actualmente no ofrece un comportamiento de snapshots consistente entre el nodo primario y las réplicas. Probablemente la transacción de solo lectura T2 se ejecutó en un nodo secundario, y las transacciones de modificación T1/T3/T4 en el nodo primario.
Como contexto, el snapshot de un nodo secundario de PostgreSQL depende del orden de durabilidad de las transacciones, es decir, de la posición del registro de commit en el WAL, para determinar qué transacciones son visibles. En cambio, en el nodo primario, el orden de visibilidad lo determina el momento en que el backend que confirmó esa transacción recibe por primera vez la notificación de que la transacción se confirmó por completo y luego marca el commit.
Dentro del nodo primario y dentro de cada nodo secundario, el orden de commit entre los backends conectados es consistente, pero entre el primario y el secundario el orden de commit puede diferir un poco. Hay trabajo en curso para mejorar esto, pero todavía está en una etapa muy activa.
AWS parece haber parcheado PostgreSQL para replicar en dos instancias y considerar suficiente que una de las dos confirme el cambio. No hay información pública sobre cuándo ocurre esa confirmación.
Personalmente, creo que para PostgreSQL es mejor una replicación a nivel de sistema de archivos como drbd. Las antiguas instancias Multi-AZ de AWS probablemente funcionaban de esa manera. Eso sí, baja el throughput y no se puede leer desde la instancia secundaria.
En particular, este punto: https://youtu.be/fLqJXTOhUg4?t=434
El título enviado apunta al punto central. RDS for PostgreSQL 17.4 no implementa correctamente el aislamiento por snapshot.
Las discusiones pueden ponerse bastante intensas: si el título es demasiado duro con el sistema, demasiado favorable, si refleja el problema más significativo entre la docena larga que se encontraron, si es justo según el estándar de Jepsen de intentar ser un intermediario honesto en resultados de seguridad de bases de datos, o cómo se interpretará dentro de 10 años cuando la gente siga enlazándolo pero ya no aplique a las versiones recientes.
Después de algunos intentos frustrantes, están evitando este problema con la política de titular todos los informes con el formato “Jepsen: ”. Si HN quiere un texto de enlace más descriptivo o más vistoso, por supuesto puede elegirlo directamente.
Aun así, esto lo escribió Kyle Kingsbury, una especie de Chuck Norris de las garantías transaccionales, así que AWS debería responder o aclararlo. Incluso si parece aplicarse solo a una de las dos opciones de RDS para PostgreSQL, los clústeres multi-AZ. Las implementaciones Multi-AZ pueden tener una o dos instancias de base de datos en espera; aquí se trata de la configuración con dos instancias de base de datos en espera.
La documentación de AWS no hace esa promesa. El manual de RDS, de 5494 páginas, apenas menciona isolation o serializable en la documentación de parámetros de cada motor.
Tampoco dice nada sobre la consistencia global de lectura en clústeres Multi-AZ. Aunque la replicación es semisíncrona y el writer espera la confirmación de los registros de log por parte de un standby, dos readers podrían estar sobre snapshots distintos.
[1] - "New Amazon RDS for MySQL & PostgreSQL Multi-AZ Deployment Option: Improved Write Performance & Faster Failover" - https://aws.amazon.com/blogs/aws/amazon-rds-multi-az-db-clus...
[2] - "Amazon RDS Multi-AZ with two readable standbys: Under the hood" - https://aws.amazon.com/blogs/database/amazon-rds-multi-az-wi...
Si un desarrollador asumió aislamiento de instantáneas, pero Amazon RDS for PostgreSQL en realidad solo ofrece aislamiento de instantáneas en paralelo, me pregunto qué bugs de seguridad o bugs a nivel de aplicación podrían surgir, especialmente en configuraciones multi-AZ que usan endpoints de réplicas de lectura.
git push. Se inicia una transacción, se lee el estado actual, se verifica si coincide con el estado esperado, se escribe el nuevo estado y se hace commit junto con el hash del nuevo estado. En una situación de mala suerte, podría generarse un hash de commit que no coincida con ningún estado válido.El hecho de que sea difícil razonar sobre esto en sí mismo hace que sea difícil evitar el problema. Por eso, la solución más sencilla probablemente sea algo parecido a: si una escritura depende de una lectura, “quizá se pueda recuperar el aislamiento de instantáneas usando solo el endpoint de writer”.
Sin embargo, sorprende que el método de “usar solo el endpoint de writer” no haya sido probado, especialmente en situaciones de pérdida de disponibilidad.
User1 deja un comentario, luego User2 deja otro, y después User1, en una transacción separada, verifica que solo hay 1 comentario y recibe la insignia. User2 también puede hacer la misma verificación en una transacción separada, ver solo su propio comentario y recibir la insignia.
Con aislamiento de instantáneas, esto no es posible. Al menos una de las transacciones separadas tendría que ver 2 comentarios.
También vale la pena leer el paper original sobre instantáneas en paralelo: https://scispace.com/pdf/transactional-storage-for-geo-repli...
Al leer la frase “este fenómeno ocurrió en todas las versiones probadas, desde la 13.15 hasta la 17.4”, me preocupó que haber subido la versión mayor hubiera sido una mala decisión, pero parece que no es así. Esto no es una regresión, sino más bien una solicitud de funcionalidad o un bug antiguo.