1 points par GN⁺ 1 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Le verrou exclusif global de Postgres LISTEN/NOTIFY limite le débit des implémentations simples, mais en mettant les notifications en mémoire tampon pour les envoyer par lots, un seul serveur peut traiter jusqu’à 60 000 écritures de flux par seconde
  • Une transaction qui appelle NOTIFY conserve le verrou global jusqu’à la fin du commit et de fsync() afin de garantir l’ordre de commit des notifications ; cela sérialise les commits et empêche aussi de tirer parti du commit groupé
  • L’implémentation initiale, qui appelait NOTIFY via un déclencheur à chaque écriture dans la table de flux, offrait une faible latence, mais se heurtait à un goulot d’étranglement à 2 900 opérations par seconde sans utiliser pleinement le CPU, la mémoire ni les IOPS
  • En prenant la table de base de données, et non les notifications, comme source de vérité, puis en envoyant périodiquement les notifications accumulées en mémoire dans une seule transaction, on peut réduire fortement le nombre d’acquisitions de verrou
  • Le risque de perte des notifications en mémoire tampon en cas de défaillance du processus est compensé par un polling peu fréquent ; même avec des lectures concurrentes, l’approche atteint une latence de 15 à 100 ms et un débit 20 fois supérieur à l’existant

Des flux à faible latence avec Postgres

  • Un flux basé sur Postgres stocke chaque fragment de flux comme une nouvelle ligne dans la table streams ; un token de réponse de LLM peut aussi constituer un fragment
  • Côté lecture, il est difficile d’attendre efficacement avec une simple requête, car on ne sait pas quand le prochain fragment arrivera
  • Le polling périodique, avec un intervalle long, augmente la latence pour des usages interactifs comme le chat en ligne ; avec un intervalle court, les pollers concurrents submergent la base de données
  • Avec LISTEN/NOTIFY, le processus de lecture peut attendre en étant bloqué, puis être réveillé immédiatement par une notification signalant qu’un nouveau fragment a été écrit, évitant ainsi le polling inutile

Le goulot d’étranglement d’un NOTIFY à chaque écriture

  • Dans l’implémentation initiale, à chaque nouveau fragment écrit dans la table streams, un déclencheur exécutait une fonction pour envoyer un NOTIFY, et le processus de lecture attendait la notification avant de lire le nouveau fragment
  • L’exactitude et la faible latence étaient assurées, mais même avec une grande base Postgres, il n’était pas possible de soutenir plus de 2 900 écritures de flux par seconde
  • Pendant que le goulot d’étranglement se manifestait, l’utilisation du CPU, de la mémoire et des IOPS n’augmentait pas de façon notable ; la cause était le verrou global dans le chemin de commit de NOTIFY

Un verrou global pour garantir l’ordre des commits

  • Une transaction qui appelle NOTIFY acquiert un verrou exclusif global au début du commit et ne le libère qu’une fois entièrement commitée et son contenu écrit sur disque via fsync()
  • Postgres garantit que les notifications sont livrées dans l’ordre de commit des transactions, et stocke toutes les notifications sortantes dans une file interne globale qui doit correspondre exactement à cet ordre de commit
  • L’ajout des notifications à la file doit aussi être traité transactionnellement dans le cadre du commit, mais comme chaque transaction a un temps de commit différent, l’ordre ne peut pas être déterminé avant la fin du commit
  • Le verrou global sérialise le commit des transactions contenant des notifications afin de fixer l’ordre à l’avance et de les ajouter dans le même ordre à la file interne des notifications

Comment la sérialisation des commits limite le débit

  • Comme toutes les écritures de flux appellent NOTIFY via un déclencheur, chaque transaction d’écriture conserve le verrou global pendant tout le commit et le flush sur disque
  • Les transactions étant commitée les unes après les autres, il n’est pas possible d’exploiter le commit groupé de Postgres, qui traite plusieurs transactions avec un seul fsync()
  • Le débit ne peut pas dépasser la vitesse à laquelle Postgres commit des transactions individuelles ; les opérations attendent sur le verrou, si bien que le CPU et le disque ne sont pas pleinement utilisés
  • Le patch qui sera inclus dans Postgres 19 ne supprime pas le verrou global et ne résout donc pas ce goulot d’étranglement
    • Il optimise plutôt un cas limité où il existe de nombreux canaux de notification et où chaque listener n’attend qu’un canal précis

