- Les tests Jepsen ont confirmé des cas où les clusters Amazon RDS for PostgreSQL Multi-AZ ne respectent pas la Snapshot Isolation, le niveau d’isolation le plus fort disponible à l’échelle de l’ensemble des nœuds
- La cause principale est que l’ordre de visibilité des transactions sur le primaire est déterminé par des verrous en mémoire, tandis que le secondaire suit l’ordre du WAL, et ces deux ordres peuvent diverger
- Même sans injection de panne ni failover, avec un stockage
gp3et des instancesdb.m6id.large, des cycles G-nonadjacent apparaissent toutes les quelques minutes à environ 150 write TPS / 1600 read-only TPS - L’anomalie correspond à un Long Fork et a été observée sur toutes les versions testées, de PostgreSQL 13.15 à 17.4 prises en charge par AWS ; aucun Short Fork / Write Skew n’a été constaté
- Pour les transactions critiques en matière de sécurité, l’usage de secondaires en lecture seule peut exposer une vision différente de l’ordre d’exécution ; il faut donc envisager d’utiliser uniquement le writer endpoint ou d’inclure au moins 1 écriture
Mise à jour sur la cause du Long Fork
- Sergey Melnik d’AWS ainsi que les participants des commentaires HN matashii et Ants Aasma ont identifié la cause du Long Fork dans les clusters PostgreSQL
- Le primaire PostgreSQL détermine l’ordre dans lequel les transactions deviennent visibles via des verrous en mémoire
- Le secondaire rend les transactions visibles selon leur ordre dans le Write-Ahead Log (WAL)
- Si l’ordre des verrous et l’ordre du WAL diffèrent, le primaire et le secondaire peuvent voir un ordre apparent différent pour les transactions
- Ce comportement avait déjà été abordé dans un message de la mailing list PostgreSQL en 2013, et Melnik a publié sur le blog AWS un article expliquant la visibilité des transactions dans les clusters PostgreSQL avec read replicas
- Jepsen recommande qu’AWS et PostgreSQL documentent ce problème en parallèle des travaux de correction
Niveaux d’isolation et architecture de RDS for PostgreSQL
- PostgreSQL est une base de données SQL open source généraliste qui fournit trois niveaux d’isolation transactionnelle via MVCC
Read UncommittedetRead Committedse comportent tous deux comme Read CommittedRepeatable Readn’offre pas en réalité un Repeatable Read, mais une Snapshot IsolationSerializablefournit la sérialisabilité
- Amazon RDS for PostgreSQL est le service AWS qui propose des clusters PostgreSQL managés
- Il automatise le provisioning, la gestion du stockage, la réplication, les sauvegardes et les mises à niveau
- Les déploiements Multi-AZ répartissent les nœuds de base de données sur plusieurs zones de disponibilité afin de réduire le risque de pannes corrélées
- RDS utilise une réplication synchrone afin de ne répondre qu’une fois la durabilité transactionnelle assurée à la fois sur le primaire et sur au moins une instance secondaire
- Deux URL parlant le protocole PostgreSQL sont fournies aux utilisateurs
- primary endpoint : pour les transactions en lecture-écriture
- reader endpoint : pour les transactions en lecture seule
- Le primary endpoint prend en charge tous les niveaux d’isolation PostgreSQL, mais le secondaire ne prend pas en charge Serializable
- Le niveau d’isolation le plus fort utilisable sur l’ensemble des nœuds est la Snapshot Isolation, que PostgreSQL appelle
Repeatable Read
Conception des tests
- Jepsen a adapté sa bibliothèque de test pour PostgreSQL à Amazon RDS for PostgreSQL, avec un petit programme wrapper
- À chaque itération de test, un cluster RDS est provisionné via l’API AWS CreateDBCluster
- Stockage :
gp3 - Instances :
db.m6id.large
- Stockage :
- Un nœud EC2 dédié à l’exécution des tests est lancé et reçoit le main endpoint ainsi que l’endpoint read-only du cluster RDS
- Aucune injection de panne n’a été effectuée et aucun failover n’a été déclenché
- La charge principale est composée de transactions manipulant des listes d’entiers uniques
- Chaque liste est stockée dans une seule row et encodée dans un champ
TEXTcontenant des valeurs séparées par des virgules - Les transactions lisent une liste par primary key ou ajoutent un entier unique à la liste via
CONCAT
- Chaque liste est stockée dans une seule row et encodée dans un champ
- Cette charge permet à Elle checker d’inférer les dépendances de flux de données entre transactions et de rechercher des cycles dans le graphe afin de vérifier plusieurs niveaux d’isolation
Observation de cycles G-nonadjacent
- Dans des conditions normales et avec une concurrence modérée, Amazon RDS for PostgreSQL 17.4 présente des cycles G-nonadjacent toutes les quelques minutes
- Une exécution de test de 2 minutes a atteint environ 150 write TPS et 1600 read-only TPS, avec un cycle de 4 transactions
- Le cycle d’exemple se compose de quatre transactions
T1,T2,T3,T4T1a ajouté9à la row 89 pour produire la liste[4 9], observée ensuite parT2T3a ajouté11à la row 90 pour produire la liste[11]T4a ajouté3à la row 90 et a lu la liste résultante[11, 3], écrasant la version deT3T2a observé l’ajout deT1sur la row 89, mais n’a pas vu l’ajout deT3sur la row 90- À l’inverse,
T4a observé l’ajout deT3sur la row 90, mais a manqué l’ajout deT1sur la row 89
- Ce cycle inclut une dépendance lecture-écriture entre transactions non adjacentes ; il s’agit donc d’un cycle G-nonadjacent, ce qui viole la Snapshot Isolation
- Dans le
Repeatable Readdu PostgreSQL standard, un tel comportement ne devrait pas se produire, et Jepsen ne l’a pas observé sur PostgreSQL standard
Pourquoi cela entre en conflit avec la Snapshot Isolation
- Avec la Snapshot Isolation, chaque transaction doit apparaître comme s’exécutant sur un snapshot de la base au timestamp de début
s - Les effets de la transaction deviennent ensuite visibles pour les autres au timestamp de commit
c - Si l’on exprime les observations du cycle d’exemple en relations entre timestamps, on obtient une contradiction
T2a lu l’ajout deT1, donc le début deT2doit être postérieur au commit deT1:c1 < s2T2n’a pas observé l’ajout deT3, doncs2 < c3T4a écrasé et observéT3, doncc3 < s4T4n’a pas observé l’ajout deT1, doncs4 < c1
- Ces relations ne peuvent pas toutes être vraies simultanément, ce qui entre en conflit avec le modèle temporel de la Snapshot Isolation
Long Fork et résultats selon les versions
- Ce cycle est aussi un exemple de Long Fork
- Les première et deuxième transactions constituent un premier fork d’état logique
- Les troisième et quatrième transactions constituent un deuxième fork
- Les deux forks mettent à jour des rows différentes mais n’observent pas les effets l’un de l’autre
- Aucun Short Fork, c’est-à-dire aucun Write Skew, n’a été observé
- Ces résultats suggèrent qu’Amazon RDS for PostgreSQL fournit peut-être une Parallel Snapshot Isolation, légèrement plus faible que la Snapshot Isolation
- Les anomalies G-nonadjacent observées incluent des cas reliés uniquement par des arêtes write-read ainsi que des cas impliquant plus de 4 transactions
- Le même type d’anomalie a été observé sur toutes les versions testées, de PostgreSQL 13.15, la plus ancienne version prise en charge par AWS, jusqu’à la 17.4, la plus récente
Points à vérifier côté utilisateur
- Puisqu’il existe des Long Fork et d’autres cycles G-nonadjacent, les clusters Amazon RDS for PostgreSQL Multi-AZ ne garantissent pas la Snapshot Isolation
- À ce titre, les clusters RDS for PostgreSQL Multi-AZ offrent des sémantiques de sûreté plus faibles qu’un PostgreSQL mono-nœud qui, lors d’un précédent test Jepsen, semblait fournir une Strong Snapshot Isolation
- Les utilisateurs peuvent examiner si la structure de leurs transactions est vulnérable au Long Fork, ou vérifier expérimentalement si les invariants visés sont bien préservés
- Les transactions de lecture peuvent voir des résultats différents de ceux d’autres transactions quant à l’ordre d’exécution des transactions
- L’anomalie semble liée aux requêtes adressées au secondaire read-only, ce qui laisse penser qu’il serait possible de rétablir la Snapshot Isolation par les moyens suivants
-
Utiliser uniquement le writer endpoint
- Inclure au moins 1 écriture dans toutes les transactions critiques pour la sécurité
- La validation Jepsen relève d’une approche expérimentale : elle peut prouver la présence d’un bug, mais pas son absence
- Ce rapport n’est pas le résultat d’une enquête exhaustive sur le fonctionnement de RDS for PostgreSQL, mais le produit d’une exploration préliminaire
-
1 commentaires
Avis sur Hacker News
J’aimerais que les textes dans le monde du logiciel soient plus souvent écrits ainsi : « Amazon RDS for PostgreSQL est un service d’Amazon Web Services (AWS) qui fournit des instances managées de bases de données PostgreSQL. Nous montrons que les clusters multi-AZ Amazon RDS for PostgreSQL violent l’isolation par instantané, le modèle de cohérence le plus fort pris en charge sur tous les endpoints… »
C’est direct, centré sur l’essentiel, sans fioritures, et cela ressemble davantage à la façon dont les résultats de recherche sont partagés dans les autres domaines STEM. À une époque, j’aimais les billets de blog spirituels qui expliquaient les choses avec des mèmes, mais aujourd’hui les textes sobres et simples me manquent.
Quand j’écrivais des articles très techniques et approfondis, il y avait très peu de likes et de commentaires, au point qu’un Staff Engineer m’a dit qu’il vaudrait mieux « viser un public plus restreint ». À l’inverse, quand j’ai testé les débuts de Kubecost et écrit que ses recommandations ne réduisaient que peu les coûts tout en pouvant créer des problèmes de performance des conteneurs, l’article était assez technique, avec du CPU throttling et des cgroups, mais dès que j’y ai ajouté des mèmes, les gens ont adoré.
Plus tard, j’ai ajouté des mèmes à un article plus austère où je créais une petite bibliothèque externe Python en C, y accédais avec ctypes et comparais les allocations sur la pile et sur le tas, et j’ai obtenu un résultat similaire. Je n’aime pas cette tendance, mais si l’on veut toucher un large lectorat, je ne vois pas vraiment comment l’éviter. Jensen ne visait pas ce type de lecteurs, et cette écriture rigoureuse et pure mérite d’être saluée.
Ce n’est pas dans le titre et ce n’est pas non plus parfaitement clair dans l’article, mais ce problème se limite aux clusters multi-AZ, une fonctionnalité relativement récente de RDS. C’est différent des instances multi-AZ que beaucoup de gens connaissent.
Les instances multi-AZ sont une ancienne fonctionnalité où la base de données primaire est répliquée de façon synchrone vers une base secondaire dans une autre zone de disponibilité, et où RDS bascule vers la secondaire si la primaire échoue.
Les clusters multi-AZ ont deux secondaires, et les transactions sont répliquées de façon synchrone vers au moins l’une d’elles. C’est plus robuste que les instances multi-AZ lorsqu’un secondaire échoue ou voit ses performances se dégrader, et cela permet aussi un accès en lecture seule aux secondaires.
Cela dit, les clusters multi-AZ doivent contenir davantage de magie supplémentaire qui ne fait pas partie des fonctionnalités de base de PostgreSQL, et c’est probablement pour cela qu’ils ont échoué aux tests Jepsen.
Cela dit, PostgreSQL présente encore un défaut qui peut permettre un problème similaire à ce schéma. Une transaction non répliquée dont le client disparaît pendant le commit devient immédiatement visible. Dans l’exemple, si T1 se produit sur un leader isolé et que la connexion se coupe pendant le commit, que T2 se produit également sur le nœud isolé, puis que T3/T4 se produisent plus tard sur le nouveau leader, on peut observer le même résultat. Mais cela ne correspond pas très bien à l’explication selon laquelle aucun fault injection n’a été effectué dans ce test.
Correction : je n’avais pas vu que ce schéma était expliqué par une divergence d’ordre des commits entre la réplique et le nœud primaire. C’est un peu gênant, car j’ai déjà présenté une méthode pour corriger ce problème.
Bonne enquête. De nos jours, beaucoup de développeurs logiciel ne connaissent même pas bien les transactions elles-mêmes, et connaissent encore moins les différents modèles de transaction. J’ai même vu des développeurs CRUD qualifiés de « développeurs seniors » qui ne savaient absolument rien des transactions de base de données.
En pratique, quand il y a du trafic à une certaine échelle et que le logiciel résout un problème non trivial, les transactions et les modèles de transaction sont essentiels pour les performances et pour écrire du code sans erreurs.
Par exemple, sur un gros projet, après beaucoup d’analyses, nous sommes passés du Read Committed par défaut de SQL Server à Read Committed Snapshot Isolation, et la contention sur les verrous a largement disparu, ce qui a beaucoup satisfait les utilisateurs. Les ingénieurs logiciel de ce projet utilisaient beaucoup les transactions, mais avant qu’on leur en enseigne les bases, ils ne connaissaient absolument rien aux modèles de transaction ni aux verrous.
Comme je travaille surtout dans le retail, je vois souvent des systèmes remplis d’erreurs assimilables à des conditions de concurrence, et c’est d’autant plus regrettable que ces niveaux d’isolation pourraient beaucoup aider dans ce domaine.
Cela dit, j’ai surtout vu ce genre de cas chez des ingénieurs de startup ; les développeurs Oracle/MSSQL typiques des grandes entreprises, eux, ont au moins les bases, ce que j’apprécie plutôt beaucoup.
À quelques reprises dans ma carrière, j’ai vu cette approche produire de très mauvais résultats.
La seule chose à laquelle faire attention lors de cette transition, c’est que tout le code qui dépend des lectures bloquantes casse. Par exemple, du code comme
select with existsdoit être réécrit avec des verrous explicites ou une autre méthode.Dans une ancienne entreprise, lorsque nous avons modifié la commande
pg_dumpde notre script de sauvegarde pour commencer à utiliser des workers parallèles (le flag-j), nous avons rarement vu, lors de la restauration, des erreurs suggérant des incohérences, comme des erreurs de clés dupliquées et des erreurs de contraintes de clés étrangères.À l’époque, nous avions essayé de le signaler à AWS et à la liste de diffusion PostgreSQL, mais comme nous n’arrivions pas à le reproduire facilement, cela n’a pas avancé, et nous avons fini par abandonner et revenir à des dumps monothread. Je me demande si le phénomène que nous avions observé à l’époque est lié à ce problème.
À lire cet article, l’impact réel semble être que des données obsolètes peuvent être renvoyées lorsqu’une lecture intervient rapidement juste après une écriture sur la même ligne. La transaction d’écriture est marquée comme terminée, mais avant que toute la couche distribuée de l’instance RDS multi-AZ soit complètement mise à jour ; si l’on relit immédiatement la même ligne, elle peut ne pas encore exister ou ses colonnes peuvent ne pas être entièrement mises à jour, et l’ancienne valeur peut ressortir.
Vu le fonctionnement des snapshots de PostgreSQL, cela ne semble pas vouloir dire qu’on puisse lire des valeurs absurdes parce que seuls certains octets d’un type de colonne multioctet auraient été mis à jour.
Au final, cela ressemble à une condition de concurrence qui converge avec le temps. Ou bien je me demande si quelqu’un a compris cela comme signifiant que les transactions ultérieures du « long fork » pourraient, même en situation normale, ne jamais se terminer.
La phrase « Ce travail a été réalisé indépendamment par Jepsen, sans compensation » n’est pas quelque chose qu’une partie prenante d’un RDBMS a envie de voir, même les bons jours. J’imagine qu’en interne, quelques e-mails inquiets ont dû circuler. Comme toujours, respect à aphyr.
Il n’est pas tout à fait clair si ce n’est pas un problème dans un cluster PostgreSQL upstream multi-instance. Je me demande s’il est correct de comprendre qu’AWS fait quelque chose dans la configuration du cluster, ou a ajouté un patch qui déclenche ce comportement
La réplication PostgreSQL se fait généralement de nombreuses façons, avec des résultats différents. Par exemple, il y a le rapport de Bin Wang sur Patroni : https://www.binwang.me/2024-12-02-PostgreSQL-High-Availabili...
Ce qui a également été constaté ici, c’est que PostgreSQL ne fournit actuellement pas de comportement de snapshot cohérent entre le nœud primaire et les réplicas. La transaction en lecture seule T2 s’est probablement exécutée sur un nœud secondaire, tandis que les transactions de modification T1/T3/T4 se sont probablement exécutées sur le nœud primaire
Pour le contexte, le snapshot d’un nœud PostgreSQL secondaire s’appuie, pour déterminer quelles transactions sont visibles, sur l’ordre de persistance des transactions, c’est-à-dire la position de l’enregistrement de commit dans le WAL. En revanche, l’ordre de visibilité sur le nœud primaire est déterminé par le moment où le backend qui a approuvé cette transaction reçoit pour la première fois la notification que la transaction est entièrement commitée, puis par le moment où il marque le commit
Sur le nœud primaire comme sur le nœud secondaire, l’ordre de commit est cohérent entre les backends connectés, mais l’ordre de commit peut différer quelque peu entre le primaire et le secondaire. Des travaux sont en cours pour améliorer cela, mais ils en sont encore à un stade très actif
AWS semble avoir patché PostgreSQL pour répliquer sur deux instances et considérer que c’est suffisant dès que l’une des deux confirme les modifications. Le moment exact où cette confirmation se produit n’est pas une information publique
Personnellement, je pense que, pour PostgreSQL, une réplication au niveau du système de fichiers à la drbd est préférable. Les anciennes instances AWS Multi-AZ fonctionnaient probablement de cette manière. En revanche, le débit baisse et il n’est pas possible de lire depuis l’instance secondaire
En particulier ce passage : https://youtu.be/fLqJXTOhUg4?t=434
Le titre soumis pose la question centrale. RDS for PostgreSQL 17.4 n’implémente pas correctement l’isolation par snapshot
Les discussions peuvent devenir assez animées : le titre est-il trop dur envers le système, trop favorable, reflète-t-il le problème le plus significatif parmi la douzaine découverte, est-il équitable au regard de l’objectif de Jepsen d’être un intermédiaire honnête sur les résultats de sûreté des bases de données, comment sera-t-il interprété dans dix ans quand les gens continueront à le partager alors qu’il ne s’appliquera plus aux versions récentes, etc.
Après quelques tentatives frustrantes, nous évitons ce problème avec une politique consistant à intituler tous les rapports sous la forme « Jepsen: ». Si HN veut un texte de lien plus explicatif ou plus coloré, il peut bien sûr le choisir lui-même
Cela dit, comme c’est écrit par Kyle Kingsbury, le Chuck Norris des garanties transactionnelles, AWS doit répondre ou clarifier. Même si cela semble ne s’appliquer qu’aux clusters multi-AZ, l’une des deux options de RDS pour PostgreSQL. Un déploiement Multi-AZ peut avoir une ou deux instances de base de données de secours ; ici, il s’agit de la configuration avec deux instances de base de données de secours
La documentation AWS ne promet rien de tel. Même le manuel RDS de 5 494 pages ne mentionne guère isolation ou serializable, sauf dans la documentation des paramètres propres à chaque moteur
Rien non plus sur la cohérence globale des lectures dans les clusters Multi-AZ. Il est bien dit qu’en réplication semi-synchrone, le writer attend la confirmation des enregistrements de log par un standby, mais deux readers peuvent se trouver sur des snapshots différents
[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...
Je me demande quels bugs de sûreté ou bugs au niveau applicatif peuvent apparaître si un développeur suppose une isolation par instantané, alors qu’Amazon RDS for PostgreSQL ne fournit en réalité qu’une isolation par instantanés parallèles, en particulier dans une configuration multi-AZ utilisant un endpoint de réplica en lecture.
git push. On démarre une transaction, on lit l’état actuel, on vérifie qu’il correspond à l’état attendu, on écrit le nouvel état, puis on commit avec le hash du nouvel état. Dans un scénario malchanceux, on peut obtenir un hash de commit qui ne correspond à aucun état valide.Le simple fait que ce genre de situation soit difficile à raisonner rend le problème difficile à éviter. La solution la plus simple serait donc sans doute quelque chose comme : si une écriture est conditionnée par une lecture, « utiliser uniquement l’endpoint writer pourrait permettre de retrouver l’isolation par instantané ».
Cela dit, il est surprenant que l’approche « utiliser uniquement l’endpoint writer » n’ait pas été testée, surtout en cas de perte de disponibilité.
User1 poste un commentaire, puis User2 poste un commentaire ; ensuite, dans une transaction séparée, User1 constate qu’il n’y a qu’un seul commentaire et reçoit le badge. User2 peut faire la même vérification dans une transaction séparée, ne voir que son propre commentaire et recevoir lui aussi le badge.
Avec une isolation par instantané, ce serait impossible. Au moins l’une des transactions séparées devrait voir les deux commentaires.
L’article original sur les instantanés parallèles vaut aussi la lecture : https://scispace.com/pdf/transactional-storage-for-geo-repli...
En lisant la phrase « ce phénomène s’est produit sur toutes les versions testées, de la 13.15 à la 17.4 », je me suis demandé si monter de version majeure avait été un mauvais choix, mais apparemment non. Ce n’est pas une régression, cela ressemble plutôt à une demande de fonctionnalité ou à un vieux bug.