Vous déployez une petite application sur un petit VPS. Au début elle est polie. Puis un jour un email marketing part, le trafic triple, et votre base de données se transforme en videur de boîte de nuit : « Pas ce soir. » Pendant ce temps, votre CPU ne transpire même pas. Votre RAM, si. Vos logs d’erreur commencent à parler en langues : too many connections, timeouts, pics de latence étranges, resets de connexion occasionnels.
C’est le moment où l’on découvre le pooling de connexions. Souvent sous pression. Souvent après avoir blâmé le réseau, l’ORM, le DNS, le fournisseur cloud, et — si on est très engagé — la lune.
La thèse directe : qui a besoin de pooling en premier ?
Si vous exécutez un seul VPS avec une RAM limitée, PostgreSQL a généralement besoin d’un pool de connexions plus tôt que MySQL pour une raison simple : une connexion cliente Postgres correspond habituellement à un processus backend dédié, et ce processus consomme une mémoire non négligeable même lorsqu’il est « idle ». Assez de connexions inactives et votre VPS passe sa vie à swapper, ce qui tue la latence et finalement votre dignité.
Le modèle par défaut de MySQL (surtout avec InnoDB) est souvent plus tolérant à un nombre modéré de clients parce qu’il n’y a pas un lourd processus OS par connexion de la même manière, et il dispose de réglages comme le thread caching qui peuvent atténuer le churn de connexions/déconnexions. Mais « plus tolérant » n’est pas « immunisé ». Sur un VPS avec peu de mémoire et un trafic web en rafales, les deux peuvent s’effondrer à cause d’une tempête de connexions ; Postgres touche le mur plus tôt, et ce mur est généralement la RAM.
Il y a une deuxième vérité franche : la plupart des applis n’ont pas besoin de pooling à 20 connexions ; elles ont besoin de limites sensées, de timeouts, et de moins de reconnexions. Mais dès que vous avez beaucoup de workers d’appli et des requêtes courtes, le pooling cesse d’être une « optimisation » et devient un « ceinture de sécurité. »
Faits intéressants et un peu d’histoire (pour que vous cessiez de répéter les mêmes erreurs)
- La lignée « un backend par connexion » de PostgreSQL vient des premiers modèles de processus Unix et d’une forte préférence pour l’isolation. Cette architecture est toujours là aujourd’hui, même si les internals ont énormément évolué.
- PgBouncer est devenu populaire non pas parce que Postgres est lent mais parce que beaucoup de stacks web créaient trop de sessions de courte durée, et « augmenter juste max_connections » est vite devenu un hobby coûteux.
- Le modèle thread-per-connection de MySQL existe depuis longtemps, mais le comportement pratique dépend fortement du thread caching et de la configuration. MySQL/MariaDB modernes peuvent gérer un fort churn mieux que leur pire réputation ne le suggère — si on les configure.
- La valeur par défaut de
max_connectionsde PostgreSQL est conservatrice car chaque connexion peut consommer de la mémoire dans plusieurs contextes (work_mem, buffers, structures par backend). Le système vous incite à utiliser le pooling. - Le coût de la poignée de connexion de MySQL était plus important quand TLS et l’authentification étaient plus lents et que les applis se reconnectaient constamment. Ça compte toujours, mais le matériel et les bibliothèques se sont améliorés.
- Le « pooling dans l’appli » est devenu courant lorsque les frameworks ont commencé à exécuter de nombreux processus workers (pensez aux serveurs pré-fork) et que chaque worker gardait son propre pool, multipliant les connexions de façon invisible.
- Le transaction pooling est un instrument relativement brutal qui sacrifie les fonctionnalités au niveau de la session (comme les prepared statements et les variables de session) pour survivre sous forte charge. C’est un compromis, pas de la magie.
- Postgres a ajouté au fil du temps une meilleure observabilité (comme
pg_stat_activity, les wait events, et plus), ce qui rend ironiquement plus facile de voir les tempêtes de connexions — et donc plus facile de paniquer à bon escient.
Ce que le pooling de connexions résout réellement (et ce qu’il ne résout pas)
Une connexion à une base n’est pas une « ficelle ». C’est une session négociée avec authentification, allocations mémoire, buffers de socket, et — selon la base — des ressources serveur dédiées. Créer et détruire ces sessions à haut débit est coûteux et imprévisible.
Le pooling de connexions résout deux problèmes :
- Le churn : il amortit le coût d’ouverture/fermeture des connexions sur de nombreuses requêtes.
- La dispersion (fan-out) : il limite le nombre de sessions côté serveur même lorsque votre appli a beaucoup de workers ou de threads.
Le pooling ne résout pas :
- Les requêtes lentes (il peut les masquer jusqu’à ce que la file d’attente apparaisse)
- Les mauvais index
- La contention sur les verrous
- La saturation d’IO disque
- Exécuter des analyses lourdes sur la même machine que votre charge OLTP parce que « ce n’est qu’un rapport »
Blague #1 : Un pool de connexions, c’est comme une imprimante de bureau partagée — tout le monde l’adore jusqu’à ce que quelqu’un lance un PDF de 300 pages et bloque la file.
MySQL sur un VPS : comportements de connexion qui mordent en premier
MySQL (et MariaDB) s’exécute souvent comme un seul processus serveur gérant de nombreux threads. Chaque connexion cliente correspond typiquement à un thread serveur. Cela peut faire beaucoup de threads, ce qui a son propre coût, mais cela n’explose généralement pas votre RSS comme les processus backend Postgres peuvent le faire sur des boxes avec peu de RAM — du moins pas aux mêmes nombres de connexions.
Ce qui tombe en panne en premier sur MySQL dans l’univers VPS ressemble souvent à :
- Churn de création de threads quand les applis se connectent/déconnectent constamment sans thread caching bien réglé.
- Épuisement de max_connections avec des sessions « sleeping » causées par des clients fuyants ou des pools d’appli énormes.
- Mise en file et timeouts si votre charge est liée au CPU ou à l’IO et que les connexions s’entassent en attente.
MySQL peut tolérer « beaucoup de connexions » mieux que Postgres dans certains cas, mais il incite aussi les gens à adopter un schéma stupide : augmenter max_connections et appeler ça du capacity planning. Sur un VPS, c’est ainsi que vous échangez « trop de connexions » contre « le kernel OOM killer a choisi ma base ».
MySQL : ce que le pooling vous apporte tôt
Si votre appli utilise beaucoup de requêtes courtes (PHP-FPM, workers de type serverless, cron storms), le pooling vous apporte surtout une réduction du coût de la poignée et moins de threads concurrents. Mais beaucoup de bibliothèques clientes MySQL et frameworks effectuent déjà un pooling basique, et MySQL est souvent déployé derrière une seule couche d’appli où vous pouvez définir des tailles de pool raisonnables par processus.
En pratique : sur MySQL, vous pouvez souvent survivre plus longtemps sans pooler externe si votre application pool correctement et que vous réglez le serveur (thread cache, timeouts, backlogs). Mais « survivre plus longtemps » ne signifie pas « bien » ; cela signifie que vous avez le temps de réparer avant que le pager ne joue du jazz.
PostgreSQL sur un VPS : pourquoi « un backend par connexion » change tout
L’architecture de PostgreSQL est fameusement simple : le postmaster accepte une connexion et fork (ou réutilise) un processus backend pour la gérer. Une connexion, un backend. C’est une isolation propre, des frontières de faute claires, et un modèle de coût très visible.
Sur un VPS, le mode de défaillance est brutalement constant :
- Votre appli crée plus de workers (ou plus de pods, mais ici on parle d’échelle VPS donc plus de processus).
- Chaque worker garde son propre pool (souvent 5–20 connexions par worker par défaut).
- Le nombre de connexions grimpe.
- L’utilisation mémoire augmente avec.
- La machine commence à swapper ou à OOM.
- La latence devient une fonction de « combien de temps passons-nous à paniquer ».
Postgres peut supporter de lourdes charges sur un matériel modeste, mais il n’aime pas être traité comme un multiplexeur de sockets pour une couche d’appli indisciplinée. Quand on dit « Postgres a besoin de pooling », on veut vraiment dire « votre appli a besoin d’un adulte pour la surveiller. »
Postgres : pourquoi le pooling n’est pas optionnel à un moment donné
Même des backends « idle » coûtent de la mémoire. Et les backends occupés coûtent plus, surtout avec des tris, des hashes, et des réglages par session. Sur de petites instances VPS, quelques centaines de connexions peuvent être catastrophiques même si le QPS est modeste, parce que le goulot d’étranglement est la mémoire et le changement de contexte — pas le débit brut des requêtes.
C’est pourquoi PgBouncer (ou équivalent) est si courant : il limite le nombre de sessions côté serveur tout en laissant l’appli croire qu’elle a beaucoup de « connexions ». Il vous donne aussi un point central pour appliquer des limites et des timeouts. Ce n’est pas glamour. C’est salvateur.
Alors, qui a besoin du pooling plus tôt sur un VPS ?
PostgreSQL a besoin d’un pool de connexions plus tôt dans le scénario typique de VPS parce que le coût serveur par connexion est plus élevé et lié directement aux processus OS. Si vous exécutez une appli web avec beaucoup de workers, vous pouvez toucher le mur avec un trafic étonnamment faible : des dizaines de workers × un pool de 10 chacun, ça fait déjà des centaines de connexions.
MySQL a besoin de pooling plus tôt si le comportement client est pathologique : beaucoup de connect/disconnect par requête, pas de keepalive, thread cache faible, ou si votre couche applicative effectue un fort fan-out (beaucoup de jobs indépendants ou services frappant la même base). MySQL peut tomber à cause du churn de connexions et des tempêtes de threads, et il peut aussi souffrir de contention de ressources sous d’énormes nombres de connexions même si la mémoire n’explose pas autant.
Voici la règle de décision pratique pour un VPS :
- Si vous êtes sur Postgres et que vous ne pouvez pas dire avec confiance « nous limitons les connexions totales en dessous de 100 et savons exactement pourquoi », vous devez supposer que vous avez besoin d’un pooler externe ou d’un pooling agressif côté applicatif dès maintenant.
- Si vous êtes sur MySQL et que vous voyez un churn élevé de connexions, des
Aborted_connectsfréquents, ou des pics de threads en cours d’exécution, vous avez besoin d’un pooler ou au moins d’une réutilisation disciplinée des connexions côté appli dès maintenant.
Blague #2 : Si votre plan est « on augmentera juste max_connections », félicitations — vous avez réinventé le déni en tant que service.
Choix de conception d’un pooler : pool côté appli vs pool proxy vs pool serveur
1) Pooling côté application (intégré aux frameworks)
C’est le défaut dans beaucoup de stacks : chaque processus maintient un pool de connexions ouvertes. C’est facile, rapide, et conserve la sémantique de session (prepared statements, tables temporaires, variables de session). Ça multiplie aussi les connexions par le nombre de workers d’appli. Sur un VPS, cette multiplication est la façon dont vous mourrez en silence à 3h du matin.
Utilisez le pooling côté appli lorsque :
- Vous avez un petit nombre de processus applicatifs
- Vous pouvez appliquer des plafonds stricts par processus
- Vous avez besoin des fonctionnalités au niveau de la session
2) Pooler/proxy externe (PgBouncer, ProxySQL)
C’est « l’adulte dans la pièce ». Vous placez un service léger devant la base, et votre application s’y connecte. Le pooler maintient un nombre contrôlé de connexions serveur tout en servant de nombreuses connexions clientes.
Compromis :
- Bien : protège la BDD des tempêtes de connexions ; centralise les limites ; réduit le churn.
- Moins bien : la sémantique de session peut être rompue en modes transaction/statement pooling.
- Opérationnel : c’est une pièce en plus à gérer sur un petit VPS ; gardez-le simple et surveillé.
3) « On augmente juste max_connections » (ne le faites pas)
Augmenter les limites peut être correct quand vous avez mesuré la mémoire par connexion et que vous avez une marge RAM. Mais sur un VPS, c’est souvent un pansement sur une artère tranchée. Si vous augmentez le plafond sans changer le comportement client, vous n’augmentez pas la capacité ; vous augmentez le rayon du sinistre.
Tâches pratiques : commandes, sorties et la décision que vous prenez
Voici les vérifications que j’exécute quand quelqu’un dit « la base est lente » alors qu’en réalité il veut dire « les connexions font fondre la machine ». Exécutez-les sur le VPS. Ne devinez pas. Le matériel VPS est fini et très franc à ce sujet.
Task 1: Check memory pressure and swapping
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 3.8Gi 2.9Gi 180Mi 52Mi 720Mi 420Mi
Swap: 1.0Gi 820Mi 204Mi
Ce que ça signifie : Le swap est fortement utilisé et la mémoire disponible est faible. Votre latence va ressembler à un générateur de nombres aléatoires.
Décision : Traitez immédiatement le nombre de connexions comme suspect ; réduisez les sessions concurrentes et envisagez un pooler externe avant de « tuner les requêtes ».
Task 2: Identify whether the DB is being OOM-killed
cr0x@server:~$ journalctl -k --since "2 hours ago" | tail -n 20
Dec 29 10:41:12 vps kernel: Out of memory: Killed process 2143 (postgres) total-vm:5216444kB, anon-rss:3102420kB, file-rss:0kB, shmem-rss:0kB
Dec 29 10:41:12 vps kernel: oom_reaper: reaped process 2143 (postgres), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Ce que ça signifie : Le kernel a tué Postgres. Ce n’est pas un « bug Postgres ». C’est de l’arithmétique des ressources.
Décision : Atténuation immédiate : plafonner les connexions, tuer les connexions inactives, activer le pooling, et arrêter d’ajouter des workers jusqu’à ce que la mémoire se stabilise.
Task 3: Check TCP connection states to the database port
cr0x@server:~$ ss -tanp | awk '$4 ~ /:5432$/ || $4 ~ /:3306$/ {print $1, $2, $3, $4, $5}' | head
ESTAB 0 0 10.0.0.10:5432 10.0.0.21:51122
ESTAB 0 0 10.0.0.10:5432 10.0.0.21:51130
SYN-RECV 0 0 10.0.0.10:5432 10.0.0.22:60718
TIME-WAIT 0 0 10.0.0.10:5432 10.0.0.23:49810
Ce que ça signifie : SYN-RECV suggère une pression sur le backlog d’acceptation ou que la BDD est lente à accepter les connexions ; beaucoup de TIME-WAIT implique du churn.
Décision : Si TIME-WAIT domine, corrigez la réutilisation/pooling côté client. Si SYN-RECV domine, vérifiez le backlog d’écoute du DB et la saturation CPU.
Task 4: Count live connections by process (PostgreSQL)
cr0x@server:~$ sudo -u postgres psql -c "select state, count(*) from pg_stat_activity group by 1 order by 2 desc;"
state | count
---------+-------
idle | 142
active | 9
| 1
(3 rows)
Ce que ça signifie : 142 sessions idles occupent des processus backend. Sur un VPS, c’est souvent de la mémoire gaspillée et des changements de contexte inutiles.
Décision : Ajoutez PgBouncer ou réduisez la taille des pools d’appli ; configurez aussi idle_in_transaction_session_timeout et revoyez le comportement de keepalive.
Task 5: Inspect Postgres max_connections and reserved slots
cr0x@server:~$ sudo -u postgres psql -c "show max_connections; show superuser_reserved_connections;"
max_connections
-----------------
200
(1 row)
superuser_reserved_connections
-------------------------------
3
(1 row)
Ce que ça signifie : Seulement 197 connexions sont disponibles pour les utilisateurs normaux. Ce plafond existe pour une raison.
Décision : Ne l’augmentez pas tant que vous n’avez pas mesuré la mémoire par backend et confirmé que swap n’est pas impliqué.
Task 6: Measure approximate Postgres backend memory usage
cr0x@server:~$ ps -o pid,rss,cmd -C postgres --sort=-rss | head
PID RSS CMD
2149 178432 postgres: appdb appuser 10.0.0.21(51122) idle
2191 165120 postgres: appdb appuser 10.0.0.21(51130) idle
2203 98204 postgres: appdb appuser 10.0.0.22(60718) active
2101 41288 postgres: checkpointer
2099 18864 postgres: writer
Ce que ça signifie : Chaque backend fait ~100–180MB RSS ici. Sur un VPS de 4GB, 50 sessions comme ça peuvent ruiner votre semaine.
Décision : Vous avez besoin d’un pooler et/ou de réduire les réglages mémoire ; investiguez aussi pourquoi le RSS par backend est si élevé (extensions, prepared statements, comportement de work_mem).
Task 7: Check MySQL connection counts and running threads
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_%';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 32 |
| Threads_connected | 180 |
| Threads_created | 9124 |
| Threads_running | 14 |
+-------------------+-------+
Ce que ça signifie : Beaucoup de connexions, des threads en cours modérés, et un Threads_created élevé indique du churn (selon le temps d’uptime).
Décision : Augmentez thread_cache_size, réduisez les reconnexions applicatives, et plafonnez les pools. Si le churn est intense, envisagez ProxySQL ou corrigez d’abord le pooling côté appli.
Task 8: Check MySQL max_connections and aborted connects
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'max_connections'; SHOW GLOBAL STATUS LIKE 'Aborted_connects';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 200 |
+-----------------+-------+
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Aborted_connects | 381 |
+------------------+-------+
Ce que ça signifie : Les Aborted connects peuvent venir d’un problème d’auth, de resets réseau, ou de tempêtes de connexions qui se heurtent à des limites/timeouts.
Décision : Si ça augmente durant les pics de trafic, votre appli se reconnecte trop souvent ou est throttlée. Corrigez le pooling ; puis ajustez timeouts et backlog.
Task 9: Find who is opening the connections (server-side view)
cr0x@server:~$ sudo lsof -nP -iTCP:5432 -sTCP:ESTABLISHED | awk '{print $1,$2,$9}' | head
postgres 2149 TCP 10.0.0.10:5432->10.0.0.21:51122
postgres 2191 TCP 10.0.0.10:5432->10.0.0.21:51130
postgres 2203 TCP 10.0.0.10:5432->10.0.0.22:60718
Ce que ça signifie : Vous pouvez cartographier les sources de connexions par IP/port ; combinez avec les logs d’appli ou la découverte de services pour identifier le voisin bruyant.
Décision : Si un hôte domine, plafonnez-le en priorité. Ne punissez pas toute la flotte parce qu’un job runner se comporte mal.
Task 10: Check Postgres for idle-in-transaction sessions
cr0x@server:~$ sudo -u postgres psql -c "select pid, usename, state, now()-xact_start as xact_age, left(query,80) as query from pg_stat_activity where state like 'idle in transaction%' order by xact_start asc limit 5;"
pid | usename | state | xact_age | query
------+----------+----------------------+------------+------------------------------------------------------------------------------
3012 | appuser | idle in transaction | 00:12:41 | UPDATE orders SET status='paid' WHERE id=$1
(1 row)
Ce que ça signifie : Une connexion qui garde une transaction ouverte peut bloquer le vacuum, provoquer du bloat, et créer une contention sur les verrous qui ressemble à « les connexions sont lentes. »
Décision : Corrigez la gestion des transactions dans l’appli ; configurez idle_in_transaction_session_timeout et envisagez le transaction pooling avec prudence (il peut masquer des bugs).
Task 11: Validate backlog and listen settings on Linux
cr0x@server:~$ sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
Ce que ça signifie : Les caps de backlog sont corrects. Si vous voyez encore beaucoup de SYN-RECV, l’appli/DB peut être trop lente à accepter ou être à court de CPU.
Décision : Si les valeurs sont petites (ex. 128), augmentez-les ; mais n’utilisez pas le tuning du kernel pour éviter de réparer des tempêtes de connexions.
Task 12: Check Postgres wait events and top queries (high-level)
cr0x@server:~$ sudo -u postgres psql -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where state='active' group by 1,2 order by 3 desc;"
wait_event_type | wait_event | count
-----------------+---------------+-------
Lock | transactionid | 6
IO | DataFileRead | 3
(2 rows)
Ce que ça signifie : Votre « problème de connexions » pourrait en réalité être de la contention sur des verrous ou des stalls d’IO. Le pooling ne corrigera pas cela ; il ne fait que mettre en file la douleur.
Décision : Si les verrous dominent, partez à la chasse aux transactions longues et aux lignes chaudes. Si l’IO domine, vérifiez la latence disque et les ratios de cache avant de toucher au pooling.
Task 13: Check MySQL process list for sleeping floods
cr0x@server:~$ mysql -e "SHOW PROCESSLIST;" | head
Id User Host db Command Time State Info
412 appuser 10.0.0.21:51912 appdb Sleep 287 NULL
413 appuser 10.0.0.21:51920 appdb Sleep 290 NULL
444 appuser 10.0.0.22:38110 appdb Query 2 Sending data SELECT ...
Ce que ça signifie : Beaucoup de Sleep signifie que les clients gardent des connexions ouvertes. C’est normal avec du pooling, mais ça doit être borné.
Décision : Si le nombre de Sleep est énorme et que vous atteignez max_connections, réduisez les tailles de pool, ajoutez de la discipline de pooling, ou utilisez un pooler proxy pour plafonner les sessions serveur.
Task 14: Verify application fan-out from the OS side
cr0x@server:~$ ps -eo pid,cmd | egrep 'gunicorn|puma|php-fpm|sidekiq|celery' | head
1021 /usr/bin/puma 5.6.7 (tcp://0.0.0.0:3000) [app]
1033 sidekiq 6.5.9 app [0 of 10 busy]
1102 php-fpm: pool www
1103 php-fpm: pool www
Ce que ça signifie : Les setups avec beaucoup de workers multiplient les pools. Si chacun de ces processus a un pool de 10, vous venez de créer une usine à connexions.
Décision : Additionnez vos pires cas de connexions : workers × pool_size. Si ça dépasse la limite sûre de votre DB, vous avez besoin d’un pooler ou d’un empreinte plus petite.
Kit de diagnostic rapide
Quand vous êtes sur un VPS, l’objectif n’est pas une analyse parfaite. L’objectif est arrêter l’hémorragie et identifier correctement le goulot d’étranglement dominant avant d’« optimiser » la mauvaise chose.
Première étape : confirmer si c’est la pression de connexions ou la pression requête/IO
- Mémoire + swap :
free -h. Si le swap est fort, le nombre de connexions est suspect jusqu’à preuve du contraire. - Nombre de connexions : Postgres
pg_stat_activity, MySQLThreads_connected. - Logs kernel : OOM kills ou erreurs TCP.
Deuxième étape : déterminer si les connexions sont inactives, bloquées ou vraiment occupées
- Postgres : comptez
idlevsactivevsidle in transaction. - MySQL :
SHOW PROCESSLIST, surveillez beaucoup de Sleep vs beaucoup de « Sending data » ou « Locked ».
Troisième étape : si c’est occupé, identifiez l’attente dominante
- Verrous : transactions longues, lignes chaudes, index manquants.
- IO : disque lent, cache insuffisant, trop de lectures aléatoires.
- CPU : trop de requêtes concurrentes ; le pooling peut aider en limitant la concurrence, mais c’est un limiteur de bande, pas une solution.
Quatrième étape : appliquer l’atténuation la moins risquée
- Plafonnez les pools et le nombre de workers applicatifs.
- Activez un pooler externe (PgBouncer/ProxySQL) si la BDD est noyée sous les sessions.
- Réduisez le churn : keepalive, timeouts raisonnables, réutilisation des connexions.
Trois mini-histoires du monde corporate (anonymisées, mais douloureusement réelles)
Incident : la mauvaise hypothèse qui a causé une inondation de connexions
Une équipe SaaS de taille moyenne exécutait Postgres sur un VPS costaud mais pas infini. La couche applicative était un mélange de web workers et de background workers. Lors d’un lancement produit, les temps de réponse sont passés de « acceptables » à « gelés ». Leurs dashboards montraient un CPU à 40 %. L’ingénieur on-call, à raison, a blâmé le load balancer et a commencé à chercher des coupures réseau.
Le vrai problème était une hypothèse tranquille : « Notre ORM pool les connexions. » Vrai. Aussi incomplet. Chaque processus worker avait son propre pool, et le déploiement a augmenté le nombre de workers pour gérer le trafic. Les connexions totales ont multiplié, Postgres a forké un backend pour chacun, la mémoire a grimpé, le swap a commencé, puis le kernel a commencé à tuer des processus. Le load balancer n’avait rien fait de mal ; il regardait simplement l’incendie.
La correction n’était pas exotique. Ils ont plafonné la taille du pool par processus, réduit temporairement le nombre de workers, et inséré PgBouncer en mode session pooling. Soudain la BDD a cessé de se forker jusqu’à l’oubli. Ils ont aussi ajouté une alerte dure sur le total des connexions et l’utilisation du swap. La leçon n’était pas « PgBouncer est génial. » La leçon était : comptez vos connexions de la même manière que vous comptez les CPUs — globalement.
Optimisation qui a mal tourné : transaction pooling agressif sans hygiène applicative
Une équipe e-commerce voulait « faire monter Postgres en charge » sur un VPS sans l’upgrader. Ils ont déployé PgBouncer en mode transaction pooling parce que ça semblait l’option la plus efficace. Le nombre de connexions s’est stabilisé. Tout le monde a célébré. Puis sont apparus les bugs les plus bizarres : « prepared statement does not exist » intermittents, « current transaction is aborted » occasionnels, et quelques paiements bloqués.
Ils avaient deux problèmes. D’abord, l’application dépendait de l’état de session : prepared statements et réglages par session. En transaction pooling, un client peut ne pas retrouver la même connexion serveur pour la transaction suivante, donc l’état de session devient peu fiable à moins d’adapter l’appli. Ensuite, ils avaient une gestion des transactions laxiste : certains chemins laissaient des transactions ouvertes plus longtemps que prévu, et le pooler a amplifié les symptômes en jonglant les clients sur moins de backends serveur.
Ils sont finalement passés au session pooling pour l’appli principale, ont gardé le transaction pooling pour un job runner sans état, et ont corrigé les limites de transaction dans le code. Le débit a un peu baissé par rapport au « maximum théorique », mais la cohérence est revenue. Le VPS est resté stable. C’était un rappel coûteux que le « mode de pooling » est un contrat applicatif, pas un réglage serveur.
Pratique ennuyeuse mais correcte qui a sauvé la mise : budgets stricts et timeouts
Une entreprise du milieu financier exécutait MySQL sur un petit VPS pour un outil interne. L’outil n’était pas « critique » jusqu’à ce qu’inévitablement il le devienne. Ils avaient une habitude qui les faisait paraître paranoïaques : une feuille de calcul de budget de connexions. Pas de fantaisie. Juste un tableau : nombre de processus d’appli, taille de pool par processus, connexions de pic attendues, et marge sur max_connections.
Quand un nouveau job batch a été introduit, il devait déclarer son usage de connexions et implémenter un backoff exponentiel sur les tentatives de connexion. Ils avaient aussi des timeouts sensés : timeout de connexion, timeout de requête côté client, et timeouts idle côté serveur pour éviter les sessions zombies.
Un trimestre, le job batch a tourné tard, l’UI web a eu plus de charge, et un flapping réseau a causé des tempêtes de reconnexions. Le système n’est pas tombé. Il a dégradé : il y a eu des files, certaines requêtes ont time-outé rapidement, et la base est restée up. La pratique ennuyeuse — budgets explicites et timeouts appliqués — a évité une défaillance en cascade. Personne n’a reçu de médaille. C’est comme ça qu’on sait que ça a marché.
Erreurs courantes : symptômes → cause racine → correctif
1) « Trop de connexions » apparaît après avoir scalé les workers d’appli
Symptômes : erreurs soudaines après un déploiement ; CPU DB correct ; mémoire qui grimpe ; beaucoup de sessions idle.
Cause racine : les pools par processus se sont multipliés avec l’augmentation du nombre de workers. Le total a dépassé la capacité de la DB.
Fix : plafonner la taille du pool par worker ; réduire le nombre de workers ; ajouter PgBouncer/ProxySQL ; définir des budgets de connexions globaux et des alertes.
2) Pics de latence avec beaucoup de sockets TIME-WAIT
Symptômes : nombreuses connexions de courte durée ; trafic lourd en handshake ; timeouts intermittents.
Cause racine : connect/disconnect par requête ; pas de keepalive ; churn de threads/processus serveur.
Fix : activer le pooling côté appli ; garder les connexions chaudes ; tuner le thread cache MySQL ; envisager un pooler ; définir timeouts et retries avec backoff.
3) Postgres OOM même si les requêtes « ne sont pas si lourdes »
Symptômes : kernel OOM kills ; swap qui augmente ; nombreuses sessions idle ; VPS non réactif.
Cause racine : trop de backends ; empreinte mémoire par backend ; parfois work_mem trop grand combiné à des tris/hashes concurrents.
Fix : réduire max_connections avec un pooler ; diminuer work_mem ou limiter la concurrence ; arrêter de considérer max_connections comme du throughput.
4) Après l’ajout de PgBouncer, l’appli casse de façon étrange
Symptômes : erreurs de prepared statement ; paramètres de session non appliqués ; tables temporaires manquantes.
Cause racine : utilisation du transaction/statement pooling alors que l’appli dépend de la sémantique de session.
Fix : passer en session pooling ; ou refactoriser l’appli pour éviter l’état de session ; s’assurer que les drivers sont compatibles avec le mode de pooling.
5) « La base est lente » mais les connexions actives sont peu nombreuses
Symptômes : faible nombre de connexions ; temps de réponse élevés ; waits IO ou waits de verrous élevés.
Cause racine : pas un problème de connexions — contention de verrous, saturation IO, index manquants, transactions longues.
Fix : investiguer wait events, slow query logs, verrous ; optimiser schéma/requêtes ; ajouter du caching ; déplacer les jobs lourds hors du VPS.
6) La file du pool bloque les requêtes même à faible QPS
Symptômes : temps d’attente du pool élevé ; la DB montre peu de requêtes actives ; threads app bloqués en attente d’une connexion.
Cause racine : taille de pool trop petite pour la concurrence ; ou fuites de connexions (non retournées) ; ou transactions longues bloquant les connexions.
Fix : détecter les fuites ; définir des timeouts de checkout ; dimensionner correctement le pool ; réduire la portée des transactions ; ajouter des coupe-circuits.
Listes de vérification / plan étape par étape
Étape par étape : décider si vous avez besoin d’un pooler externe sur un VPS
- Comptez les workers d’appli sur le VPS/hosts applicatifs et multipliez par la taille de pool configurée. Si vous ne savez pas, vous avez déjà un problème.
- Mesurez les connexions réelles (
pg_stat_activityou variables d’état MySQL). - Vérifiez la marge mémoire et le swap. Si le swap est non négligeable sous charge, traitez les connexions comme suspectes.
- Classez les connexions : idle vs active vs idle-in-transaction.
- Si Postgres et beaucoup d’idles : ajoutez PgBouncer, plafonnez agressivement les connexions serveur, et appliquez des caps côté appli.
- Si MySQL et churn de threads/connexions élevé : corrigez la réutilisation côté appli, tunez le thread cache, et plafonnez les pools ; envisagez ProxySQL si vous avez beaucoup de clients.
- Définissez des timeouts qui échouent rapidement : connect timeout client, query timeout, et timeouts idle côté serveur.
- Mettez des alertes sur : connexions totales, usage swap, événements OOM, et temps d’attente du pool.
Étape par étape : « rendre stable ce soir » — atténuations
- Réduire temporairement la concurrence de l’appli (workers/threads).
- Réduire les tailles de pool dans la config appli. Redémarrer les processus d’appli pour appliquer.
- Si Postgres swappe : déployer PgBouncer avec un
default_pool_sizeconservateur et plafonner les connexions serveur. - Tuer les acteurs évidents : sessions idle-in-transaction ; jobs batch hors contrôle.
- Valider que les erreurs de connexion diminuent et que la latence se normalise.
- Puis seulement passer à l’optimisation des requêtes et à l’indexation.
Étape par étape : dimensionner les connexions de façon sensée sur un VPS
- Commencez par la machine : combien de RAM peut la base utiliser en toute sécurité sans swap ?
- Estimez le coût par connexion : pour Postgres, mesurez le RSS par backend sous une charge représentative.
- Fixez un plafond dur : les connexions serveur Postgres devraient souvent être bien inférieures aux connexions clientes d’application ; c’est pour ça que les poolers existent.
- Privilégiez la mise en file plutôt que le crash : une file dans le pool est gênante ; un OOM est un redémarrage de service avec des étapes supplémentaires.
FAQ
1) Le pooling de connexions est-il toujours nécessaire sur un VPS ?
Non. Si vous avez un seul processus applicatif avec un pool petit et stable et peu de churn, vous n’avez peut-être pas besoin d’un pooler externe. Mais vous avez toujours besoin de limites et de timeouts.
2) Pourquoi PostgreSQL « a-t-il besoin » de PgBouncer si souvent ?
Parce que chaque connexion correspond à un processus backend avec un coût mémoire significatif. Le pooling vous permet de servir de nombreuses sessions clientes avec moins de backends serveur, ce dont un VPS limité en RAM a précisément besoin.
3) Pourquoi ne pas simplement augmenter max_connections de PostgreSQL ?
Vous pouvez, mais c’est souvent la mauvaise opération sur un VPS. Plus de connexions augmentent la pression mémoire et le changement de contexte. Si vous êtes déjà proche du swap, augmenter le plafond peut transformer « erreurs » en « panne ».
4) MySQL a-t-il un équivalent à PgBouncer ?
Écosystème différent, idée similaire. ProxySQL est couramment utilisé comme couche proxy/pooler, et beaucoup d’applications comptent sur le pooling côté client. Le besoin d’un proxy dépend du nombre de clients indépendants et de la discipline des pools applicatifs.
5) Quel est le meilleur mode de pooling dans PgBouncer ?
Le session pooling est le plus sûr pour la compatibilité. Le transaction pooling est puissant mais casse les hypothèses de session ; ne l’utilisez que si votre appli est sans état au niveau de la session et que vous avez testé les cas limites.
6) Le pooling peut-il masquer des requêtes lentes ?
Oui. Il peut rendre le système « stable » alors que les requêtes s’empilent derrière un petit nombre de connexions serveur. C’est mieux que de planter, mais vous devez tout de même corriger les requêtes lentes ou la contention sur les verrous.
7) Ma base affiche beaucoup de connexions idle. Est-ce mauvais ?
Pas intrinsèquement. C’est problématique quand les connexions idle consomment des ressources que vous ne pouvez pas vous permettre (Postgres sur un petit VPS) ou quand elles vous poussent à l’épuisement de max_connections. Idle, c’est bien ; idle non borné, non.
8) Sur quoi dois-je alerter pour détecter ça tôt ?
Au minimum : connexions totales, connexions actives, usage du swap, événements OOM killer, temps d’attente du pool (métriques applicatives), et taux d’erreurs de connexion/timeouts.
9) Dois-je pooler dans l’appli et aussi utiliser un pooler externe ?
Parfois. Ça peut bien marcher si les pools applicatifs sont petits et que le pooler externe applique un plafond global. Le danger est la double mise en file et la latence confuse. Gardez le design simple et observable.
10) Une citation pour la route — quel état d’esprit fiabilité adopter ?
Idée paraphrasée, attribuée à John Allspaw : la fiabilité vient de la conception de systèmes qui échouent de manière contrôlée, pas de l’espoir qu’ils n’échouent jamais.
Conclusion : prochaines étapes pratiques
Sur un VPS, le pooling de connexions est moins une question d’extraire des performances que d’éviter une auto-attaque par déni de service. PostgreSQL exige généralement du pooling plus tôt parce que les connexions serveur sont plus lourdes. MySQL vous donne souvent plus de marge, mais il punira quand même le churn et le fan-out non borné.
Faites ceci ensuite :
- Écrivez votre budget de connexions : workers d’appli × taille de pool, plus les jobs background.
- Mesurez la réalité : connexions actuelles, idle vs active, et usage mémoire sous charge.
- Plafonnez et mettez en file : préférez une file de pool contrôlée à une spirale mémoire incontrôlée.
- Ajoutez un pooler quand les comptes l’indiquent : PgBouncer pour Postgres, ou une stratégie proxy pour MySQL si le nombre de clients est chaotique.
- Instrumentez : alertez sur connexions, swap, et temps d’attente du pool. La première fois que vous l’attraperez tôt, l’effort sera rentabilisé.