Mise en mémoire tampon et envoi par lots des notifications

  • Dans de nombreux usages de LISTEN/NOTIFY, y compris les flux, les notifications ne sont pas la source de vérité : elles ne sont qu’un signal indiquant qu’il faut consulter la table où les données réelles sont stockées
  • Dans cette architecture, les notifications elles-mêmes n’ont pas besoin d’un ordre global parfait ni d’une durabilité complète ; elles peuvent être mises en mémoire tampon puis envoyées périodiquement dans une seule transaction de lot
  • Le verrou global n’est acquis qu’au moment de vider le tampon, et non à chaque écriture de flux individuelle
  • Les écritures individuelles progressent rapidement, séparées de l’envoi des notifications en arrière-plan, et le débit augmente grâce aux optimisations de Postgres comme le commit groupé

Polling peu fréquent pour compenser la perte de notifications

  • Si le processus s’arrête alors que des notifications restent en mémoire, celles-ci peuvent ne pas être livrées
  • Le processus de lecture attend les notifications tout en interrogeant périodiquement la base de données pour vérifier s’il existe des données de flux écrites sans notification
  • Ce polling sert uniquement de mécanisme auxiliaire pour récupérer les notifications perdues ; il peut donc être exécuté à faible fréquence, avec un impact limité sur les performances

Débit et latence

  • L’implémentation optimisée traite jusqu’à 60 000 écritures de flux par seconde dans un environnement avec des processus de lecture concurrents, soit un débit 20 fois supérieur à celui de l’implémentation initiale
  • Même avec ce débit plus élevé, la latence reste dans une plage de 15 à 100 ms
  • Au débit maximal, le CPU de Postgres est pleinement utilisé, ce qui montre que l’on atteint une véritable saturation de la base de données elle-même, et non une contention sur les verrous
  • Le code complet du benchmark est disponible dans dbos-postgres-benchmark

