Le HPA résout un problème (pas assez de pods pour la charge) en créant un autre, rarement anticipé : chaque pod supplémentaire ouvre son propre pool de connexions vers la base de données, et une base Postgres a une limite stricte et finie de connexions simultanées. Sous une charge qui déclenche justement le scale-up du HPA, la base peut atteindre cette limite avant même que le HPA n’ait fini de créer les pods censés absorber cette charge.
Le calcul qui ne tient pas à l’échelle
Une application qui ouvre un pool de 20 connexions par instance, avec un HPA configuré pour scaler jusqu’à 15 replicas, réclame potentiellement 300 connexions simultanées à la base, un chiffre qui dépasse largement le max_connections par défaut de Postgres (100 dans de nombreuses configurations). Le nombre de replicas est justement celui que le HPA choisit dynamiquement en fonction de la charge, ce qui rend ce plafond invisible tant que le trafic reste modéré, et brutal le jour où il ne l’est plus.
-- Le plafond dur qui casse tout au-delà,
-- souvent jamais dimensionné en fonction du HPA
SHOW max_connections;
-- 100
Le symptôme trompeur : des erreurs qui ressemblent à un bug applicatif
Quand la base atteint son plafond, les nouvelles connexions échouent avec une erreur (FATAL: too many connections), un message qui ressemble à un bug applicatif ou réseau plutôt qu’à un problème de dimensionnement. Le piège s’aggrave en cascade : les pods qui échouent à se connecter peuvent échouer leur sonde de readiness, ce qui les retire du service, réduit la capacité effective, et pousse le HPA à créer encore plus de replicas, chacun tentant à son tour d’ouvrir des connexions vers une base déjà saturée.
PgBouncer : un intermédiaire qui multiplexe les connexions
PgBouncer s’intercale entre les pods applicatifs et Postgres, en multiplexant un grand nombre de connexions applicatives sur un petit nombre de connexions réelles vers la base. En mode transaction (le plus courant), une connexion Postgres n’est occupée que pendant la durée d’une transaction, puis immédiatement rendue disponible pour un autre appelant, ce qui permet à des centaines de pods de partager quelques dizaines de connexions réelles.
# pgbouncer.ini : 20 connexions réelles vers Postgres
# absorbent des centaines de connexions applicatives
[databases]
myapp = host=postgres port=5432 dbname=myapp
[pgbouncer]
pool_mode = transaction
default_pool_size = 20
max_client_conn = 1000
# Les pods applicatifs se connectent à PgBouncer,
# jamais directement à Postgres
env:
- name: DATABASE_URL
value: "postgresql://pgbouncer:6432/myapp"
Ce que le mode transaction empêche
Le mode transaction de PgBouncer, le plus efficace en multiplexage, casse silencieusement certaines fonctionnalités qui supposent une session Postgres persistante : les prepared statements au niveau session, les variables de session personnalisées, ou LISTEN/NOTIFY. Une application qui utilise ces fonctionnalités sans le savoir découvre le problème sous forme d’erreurs difficiles à relier à PgBouncer, un compromis à valider explicitement avant de déployer ce mode plutôt qu’après l’incident.
À retenir
Le HPA scale les pods sans connaître la limite de connexions de la base qu’ils sollicitent, un angle mort qui reste invisible tant que la charge ne pousse jamais le nombre de replicas vers son maximum configuré. PgBouncer résout le problème en multiplexant les connexions plutôt qu’en les multipliant, au prix de restrictions réelles sur certaines fonctionnalités Postgres en mode transaction, à valider avant déploiement. Dimensionner max_connections en fonction du HPA (et pas l’inverse) est un des calculs de capacité qui se posent dès qu’une migration Kubernetes introduit de l’autoscaling devant une base de données à connexions limitées.