1 points par GN⁺ 5 시간 전 | 1 commentaires | Partager sur WhatsApp
  • 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_reuseport et 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_reuseport comme 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_conn et max_db_connections sont 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_conn avec l’erreur suivante
    • FATAL: 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 : pgbench sur un troisième serveur
    • charge de travail : lecture seule (select-only), mode transaction pooling
  • 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_reuseport et le peering entre processus pour éliminer ce goulot d’étranglement
  • Tous les serveurs ClickHouse Managed Postgres fournissent cette configuration par défaut

1 commentaires

 
GN⁺ 5 시간 전
Avis de Hacker News
  • Il suffit d’utiliser https://github.com/yandex/odyssey. Odyssey est un PgBouncer scalable

    • Nous avons d’abord correctement configuré et utilisé PgBouncer, la solution la plus éprouvée et la plus naturelle avec PostgreSQL ; des problèmes restés longtemps en suspens, comme la prise en charge des instructions préparées, ont aussi été résolus, et cela fonctionne très bien
      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
    • Fait intéressant lié à cela, ClickHouse a lui aussi été développé à l’origine par Yandex
    • Yandex étant une entreprise russe, je pense qu’il vaut mieux éviter de l’utiliser
  • 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 me demande quel impact a sur le débit l’ajout d’un saut réseau supplémentaire entre PgBouncer et PostgreSQL. Je m’attends à une faible différence, mais cela vaudrait la peine de vérifier
  • 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é ?

  • J’utilise pgdog et il correspond très bien à l’usage dont j’ai besoin

    • Pgdog a été créé pour résoudre les limites de PgBouncer rencontrées il y a longtemps chez Instacart, et pour fournir une base plus solide au scaling horizontal de PostgreSQL, à savoir le sharding
    • Nous envisagerons aussi de proposer pgdog à l’avenir. La raison du choix de PgBouncer est détaillée ici : https://news.ycombinator.com/item?id=48873867
    • Ces derniers temps, des projets de proxy inverse pour PostgreSQL sont présentés tour à tour. On peut voir ici pourquoi Pgdog a été créé : https://news.ycombinator.com/item?id=48819308
  • 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 ?

    • L’annulation d’une requête est transmise séparément de la connexion de requête existante, donc PgBouncer doit envoyer la demande au bon serveur et à la bonne connexion
      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

    • C’est vrai si, sur chaque hôte, l’application backend s’exécute sous la forme d’un seul gros binaire. Mais si le backend est composé de nombreux processus forkés qui ne partagent pas d’état, c’est clairement nécessaire
  • 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 s’est nettement amélioré, et il gère désormais plutôt bien des milliers de connexions : https://techcommunity.microsoft.com/blog/adforpostgresql/imp...
      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
    • À l’époque, plusieurs entreprises proposaient pour la première fois des réponses utilisables au modèle de threading mis en avant par Java. C’était une version enrichie du modèle de Windows ; même Solaris avait du mal avec les traitements Java, HP-UX était encore pire, et je ne me souviens plus si SGI faisait mieux que HP
      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