1 commentaires

 
GN⁺ 1 시간 전
Avis sur Hacker News
  • La scalabilité est une échelle continue, pas une dichotomie. 60 000 événements par seconde peuvent représenter 100 000 fois plus que nécessaire pour certains systèmes, et 100 000 fois trop peu pour d’autres. Parmi les erreurs courantes des développeurs, je citerais davantage le choix d’une technologie dont les caractéristiques de scalabilité ne conviennent pas que « l’optimisation prématurée »
    Une technologie trop petite échoue clairement une fois ses limites dépassées, mais une technologie excessivement scalable apporte aussi une charge opérationnelle et des contraintes. Introduire ce type de technologie dans un petit système où un modèle plus riche pourrait fortement réduire l’effort de développement est aussi un mauvais choix
    Les limites de LISTEN/NOTIFY sont suffisamment basses pour mériter l’attention ; après avoir calculé une charge maximale pessimiste, il vaut donc mieux garder au moins une marge de 10x. Mais pour beaucoup de projets, c’est suffisant. Grâce à son intégration à la base de données, à sa disponibilité et au fait qu’il ne nécessite pas d’exploiter un service séparé, ce n’est pas une option à exclure d’emblée ; même les 2 000 événements par seconde évoqués auparavant représentent déjà beaucoup pour un système qui traite un message à l’échelle de la seconde

    • Petite objection à un bon article : il existe très peu de systèmes qui traitent 6 milliards de requêtes par seconde, et s’ils existent, ils utilisent probablement des outils construits sur mesure pour cet objectif
    • S’attendre à moins de 60 000 événements par seconde et grimper soudainement de 20 000 à 200 000 est un meilleur problème que de construire pour 1 million par seconde alors que la charge réelle est de 20 000. Dans le premier cas, ce succès inattendu finance les rustines temporaires et les coûts de montée en charge ; dans le second, on reste coincé avec une structure de coûts élevée et un investissement initial
      Il vaut mieux concevoir pour l’échelle réellement attendue avec une marge raisonnable, et ne viser au-delà que lorsque la scalabilité supplémentaire est pratiquement gratuite. Si quelques milliers de dollars suffisent pour acheter du matériel plus gros, ou si les options sont équivalentes hors scalabilité, autant choisir la plus grande
    • Dans ce cas, on atteint le maximum du matériel, donc on peut dire que ça scale. Le goulot d’étranglement n’est pas la base de données ni les entrées-sorties, mais le matériel ; au débit maximal, le CPU de Postgres est entièrement utilisé, ce qui montre une saturation de la base elle-même plutôt qu’une contention
  • Nous avons eu beaucoup de succès en combinant LISTEN/NOTIFY avec un broker d’abonnements GraphQL en Rust. Il y avait des dizaines de milliers d’abonnements, mais seulement une connexion LISTEN par hôte, soit 3 à 4 au total
    Toutes les modifications étaient envoyées à chaque hôte, puis l’hôte gérait les abonnements réels des utilisateurs et décidait quoi publier. Remplacer des centaines d’hôtes Ruby ou Node par quelques hôtes Rust peut fortement simplifier l’architecture, et des approches réputées ne pas scaler peuvent en réalité très bien fonctionner

    • Je serais curieux de connaître des bibliothèques GraphQL recommandables pour Rust
  • Quand j’étais CTO, nous sommes passés d’environ 100 000 traitements par jour sur l’ensemble des services à plusieurs millions, puis finalement à des dizaines de millions. Pendant cette période, un ingénieur a construit une file d’attente au-dessus de la sémantique LISTEN/NOTIFY afin de tirer parti de la forte cohérence avec le modèle de données. Ce n’était pas difficile à comprendre, et cela supprimait aussi une couche séparée de stockage et de transport ; à l’époque, cela semblait raisonnable
    Mais en faisant évoluer cette fonctionnalité maison, nous avons dû contourner le fonctionnement interne de PostgreSQL, ce qui est devenu très pénible, et nous aurions dû migrer plus tôt vers un autre système. La scalabilité était aussi mauvaise : sur RDS, nous avons rencontré une forte contention disque dont la cause était difficile à diagnostiquer, et le VACUUM de cette table était un cauchemar. Comme nous avions réimplémenté de façon inhabituelle une file d’attente familière à l’aide de fonctionnalités internes de PostgreSQL, les autres ingénieurs en avaient peur et évitaient le débogage comme la prise de responsabilité
    La leçon principale, au-delà des détails comme le schéma et les index, est de toujours commencer par des technologies simples et prévisibles. Si une cohérence très forte des données n’est pas indispensable, il vaut mieux utiliser une file simple au contrat d’API clair, comme SQS ou une file Redis, même si cela ajoute un composant d’infrastructure, puis adapter le reste autour. Moins le stockage de données principal assume de responsabilités mécaniques, mieux c’est

    • Au fond, on peut résumer cela à : « ça ne scale pas si on l’utilise sans comprendre son fonctionnement ». Le LISTEN/NOTIFY de base ne scale pas, mais l’article original a réellement trouvé une manière de le faire scaler, donc chaque équipe n’a pas besoin de résoudre à nouveau le problème
      À mesure que l’écosystème des systèmes distribués progresse, on comprend ce que chaque composant peut faire. Commencer avec peu de composants et n’en ajouter que lorsque c’est réellement nécessaire produit des systèmes plus sains
    • Il est rare qu’un nouveau composant domine tous les autres facteurs, mais exploiter une file indépendamment des autres composants d’architecture apporte de gros avantages. Une file est conçue dès le départ pour stocker puis transmettre ; découpler son cycle de vie permet aussi de découpler les autres systèmes pendant les mises à jour, l’analyse d’incidents et les fenêtres de patch
    • Cela relève davantage d’un problème de gestion, et je ne suis pas d’accord avec la conclusion selon laquelle il faut ajouter un nouveau composant à l’infrastructure. Ajouter un nouveau nœud réseau parce que des développeurs ne veulent pas s’occuper d’une partie de la stack n’est pas justifié ; il faut simplement leur en confier la responsabilité
  • J’apprécie toujours autant DBOS, qui exploite correctement Postgres et désormais SQLite. On peut l’introduire dans une stack CRUD existante avec presque aucun effort
    Une fois qu’on commence à utiliser des workflows durables, on voit sans cesse de nouveaux endroits où les appliquer. Dernièrement, j’expérimente l’idée de considérer chaque e-mail comme un workflow durable, où l’utilisateur, son interlocuteur, des agents et des outils comme GitHub ou Attio participent tour à tour au flux
    https://housecat.com/blog/gmail-durable-workflows-sandbox-vm

  • Ce type d’article résulte généralement de l’évaluation indépendante du problème, de la compréhension et de la solution de chacun. Il est discutable de parler de manque d’expertise simplement parce qu’on attendait une certaine performance avec la configuration par défaut d’un outil ; tout le monde continue d’apprendre par l’échec
    Le fait que l’expérience ait utilisé un serveur de base de données avec 96 cœurs et 384 Go de RAM (https://github.com/dbos-inc/dbos-postgres-benchmark/blob/mai...) est très important et aurait dû être clairement indiqué. Les bases de données peuvent scaler verticalement, mais cela a aussi ses limites. Qui se connecte depuis où influence également les performances et la latence globale
    60 000 événements par seconde peuvent sembler beaucoup, mais ce qui fait tomber les systèmes réels, ce sont les pics soudains de trafic, pas le trafic normal. À moins d’être une grande entreprise, on ne commence pas avec ce genre de gros serveur. Avec des réplicas de lecture et une redondance interrégion, un seul cluster de base de données de production coûte plus de 100 000 dollars

  • Cela ressemble à un article lié : Postgres LISTEN/NOTIFY does not scale - https://news.ycombinator.com/item?id=44490510 - juillet 2025, 321 commentaires

  • Il semble manquer la partie la plus importante de l’article : la méthode d’attribution d’un offset ou d’un numéro de séquence qui permet de suivre jusqu’où un consommateur a lu et de consulter les nouveaux messages via un chemin de repli. Il existe plusieurs approches, mais ce n’est pas facile à résoudre sans complexité ni contention de verrous, et une mauvaise implémentation peut aussi créer des conditions de concurrence avec les consommateurs. En général, l’auteur verrouillerait une ligne unique d’une table d’état, par exemple, pour attribuer le prochain numéro d’un sujet d’événements
    Je me demande quelle est la meilleure méthode. Un outil qui lit la capture de données modifiées (CDC) et écrit les numéros d’événements attribués dans une autre table pourrait convenir, mais la latence risque d’être plus élevée, et ce processeur CDC devrait aussi effectuer le NOTIFY
    Côté consommateur, il y a aussi des usages où le traitement par lots peut fortement augmenter le débit. Dans ce cas, on peut exécuter les consommateurs en boucle sans LISTEN/NOTIFY, traiter à chaque itération tous les nouveaux messages non traités, puis stocker le dernier numéro de séquence entre deux itérations

  • Je me souviens que la première version prenant en charge LISTEN/NOTIFY avait une implémentation de verrouillage médiocre, ce qui provoquait des problèmes de performances. L’ancien article critiqué ici le corrigeait d’ailleurs dans une note juste après le premier paragraphe
    Si la correction date du 8 mai, l’article du 24 juillet doit reconnaître que l’article connu affirmant que cette fonctionnalité ne passait pas à l’échelle n’était pas malveillant et n’était peut-être pas faux à l’époque

    • Si cela fait référence à l’optimisation prévue pour Postgres 19, elle est aussi abordée dans l’article d’origine. Ce patch (https://github.com/postgres/postgres/commit/282b1cde9dedf456...) ne supprime pas le verrou global et ne résout pas le goulot d’étranglement observé
      Il optimise plutôt le cas plus restreint où il y a beaucoup de canaux de notification et où chaque récepteur n’attend qu’un canal précis
  • Lors de ma dernière vérification, LISTEN/NOTIFY avait une limite de 8 000 octets sur les données de notification, ce qui constituait clairement un aspect qui ne passait pas à l’échelle. Si les données ne peuvent pas être stockées sous forme de ligne avec seulement un ID transmis, c’est difficile à utiliser
    Les événements d’un jeu web étaient des données temporaires décrivant des changements d’état ; il n’y avait donc aucune raison de les stocker en base, et ils pouvaient dépasser 8 000 octets, ce qui ne convenait pas à cet usage

    • Si je devais implémenter un système de notifications scalable, je fixerais une limite à la taille des notifications. Cela permet de maintenir la taille des messages en O(1) et de se concentrer sur le passage à l’échelle du nombre de notifications ; des messages arbitrairement volumineux peuvent bloquer les performances et signalent probablement aussi une mauvaise utilisation du système de notifications
    • Je me demande s’il ne serait pas possible d’envoyer un message faisant référence à l’état modifié, même s’il ne pointe pas vers une ligne particulière
  • L’article traite de la contention de verrous sur la file globale, mais ne semble pas mentionner un autre problème de la file globale de taille fixe. Un seul récepteur lent sur un canal pouvait bloquer les écritures sur tous les canaux. Du moins, ce type de panne était possible il y a quelques années ; cela a peut-être changé depuis