Comment PgBouncer a multiplié son débit par 4
(clickhouse.com)- ClickHouse Managed Postgres exploite par défaut une flotte multi-processus dimensionnée en fonction du nombre de cœurs, afin d’éviter la limite d’un PgBouncer mono-thread bloqué sur un seul cœur CPU
- Tous les processus se lient au même port avec
so_reuseport, et le noyau répartit les nouvelles connexions, ce qui apparaît côté client comme un endpoint unique - Les requêtes d’annulation Postgres, qui arrivent sur une connexion distincte, peuvent atteindre un processus qui ne détient pas la session ; elles sont donc relayées au véritable propriétaire via le peering entre processus
- Sur une instance
c7i.4xlargeà 16 vCPU, un processus unique plafonnait à environ 87 000 TPS, tandis qu’une flotte de 16 processus est montée à environ 336 000 TPS, soit près de 4 fois plus de débit - Avec peu de connexions, un processus unique peut être comparable, voire légèrement plus rapide, mais à forte concurrence un seul cœur devient le goulet d’étranglement ; il faut donc répartir le budget de connexions sur l’ensemble de la flotte pour augmenter le débit et la limite de connexions sans surconnecter Postgres
Le goulot d’étranglement CPU créé par un processus unique
- PgBouncer est mono-thread, donc quel que soit le nombre de CPU du système, un processus n’utilise qu’un seul cœur
- Même sur un serveur à 16 vCPU, le travail de pooling de connexions se concentre sur un seul cœur et les 15 autres restent inactifs
- En conséquence, le pooler limite le débit global avant même que Postgres n’ait épuisé sa capacité de traitement
- ClickHouse Managed Postgres exploite une flotte de processus dont le nombre de processus PgBouncer est déterminé en proportion du nombre de cœurs disponibles
Répartition des connexions avec so_reuseport
- Tous les processus de la flotte activent
so_reuseportet se lient au même port - Le noyau répartit les connexions entrantes entre les différents processus, ce qui permet au client de se connecter à un seul endpoint sans avoir besoin de savoir que plusieurs PgBouncer tournent en arrière-plan
- La documentation de PgBouncer recommande également
so_reuseportcomme moyen d’exploiter plusieurs cœurs avec plusieurs processus mono-thread
Peering entre processus pour garantir l’annulation des requêtes
- Les requêtes d’annulation de Postgres arrivent avec une clé d’annulation via une nouvelle connexion, distincte de celle de la requête en cours d’exécution
- Avec
so_reuseport, le noyau peut envoyer cette nouvelle connexion d’annulation à un autre processus que celui qui détient la session concernée- Si le processus qui reçoit la requête ne connaît pas cette requête, l’annulation ne s’exécute pas
- Le peering entre processus PgBouncer permet de relayer une requête arrivée au mauvais processus vers celui qui détient réellement la session
- Ainsi, quel que soit le processus qui reçoit la requête, l’annulation fonctionne correctement à l’échelle de toute la flotte
Pooling transactionnel et budget de connexions
- Le pooling fonctionne en mode transactionnel : dès qu’une transaction est commit, la connexion serveur est rendue au pool
max_client_connetmax_db_connectionssont divisés par le nombre de processus afin d’être alloués à chacun- Ce découpage du budget de connexions permet de maintenir le nombre total de connexions de la flotte dans les limites sûres de Postgres tout en augmentant la limite agrégée de connexions
- Un processus unique rejette les nouveaux clients au-delà de sa propre limite
max_client_connavec l’erreur suivanteFATAL: no more connections allowed (max_client_conn)
Benchmark sur le même matériel
- Les deux configurations ont été mesurées dans le même environnement AWS EC2
- serveur pooler :
c7i.4xlargeà 16 vCPU - Postgres : serveur séparé
- générateur de charge :
pgbenchsur un troisième serveur - charge de travail : lecture seule (select-only), mode transaction pooling
- serveur pooler :
- La comparaison porte sur un processus unique PgBouncer et une flotte de 16 processus, avec le même type d’instance, le même Postgres et la même charge de travail
- Le nombre de connexions client a été augmenté de 8 à 256, tout en mesurant le débit et l’utilisation CPU du serveur à 16 cœurs
| Clients | TPS processus unique | CPU serveur processus unique | TPS flotte | CPU serveur flotte |
|---|---|---|---|---|
| 8 | 8,910 | 0.8% | 6,450 | 2.9% |
| 32 | 54,203 | 5.2% | 64,244 | 12.3% |
| 64 | 86,570 | 8.3% | 219,439 | 31.9% |
| 128 | 83,463 | 8.1% | 320,547 | 45.9% |
| 256 | 76,893 | 7.7% | 336,469 | 48.9% |
Résultats sur le débit et l’utilisation CPU
- Le processus unique a culminé à environ 87 000 TPS, puis les performances ont baissé à mesure que la charge augmentait, pour tomber à environ 77 000 TPS avec 256 clients
- La flotte de 16 processus a continué à progresser jusqu’à environ 336 000 TPS en exploitant davantage de cœurs, soit environ 4 fois le débit d’un processus unique
- D’après les mesures
pidstat, un seul processus PgBouncer saturait pratiquement un cœur avec environ 97% de CPU sous charge, tandis que l’utilisation totale du serveur 16 vCPU restait sous les 10% - La flotte répartissait le travail sur l’ensemble du serveur et utilisait environ 8 cœurs, avec encore de la marge lorsque Postgres et le générateur de charge atteignaient leurs propres limites
- Lors d’une mesure maintenue à 256 clients, le serveur en processus unique utilisait environ 9% de CPU, contre environ 52% CPU pour le serveur en flotte
- Dans les mesures externes d’EC2 CloudWatch, le CPUUtilization moyen de l’instance en processus unique était d’environ 16%, contre environ 60% pour la flotte
- Les chiffres CloudWatch sont légèrement supérieurs aux mesures internes au guest, mais l’écart montre toujours qu’un PgBouncer unique n’exploite pas la majorité des 16 vCPU
Choisir la configuration selon le niveau de concurrence
- Quand le nombre de connexions est faible, il n’y a pas assez de travail à paralléliser et les connexions de la flotte sont réparties entre plusieurs processus ; un processus unique peut donc suffire, voire être légèrement plus rapide
- Avec 8 clients, le processus unique atteint 8,910 TPS contre 6,450 TPS pour la flotte
- Quand la concurrence augmente, le seul cœur occupé par le processus unique devient le mur de débit, et l’écart de performance avec la flotte se creuse
- Dans les environnements où Postgres atteint ses limites avant PgBouncer, un processus unique reste un choix par défaut pertinent
- Si le pooler commence à limiter le débit, il faut combiner une flotte de processus adaptée au nombre de cœurs, le partage de port via
so_reuseportet le peering entre processus pour éliminer ce goulot d’étranglement - Tous les serveurs ClickHouse Managed Postgres fournissent cette configuration par défaut
1 commentaires
Avis de Hacker News
Il suffit d’utiliser https://github.com/yandex/odyssey. Odyssey est un PgBouncer scalable
Beaucoup de clients ont porté les connexions PostgreSQL à plus de 10 000, et nous évaluerons aussi à l’avenir des alternatives comme Odyssey ou pgdog. Cela dit, personnellement, je ne suis pas favorable à une approche consistant à avoir plus de 10 000 connexions à PostgreSQL, et je pense qu’on peut scaler largement avec seulement quelques centaines de connexions
En exploitant PgBouncer sur Kubernetes, il est facile de lancer plusieurs processus sur une même machine, et tout aussi simple de les répartir sur plusieurs machines
Azure provoque parfois des pannes séquentielles sur tout un parc de serveurs lors de la maintenance des VM, ce qui rend une configuration multi-machines particulièrement utile
Je comprends le concept de peering évoqué ici, mais je ne l’ai jamais utilisé avec PostgreSQL
A) Existe-t-il dans PostgreSQL un mode ou un réglage permettant de le configurer facilement ? J’imagine une structure où les demandes d’annulation seraient transmises séquentiellement aux pairs jusqu’à ce qu’elles ne produisent plus d’erreur, ou bien où la demande d’annulation contiendrait des métadonnées permettant au processus qui l’a reçue par erreur d’identifier le bon processus
B) Si tous les processus PostgreSQL reçoivent les requêtes client avec
so_reuseport, il semble qu’un mécanisme distinct de communication inter-processus (IPC) soit nécessaire pour la communication entre pairs ; quel mécanisme est réellement utilisé ?En incluant l’ID de processus PgBouncer dans la clé d’annulation, cela devrait pouvoir être implémenté assez simplement
Voir à partir de la diapositive 26 : https://www.pgevents.ca/events/pgconfdev2024/sessions/sessio...
Il existe aussi une vidéo de la présentation de Jelte, mainteneur de PgBouncer : https://www.youtube.com/watch?v=X-nCHcZ6vQU
J’utilise pgdog et il correspond très bien à l’usage dont j’ai besoin
Je me demande pourquoi PgBouncer doit même se préoccuper des demandes d’annulation. Ne pourrait-il pas simplement les transmettre à PostgreSQL, puis, lorsque PostgreSQL renvoie une erreur au lieu d’une réponse normale pour la requête annulée, laisser le PgBouncer responsable de cette connexion la traiter ?
PgBouncer donne au client un PID d’annulation et une valeur secrète factices mais traçables ; quand la demande d’annulation revient, il retrouve le PID et la valeur secrète du serveur/processus réels et les transmet. De plus, il peut arriver que le client croie que la requête est en cours d’exécution alors que PgBouncer sait qu’elle est déjà terminée et que la connexion a été réutilisée ; il faut donc vérifier que cette connexion et ce PID exécutent toujours cette requête
Est-ce destiné à contrôler l’accès au serveur PostgreSQL via un pool de connexions dans des microservices ? Avec un backend monolithique, cela ne semble pas nécessaire
La plupart des bons frameworks backend intègrent un pool de connexions, et cela suffit pour 98 % des cas d’usage où les microservices ne sont ni nécessaires ni recommandés
PgBouncer est un excellent logiciel. Nous l’utilisons beaucoup et il simplifie considérablement l’exploitation des bases de données
Je viens de découvrir
so_reuseport, c’est intéressant. Le cœur de la configuration semble être cela, avec le peering ; le peering est-il intégré à PgBouncer et simple à configurer ?Peut-on aussi utiliser le peering avec Kubernetes ? Dans cet environnement, il ne devrait pas être nécessaire de réutiliser les ports ; je me demande aussi si chaque Pod dispose de son propre pool de connexions et fonctionne indépendamment
J’ai 46 ans aujourd’hui, et je me souviens avoir été choqué, à 23 ans, par le modèle de connexion lourd de PostgreSQL. Cela n’a donc pas été amélioré depuis ?
Cela dit, sans pool de connexions, PostgreSQL doit forker un processus, donc la création d’une nouvelle connexion coûte toujours plusieurs dizaines de millisecondes ou plus. Les applications écrites sans pool de connexions ne sont pas idéales, mais elles existent couramment
Les frameworks applicatifs ont aussi changé, et les architectures serverless peuvent créer des dizaines de milliers de connexions, là où PostgreSQL commence à rencontrer des problèmes. Personnellement, je n’aime pas dépasser quelques centaines de connexions, mais c’est aujourd’hui une situation tout à fait réaliste
Pour assurer la compatibilité avec Java, de nombreuses entreprises ont dû modifier en profondeur leur façon de gérer la concurrence. PostgreSQL et SQLite ont été conçus à peu près à la même période, mais ils avaient été créés par des vétérans du secteur qui déployaient des systèmes à forte charge bien avant l’apparition de ces discussions sur le threading, tout en continuant à prendre en charge les utilisateurs de matériel ancien