Aller au contenu
Ingénierie · 5 min de lecture

Un bassin de connexions est une file que vous avez oublié de dimensionner

L'affirmation Le bassin de connexions par défaut de la plupart des cadriciels applicatifs est réglé sur un nombre choisi parce qu'il semblait raisonnable, et il est presque toujour...

A Rédigé par Administrator
Un bassin de connexions est une file que vous avez oublié de dimensionner

L'affirmation

Le bassin de connexions par défaut de la plupart des cadriciels applicatifs est réglé sur un nombre choisi parce qu'il semblait raisonnable, et il est presque toujours faux dans le même sens : trop grand. Un bassin plus grand que ce que la base peut utilement servir n'ajoute pas de capacité — il convertit une file gérable dans votre application en une ruée ingérable à la base, et la défaillance qu'il produit ressemble exactement à une base lente, ce qui pousse les équipes à magasiner une instance plus grosse pour régler un problème qu'un nombre plus petit aurait réglé.

Pourquoi plus grand n'est pas plus

Un serveur PostgreSQL ne peut faire qu'une quantité limitée de travail en parallèle, et cette limite est fixée par les cœurs de processeur et le disque, pas par le nombre de connexions ouvertes. Au-delà de la saturation, ajouter des connexions n'ajoute pas de débit ; cela ajoute de la contention. Chaque connexion se dispute les mêmes cœurs, les mêmes verrous, le même disque, de sorte que le travail ne se termine pas plus vite — chaque requête prend seulement plus de temps, et le total complété par seconde diminue en réalité.

La règle empirique qui tient depuis des années surprend les gens la première fois : le nombre utile de connexions actives est proche du nombre de cœurs de processeur, plus une petite marge pour l'attente disque. Pour une base à 4 cœurs, un ensemble de travail efficace n'est pas 100 connexions ; c'est plutôt une douzaine. Un bassin de 100 contre ce serveur ne sert pas 100 requêtes plus vite — il les sert toutes plus lentement, en même temps, pendant que le serveur s'engorge.

L'arithmétique à travers votre parc

Le piège est multiplicatif et les équipes le manquent parce que chaque application paraît raisonnable isolément. Supposons 6 instances applicatives, chacune configurée avec un bassin de 20. Ce ne sont pas 20 connexions à la base ; ce sont 120. Si la base est configurée avec max_connections = 100, vous êtes sursouscrit avant même que la septième instance démarre, et le symptôme est des erreurs de connexion refusée qui n'apparaissent que sous charge et disparaissent quand vous regardez.

instances x taille_bassin = total de connexions ouvertes
6 x 20 = 120  ->  depasse max_connections = 100

Le correctif est de dimensionner le bassin par instance pour que la somme reste confortablement sous la limite de la base, en laissant de la marge pour les connexions administratives et pour le bref chevauchement lors d'un déploiement où anciennes et nouvelles instances détiennent toutes deux des connexions.

Déplacez l'attente là où vous la voyez

Voici la partie contre-intuitive : un bassin plus petit n'est pas plus lent, il est plus honnête. Avec un bassin de 12, quand 40 requêtes arrivent d'un coup, 12 s'exécutent et 28 attendent dans la file de votre application — une file que vous pouvez mesurer, sur laquelle alerter et raisonner. Avec un bassin de 100, les 40 frappent la base d'un coup, la base s'enlise, et chaque requête, y compris celles qui auraient été rapides, est désormais lente. Le travail ne se fait pas plus vite dans le second cas ; la file s'est seulement déplacée là où vous ne la voyez pas, dans la base, où elle dégrade tout au lieu de l'excédent.

# la mesure qui compte :
pool_acquire_wait_time   # combien de temps les requetes attendent une connexion
# si cela monte, le bassin est trop petit OU les requetes sont trop lentes.
# ajouter des connexions n'aide que le premier cas, et jusqu'a saturation seulement.

Un mandataire en façade change le calcul

Quand vous avez réellement beaucoup d'instances applicatives, la réponse n'est pas un bassin plus grand par instance mais un mandataire de connexions dédié — PgBouncer est le standard — entre les applications et la base. En mode de mandataire par transaction, il laisse des centaines de connexions applicatives partager un petit nombre de vraies connexions de base, parce que la plupart des connexions applicatives sont inactives à tout moment, occupant une place qu'elles n'utilisent pas.

# pgbouncer.ini
pool_mode = transaction
max_client_conn = 1000      # les apps peuvent en ouvrir autant
default_pool_size = 20      # mais seulement autant atteignent Postgres

C'est l'architecture qui permet à une petite base de servir un grand parc : les applications croient avoir mille connexions, la base n'en voit jamais que vingt, et le mandataire absorbe la différence. Le mode par transaction a une contrainte à connaître — il ne prend pas en charge les fonctions de niveau session comme les instructions préparées conservées entre transactions ou les verrous consultatifs — alors confirmez que votre application n'en dépend pas avant de basculer.

Comment le dimensionner en pratique

  1. Trouvez le parallélisme réel de la base. Partez de (2 x cœurs) + nombre effectif de disques comme cible de requêtes actives concurrentes. Sur une instance à 4 cœurs sur SSD, cela fait environ 10 à 12.
  2. Répartissez entre les instances. Le bassin total sur toutes les instances devrait sommer à cette cible, sans la dépasser. Six instances partageant une cible de 12 signifient un bassin de 2 chacune, ce qui paraît minuscule et est correct — ajoutez un mandataire si c'est trop serré.
  3. Laissez de la marge dans max_connections. Fixez la limite de la base au-dessus de votre bassin total, avec de la place pour les déploiements, les migrations et un humain avec psql pendant un incident.
  4. Surveillez la mesure d'attente d'acquisition, pas le nombre de connexions. Une attente croissante alors que le processeur de la base n'est pas encore saturé signifie que le bassin est vraiment trop petit ; une attente croissante alors que la base est déjà à fond signifie que les requêtes sont le problème, et plus de connexions l'aggraveront.

La décision

Avant d'agrandir l'instance de base parce qu'elle semble lente sous charge, vérifiez l'arithmétique des connexions. Multipliez votre nombre d'instances par la taille du bassin par instance et comparez au nombre de cœurs et à la limite de connexions de la base. Si le produit vaut plusieurs fois le nombre de cœurs, la base n'est pas trop petite — le bassin est trop grand, il noie un serveur capable dans la contention, et le correctif ne coûte qu'un nombre plus petit dans un fichier de configuration.

#postgresql #connection pooling #performance #scaling

À lire aussi