🔝 Retour au Sommaire
Imaginez un restaurant avec seulement 10 tables. Si 100 clients arrivent en même temps, le chaos s'installe : certains attendent debout, les serveurs sont débordés, la cuisine est saturée. C'est exactement ce qui se passe lors d'un connection storm (tempête de connexions) dans PostgreSQL.
Contrairement à ce qu'on pourrait penser, PostgreSQL ne gère pas des milliers de connexions simultanées facilement. Chaque connexion consomme des ressources (mémoire, CPU) et au-delà d'un certain seuil, les performances s'effondrent.
Dans ce chapitre, vous apprendrez :
- Pourquoi trop de connexions posent problème
- Comment détecter un connection storm
- Le concept de connection pooling
- PgBouncer : la solution la plus populaire
- Les bonnes pratiques pour gérer les connexions
PostgreSQL utilise une architecture process-per-connection : chaque connexion client = un processus backend distinct.
┌─────────────┐
│ Client 1 │─────┐
└─────────────┘ │
▼
┌─────────────┐ ┌──────────────────┐
│ Client 2 │──▶│ PostgreSQL │
└─────────────┘ │ (Postmaster) │
└──────────────────┘
┌─────────────┐ │ │
│ Client 3 │─────┘ │
└─────────────┘ │
▼
┌──────────────────┐
│ Backend 1 (PID) │
│ Backend 2 (PID) │
│ Backend 3 (PID) │
└──────────────────┘
Conséquence : Chaque connexion crée un processus système complet avec :
- Mémoire propre (~10 MB par connexion)
- Ressources CPU pour la gestion
- Overhead de synchronisation entre processus
-- Voir la mémoire utilisée par connexion
SELECT
pid,
usename,
application_name,
pg_size_pretty(
pg_backend_memory_contexts.total_bytes
) AS memory_used
FROM pg_stat_activity
JOIN pg_backend_memory_contexts ON pg_stat_activity.pid = pg_backend_memory_contexts.pid
ORDER BY pg_backend_memory_contexts.total_bytes DESC
LIMIT 10; Coûts typiques par connexion :
- Mémoire de base : ~5-10 MB
- Mémoire work_mem (si utilisée) : 64 MB par défaut
- CPU : Context switching, gestion des verrous
- Connexion/déconnexion : ~5-10 ms par cycle
-- Voir les connexions configurées
SHOW max_connections;
-- Valeur par défaut : 100
-- Voir les connexions actuellement utilisées
SELECT count(*) FROM pg_stat_activity;Calcul de mémoire potentielle :
Configuration :
- max_connections = 200
- work_mem = 64 MB
Mémoire potentielle = 200 × (10 MB base + 64 MB work_mem × 2-4 opérations)
= 200 × (10 + 128-256 MB)
= 27 GB - 53 GB de RAM !
Un connection storm se produit quand un grand nombre de connexions sont créées en très peu de temps.
Scénarios typiques :
- Application redémarre : Tous les workers se reconnectent en même temps
- Traffic spike : Pic soudain de trafic (Black Friday, annonce virale)
- Retry loops : Application retry automatiquement après échec, créant une boucle
- Load balancer mal configuré : Ouvre trop de connexions
- Fonction serverless : Chaque invocation = nouvelle connexion
FATAL: sorry, too many clients already
FATAL: remaining connection slots are reserved for non-replication superuser connections
-- Voir le nombre de connexions actives
SELECT
count(*) FILTER (WHERE state = 'active') as active,
count(*) FILTER (WHERE state = 'idle') as idle,
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
count(*) as total
FROM pg_stat_activity;Résultat problématique :
active | idle | idle_in_transaction | total
--------+------+---------------------+-------
450 | 200 | 50 | 700
Si total approche max_connections (ex: 800 connexions pour max_connections=800), vous êtes en saturation !
# CPU élevé pour gestion des processus
top
# Charge : load average > nombre de cœurs × 3
# Beaucoup de processus PostgreSQL
ps aux | grep postgres | wc -l
# Résultat : 700+ processusSELECT
datname,
count(*) as connections,
max(now() - backend_start) as oldest_connection
FROM pg_stat_activity
WHERE pid != pg_backend_pid()
GROUP BY datname
ORDER BY connections DESC; Exemple de résultat :
datname | connections | oldest_connection
-----------+-------------+-------------------
mydb | 650 | 02:34:56.789012
postgres | 10 | 01:23:45.678901
SELECT
usename,
application_name,
count(*) as connection_count,
count(*) FILTER (WHERE state = 'active') as active,
count(*) FILTER (WHERE state = 'idle') as idle
FROM pg_stat_activity
GROUP BY usename, application_name
ORDER BY connection_count DESC; Résultat typique d'un problème :
usename | application_name | connection_count | active | idle
-----------+------------------+------------------+--------+------
app_user | MyWebApp | 580 | 45 | 535
app_user | BackgroundJob | 80 | 12 | 68
Analyse : 580 connexions pour une application web = PROBLÈME !
- Pourquoi autant de connexions idle ?
- L'application ouvre-t-elle trop de connexions ?
- Y a-t-il des connection leaks ?
-- Connexions idle depuis longtemps
SELECT
pid,
usename,
application_name,
client_addr,
backend_start,
state,
state_change,
now() - state_change as idle_duration
FROM pg_stat_activity
WHERE state = 'idle'
AND now() - state_change > interval '10 minutes'
ORDER BY state_change;Connexions "idle in transaction" (très problématiques) :
SELECT
pid,
usename,
application_name,
now() - xact_start as transaction_duration,
query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - xact_start > interval '5 minutes';-- Connexions par état (pour graphique)
SELECT
state,
count(*) as count
FROM pg_stat_activity
GROUP BY state; Résultat à exporter vers Grafana :
state | count
----------------------+-------
active | 45
idle | 520
idle in transaction | 15
disabled | 0
-- Créer une fonction d'alerte
CREATE OR REPLACE FUNCTION check_connection_saturation()
RETURNS TABLE(
max_conn int,
current_conn bigint,
percent_used numeric,
status text
) AS $$
BEGIN
RETURN QUERY
SELECT
current_setting('max_connections')::int as max_conn,
count(*)::bigint as current_conn,
round(100.0 * count(*) / current_setting('max_connections')::numeric, 2) as percent_used,
CASE
WHEN count(*) > current_setting('max_connections')::numeric * 0.9 THEN 'CRITICAL'
WHEN count(*) > current_setting('max_connections')::numeric * 0.75 THEN 'WARNING'
ELSE 'OK'
END as status
FROM pg_stat_activity;
END;
$$ LANGUAGE plpgsql;
-- Utiliser
SELECT * FROM check_connection_saturation();Le connection pooling est comme un système de location de voitures :
- Au lieu que chaque personne achète sa propre voiture (connexion dédiée)
- On partage un parc de voitures (pool de connexions)
- Quand vous avez besoin, vous louez une voiture
- Quand vous avez fini, vous la rendez au pool
SANS POOLING :
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client1 │────│ Conn1 │────│ │
└─────────┘ └─────────┘ │ │
┌─────────┐ ┌─────────┐ │ SGBD │
│ Client2 │────│ Conn2 │────│ │
└─────────┘ └─────────┘ │ (500 │
┌─────────┐ ┌─────────┐ │ conn) │
│ Client3 │────│ Conn3 │────│ │
└─────────┘ └─────────┘ └─────────┘
... ...
┌─────────┐ ┌─────────┐
│ Client500─── │ Conn500 │
└─────────┘ └─────────┘
Problème : 500 connexions = Saturation !
AVEC POOLING :
┌─────────┐
│ Client1 │────┐
└─────────┘ │
┌─────────┐ │ ┌──────────────┐ ┌─────────┐
│ Client2 │────┼─▶│ Connection │───│ Conn1 │
└─────────┘ │ │ Pool │───│ Conn2 │───▶ SGBD
┌─────────┐ │ │ (PgBouncer) │───│ Conn3 │ (3 conn)
│ Client3 │────┘ └──────────────┘ │ ... │
└─────────┘ │ Conn20 │
... └─────────┘
┌─────────┐
│ Client500
└─────────┘
Solution : 500 clients → 20 connexions réelles !
| Aspect | Sans Pooling | Avec Pooling |
|---|---|---|
| Connexions SGBD | 1 par client | Partagées |
| Mémoire PostgreSQL | 500 × 10MB = 5GB | 20 × 10MB = 200MB |
| Overhead connexion | 5-10ms par requête | 0.1ms (réutilisation) |
| Scalabilité | Limitée (max_connections) | Illimitée côté app |
| Performances | Dégradées > 200 conn | Stables |
Client connect → Get connection → Keep connection → Client disconnect
(toute la session)
Caractéristiques :
- Une connexion = une session client complète
- TOUTES les fonctionnalités PostgreSQL disponibles
- Prepared statements, temporary tables, cursors : OK
- Moins d'économie de connexions
Cas d'usage : Applications avec sessions longues, utilisant features avancées.
Client → BEGIN → Get connection → COMMIT → Release connection
Caractéristiques :
- Connexion rendue après chaque transaction
- Maximum d'économie de connexions
- Limitations : Pas de prepared statements entre transactions
- Pas de session state (SET commands limités)
Cas d'usage : APIs web, microservices (recommandé).
Client → Query → Get connection → Execute → Release connection
Caractéristiques :
- Connexion rendue après chaque requête
- Économie maximale mais limitations majeures
- Pas de transactions multi-requêtes
- Rarement utilisé
Cas d'usage : Très rare, seulement pour read-only queries simples.
PgBouncer est un connection pooler léger et ultra-performant pour PostgreSQL. C'est la solution la plus utilisée en production.
Avantages :
- ✅ Très léger (< 10 MB RAM)
- ✅ Très rapide (~2 MB/s overhead)
- ✅ Facile à configurer
- ✅ Support transaction et session pooling
- ✅ Peut gérer 10,000+ connexions clients
# Ubuntu/Debian
sudo apt update
sudo apt install pgbouncer
# CentOS/RHEL
sudo yum install pgbouncer
# macOS
brew install pgbouncer
# Vérifier l'installation
pgbouncer --version[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
# Mode de pooling (RECOMMANDÉ pour web apps)
pool_mode = transaction
# Connexions
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
# Limites connexions
max_client_conn = 10000 # Connexions clients max
default_pool_size = 25 # Connexions PostgreSQL par base
min_pool_size = 5 # Connexions minimum gardées
reserve_pool_size = 5 # Connexions de réserve
reserve_pool_timeout = 3 # Timeout pour obtenir connexion (secondes)
# Logs
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
# Performance
max_db_connections = 100 # Limite totale connexions PostgreSQL
ignore_startup_parameters = extra_float_digits
# Sécurité
admin_users = postgres
stats_users = postgres, monitoring_user Explication des paramètres clés :
| Paramètre | Description | Valeur recommandée |
|---|---|---|
pool_mode |
transaction|session|statement | transaction |
max_client_conn |
Connexions clients max | 10000 |
default_pool_size |
Connexions PostgreSQL par DB | 25 |
reserve_pool_size |
Connexions réserve | 5 |
# Format : "username" "password" (hashed avec SCRAM-SHA-256)
# Obtenir le hash :
echo -n "passwordMD5$(echo -n 'passwordusername' | md5sum | awk '{print $1}')" | md5sum
# Ou utiliser le format SCRAM (PostgreSQL 10+)
"app_user" "SCRAM-SHA-256$4096:salt:hash:serverkey"
# Pour développement (INSECURE) :
"app_user" "plain_password"Méthode recommandée : Copier depuis PostgreSQL
-- Dans PostgreSQL, voir le hash du mot de passe
SELECT rolname, rolpassword
FROM pg_authid
WHERE rolname = 'app_user';
-- Copier ce hash dans userlist.txt# Démarrer
sudo systemctl start pgbouncer
# Activer au démarrage
sudo systemctl enable pgbouncer
# Vérifier le statut
sudo systemctl status pgbouncer
# Voir les logs
sudo tail -f /var/log/postgresql/pgbouncer.log# Connexion client → PgBouncer (port 6432)
psql -h localhost -p 6432 -U app_user -d mydb
# PgBouncer → PostgreSQL (port 5432)
# (géré automatiquement par PgBouncer)# Se connecter à la console d'admin
psql -h localhost -p 6432 -U postgres pgbouncer
# Commandes disponibles :
pgbouncer=# SHOW HELP;Commandes utiles :
-- Voir les statistiques
SHOW STATS;
-- Voir les pools de connexions
SHOW POOLS;
-- Voir les clients connectés
SHOW CLIENTS;
-- Voir les connexions PostgreSQL
SHOW SERVERS;
-- Voir la configuration
SHOW CONFIG;
-- Recharger la configuration (sans redémarrage)
RELOAD;
-- Suspendre les connexions (maintenance)
PAUSE;
-- Reprendre
RESUME;
-- Tuer une connexion client
KILL client_addr; database | user | cl_active | cl_waiting | sv_active | sv_idle | sv_used | sv_tested | sv_login | maxwait | pool_mode
-----------+----------+-----------+------------+-----------+---------+---------+-----------+----------+---------+-----------
mydb | app_user | 12 | 0 | 12 | 13 | 0 | 0 | 0 | 0 | transaction
Interprétation :
cl_active: 12 clients actifscl_waiting: 0 clients en attente (bon signe !)sv_active: 12 connexions PostgreSQL activessv_idle: 13 connexions PostgreSQL idle (disponibles)- 25 connexions PostgreSQL au total (12 + 13) =
default_pool_size
database | total_xact_count | total_query_count | total_received | total_sent | total_xact_time | total_query_time | total_wait_time | avg_xact_count | avg_query_count | avg_recv | avg_sent | avg_xact_time | avg_query_time | avg_wait_time
-----------+------------------+-------------------+----------------+------------+-----------------+------------------+-----------------+----------------+-----------------+----------+----------+---------------+----------------+---------------
mydb | 1234567 | 2345678 | 123456789 | 98765432 | 123456789 | 234567890 | 1234 | 123 | 234 | 12345 | 9876 | 123 | 234 | 1
Métriques importantes :
total_xact_count: Nombre de transactions traitéesavg_wait_time: Temps d'attente moyen (doit être < 1ms)total_query_time/total_query_count: Latence moyenne
Formule de base :
default_pool_size = Nombre de cœurs CPU × 2 à 4
Exemples :
| Serveur | Cœurs CPU | default_pool_size | Justification |
|---|---|---|---|
| Petit (dev) | 2 | 8-10 | 2 × 4 |
| Moyen (prod) | 8 | 20-30 | 8 × 3 |
| Grand (prod) | 32 | 80-100 | 32 × 3 |
Règle importante : Plus n'est pas toujours mieux !
- Trop de connexions PostgreSQL = contention, context switching
- Optimal : Garder CPU à 70-80% d'utilisation
[databases]
# Base principale (beaucoup de traffic)
mydb = host=localhost port=5432 dbname=mydb pool_size=50
# Base analytics (requêtes lourdes, peu de concurrence)
analytics = host=localhost port=5432 dbname=analytics pool_size=10 pool_mode=session
# Base de reporting (read-only)
reports = host=replica.example.com port=5432 dbname=mydb pool_size=20# ❌ NE FONCTIONNE PAS avec transaction pooling
conn = psycopg2.connect("host=pgbouncer port=6432 dbname=mydb")
cursor = conn.cursor()
cursor.execute("PREPARE myplan AS SELECT * FROM users WHERE id = $1")
cursor.execute("EXECUTE myplan (123)") # ERREUR : prepared statement n'existe pas Solution : Ne pas utiliser PREPARE/EXECUTE explicites. Les drivers modernes gèrent ça automatiquement avec le protocole Extended Query.
# ✅ FONCTIONNE : Utilise le protocole Extended Query
cursor.execute("SELECT * FROM users WHERE id = %s", (123,))-- ❌ NE FONCTIONNE PAS : table perdue après transaction
BEGIN;
CREATE TEMP TABLE temp_data (id int);
INSERT INTO temp_data VALUES (1), (2);
COMMIT;
-- Nouvelle transaction = nouvelle connexion = table perdue !
SELECT * FROM temp_data; -- ERROR: relation does not existSolution 1 : Tout faire dans une transaction
BEGIN;
CREATE TEMP TABLE temp_data (id int);
INSERT INTO temp_data VALUES (1), (2);
-- Utiliser la table
SELECT * FROM temp_data;
-- Nettoyage automatique
COMMIT;Solution 2 : Utiliser CTE au lieu de TEMP TABLE
WITH temp_data AS (
SELECT generate_series(1, 100) as id
)
SELECT * FROM temp_data WHERE id < 10;-- ❌ PEUT NE PAS FONCTIONNER
SET work_mem = '256MB'; -- Perdu après transaction
-- ✅ FONCTIONNE : SET LOCAL dans transaction
BEGIN;
SET LOCAL work_mem = '256MB';
-- Requête lourde ici
COMMIT;Configuration PgBouncer pour autoriser certains SET :
[pgbouncer]
# Ignorer ces paramètres (ne pas les réinitialiser)
ignore_startup_parameters = extra_float_digits,options
# Autoriser ces commandes SET
server_reset_query = DISCARD ALL
server_reset_query_always = 0 -- Dans la console PgBouncer
SHOW POOLS;
-- Métriques clés :
-- cl_waiting > 0 : Clients en attente → Augmenter pool_size
-- sv_active ≈ pool_size : Pool saturé → Augmenter pool_size
-- avg_wait_time > 1ms : Latence élevée → Problème de sizing# Installer pgbouncer_exporter
wget https://github.com/prometheus-community/pgbouncer_exporter/releases/download/v0.7.0/pgbouncer_exporter-0.7.0.linux-amd64.tar.gz
tar xvfz pgbouncer_exporter-0.7.0.linux-amd64.tar.gz
cd pgbouncer_exporter-0.7.0.linux-amd64
# Lancer l'exporter
./pgbouncer_exporter \
--pgBouncer.connectionString="postgres://stats_user:password@localhost:6432/pgbouncer?sslmode=disable"Dashboards Grafana recommandés :
- PgBouncer Dashboard (ID: 12421)
Caractéristiques :
- Plus de fonctionnalités que PgBouncer (load balancing, replication)
- Plus complexe à configurer
- Plus de overhead
Cas d'usage : Quand vous avez besoin de load balancing automatique + pooling.
# Installation
sudo apt install pgpool2
# Configuration de base
/etc/pgpool2/pgpool.conffrom psycopg2 import pool
# Créer un pool de connexions
connection_pool = pool.SimpleConnectionPool(
minconn=5, # Minimum 5 connexions
maxconn=20, # Maximum 20 connexions
host="localhost",
database="mydb",
user="app_user",
password="secret"
)
# Utiliser une connexion
conn = connection_pool.getconn()
try:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
finally:
# IMPORTANT : Rendre la connexion au pool
connection_pool.putconn(conn)const { Pool } = require('pg');
const pool = new Pool({
host: 'localhost',
database: 'mydb',
user: 'app_user',
password: 'secret',
max: 20, // Maximum 20 connexions
idleTimeoutMillis: 30000, // Timeout connexion idle
connectionTimeoutMillis: 2000,
});
// Utiliser le pool
pool.query('SELECT * FROM users', (err, result) => {
console.log(result.rows);
});HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("app_user");
config.setPassword("secret");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
HikariDataSource ds = new HikariDataSource(config);
// Utiliser
Connection conn = ds.getConnection();
try {
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// ...
} finally {
conn.close(); // Rend au pool
}Avantage : Pas de composant externe.
Inconvénient : Chaque instance d'application a son propre pool.
Pour PostgreSQL sur AWS RDS :
Application → RDS Proxy → RDS PostgreSQL
(10000) (100) (100 connexions)
Avantages :
- Géré par AWS (pas de maintenance)
- Connection pooling automatique
- Failover automatique
- Intégration IAM
Configuration :
# Via AWS CLI
aws rds create-db-proxy \
--db-proxy-name mydb-proxy \
--engine-family POSTGRESQL \
--auth {...} \
--role-arn {...} \
--vpc-subnet-ids subnet-xxx subnet-yyyAzure propose également un pooling intégré.
Ces services cloud incluent du connection pooling natif optimisé pour serverless.
# ❌ MAUVAIS : Connection leak
def get_users():
conn = psycopg2.connect(...)
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
return cursor.fetchall()
# Connexion jamais fermée !
# ✅ BON : Utiliser context manager
def get_users():
with psycopg2.connect(...) as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM users")
return cursor.fetchall()
# Connexion fermée automatiquement# Application
conn = psycopg2.connect(
host="localhost",
database="mydb",
connect_timeout=5, # Timeout connexion
options="-c statement_timeout=30000" # Timeout requête (30s)
)-- PostgreSQL
-- Dans postgresql.conf
idle_in_transaction_session_timeout = 60000 # 60 secondes
statement_timeout = 30000 # 30 secondes # PgBouncer
[pgbouncer]
query_timeout = 30
query_wait_timeout = 5
idle_transaction_timeout = 60 # ❌ MAUVAIS : Connexion gardée toute la durée de vie de l'app
class MyApp:
def __init__(self):
self.conn = psycopg2.connect(...) # Garde la connexion
def do_something(self):
cursor = self.conn.cursor()
# ...
# ✅ BON : Connexion par requête (avec pool)
class MyApp:
def __init__(self):
self.pool = create_pool(...)
def do_something(self):
with self.pool.get_connection() as conn:
cursor = conn.cursor()
# ...
# Connexion rendue au poolRègle empirique :
Connexions PostgreSQL = Nombre de cœurs CPU × 2 à 4
Exemples :
- Serveur 4 cœurs : 10-15 connexions
- Serveur 8 cœurs : 20-30 connexions
- Serveur 16 cœurs : 40-60 connexions
Validation : Surveiller CPU et load average
- Si CPU < 70% : Peut augmenter connexions
- Si CPU > 90% : Trop de connexions, réduire
# PgBouncer : Différents pools pour différents usages
[databases]
# API web : Transaction pooling, petit pool
api_db = host=localhost dbname=mydb pool_size=25 pool_mode=transaction
# Background jobs : Session pooling, pool moyen
jobs_db = host=localhost dbname=mydb pool_size=15 pool_mode=session
# Analytics : Session pooling, grand pool
analytics_db = host=replica dbname=mydb pool_size=50 pool_mode=session# Utiliser pgbench pour simuler charge
pgbench -h localhost -p 6432 -U app_user -c 100 -j 4 -T 300 mydb
# -c 100 : 100 clients simultanés
# -j 4 : 4 threads
# -T 300 : Test de 5 minutesSymptôme :
FATAL: sorry, too many clients already
Diagnostic :
-- Voir les connexions actuelles
SELECT count(*), state
FROM pg_stat_activity
GROUP BY state;
SHOW max_connections;Solutions :
- Immédiat : Tuer les connexions idle
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
AND now() - state_change > interval '10 minutes';- Court terme : Augmenter max_connections
-- Dans postgresql.conf
max_connections = 200 # Au lieu de 100
-- Redémarrer
sudo systemctl restart postgresql- Long terme : Implémenter PgBouncer
# Permet 10000 clients avec seulement 25 connexions PostgreSQL
max_client_conn = 10000
default_pool_size = 25 Symptôme dans PgBouncer :
SHOW POOLS;
# cl_waiting > 0 constamment
Causes :
- Pool trop petit
- Requêtes lentes monopolisent connexions
- Transactions non fermées
Diagnostic :
-- Voir les requêtes actives longues
SELECT pid, now() - query_start as duration, query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY duration DESC
LIMIT 10; Solutions :
- Augmenter pool_size
default_pool_size = 50 # Au lieu de 25-
Optimiser les requêtes lentes (voir chapitre Query Tuning)
-
Configurer timeouts
query_timeout = 30
query_wait_timeout = 5 Symptôme : Nombre de connexions augmente continuellement.
Diagnostic :
# Script de test pour détecter leaks
import psycopg2
import time
for i in range(100):
conn = psycopg2.connect(...)
# Oublier de fermer : conn.close()
time.sleep(1)
# Résultat : 100 connexions ouvertes !Solution : Toujours utiliser context managers ou try/finally
# ✅ BON
try:
conn = psycopg2.connect(...)
# Utiliser conn
finally:
conn.close()
# Ou mieux : context manager
with psycopg2.connect(...) as conn:
# Utiliser conn
pass
# Fermeture automatiqueErreurs communes :
# Erreur : permission denied
# Solution : Vérifier les permissions
sudo chown pgbouncer:pgbouncer /etc/pgbouncer/*
sudo chmod 600 /etc/pgbouncer/userlist.txt
# Erreur : could not connect to server
# Solution : Vérifier pg_hba.conf
# Ajouter dans PostgreSQL /etc/postgresql/*/main/pg_hba.conf :
host all all 127.0.0.1/32 scram-sha-256
# Recharger PostgreSQL
sudo systemctl reload postgresql-- Alerte si > 80% de max_connections
SELECT
current_setting('max_connections')::int as max,
count(*) as current,
round(100.0 * count(*) / current_setting('max_connections')::int, 2) as percent
FROM pg_stat_activity;Seuil d'alerte : > 80%
-- Alerte si > 10 connexions idle in transaction
SELECT count(*)
FROM pg_stat_activity
WHERE state = 'idle in transaction'; Seuil d'alerte : > 10 connexions
-- Dans console PgBouncer
SHOW POOLS;
-- Alerte si cl_waiting > 0 pendant > 1 minuteRequêtes Prometheus :
# Nombre de connexions PostgreSQL
pg_stat_activity_count
# Connexions par état
pg_stat_activity_count{state="active"}
pg_stat_activity_count{state="idle"}
pg_stat_activity_count{state="idle in transaction"}
# PgBouncer : Clients en attente
pgbouncer_pools_cl_waiting
# PgBouncer : Connexions serveur
pgbouncer_pools_sv_active
pgbouncer_pools_sv_idle groups:
- name: postgresql_connections
rules:
- alert: PostgreSQLTooManyConnections
expr: pg_stat_activity_count / pg_settings_max_connections > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "PostgreSQL connections above 80%"
- alert: PostgreSQLIdleInTransaction
expr: pg_stat_activity_count{state="idle in transaction"} > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Too many idle in transaction connections"
- alert: PgBouncerClientsWaiting
expr: pgbouncer_pools_cl_waiting > 0
for: 1m
labels:
severity: critical
annotations:
summary: "PgBouncer has clients waiting for connections"- PgBouncer installé et configuré
-
pool_mode = transaction(sauf besoin spécifique) -
default_pool_sizedimensionné (CPU × 2-4) -
max_client_connsuffisant pour charge attendue - Authentification configurée (scram-sha-256)
- Application modifiée pour se connecter via PgBouncer
- Tests de charge effectués (pgbench)
- Monitoring configuré (Prometheus + Grafana)
- Alertes configurées
- Documentation des runbooks
- Surveiller métriques pendant 48h
- Vérifier absence de cl_waiting
- Vérifier latence acceptable
- Ajuster pool_size si nécessaire
- Vérifier absence de connection leaks
- Documenter toute anomalie
# CONNEXIONS
max_connections = 100 # Réduit (PgBouncer gère)
superuser_reserved_connections = 5
# TIMEOUTS
idle_in_transaction_session_timeout = 60000 # 60 secondes
statement_timeout = 30000 # 30 secondes
# Pas besoin de plus avec PgBouncer[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
# Connection pooling
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = transaction
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
# Pool sizing (adapter selon CPU)
max_client_conn = 10000
default_pool_size = 25 # Pour serveur 8 cœurs
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
# Timeouts
query_timeout = 30
query_wait_timeout = 5
client_idle_timeout = 600 # 10 minutes
server_idle_timeout = 600
idle_transaction_timeout = 60
# Performance
max_db_connections = 100
server_check_delay = 30
server_check_query = SELECT 1
# Logging
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
admin_users = postgres
stats_users = postgres, monitoring # config.py
DATABASE_CONFIG = {
'host': 'localhost',
'port': 6432, # PgBouncer, pas PostgreSQL direct !
'database': 'mydb',
'user': 'app_user',
'password': 'secret',
'connect_timeout': 5,
'options': '-c statement_timeout=30000'
}
# app.py
from psycopg2 import pool
# Créer un pool applicatif (optionnel avec PgBouncer)
app_pool = pool.SimpleConnectionPool(5, 20, **DATABASE_CONFIG)
def get_users():
conn = app_pool.getconn()
try:
with conn.cursor() as cur:
cur.execute("SELECT * FROM users")
return cur.fetchall()
finally:
app_pool.putconn(conn)✅ Connection storms : Saturation par trop de connexions simultanées
✅ PostgreSQL = process-per-connection : Chaque connexion coûte ~10 MB RAM + CPU
✅ Limite pratique : ~200-300 connexions sans pooling
✅ Connection pooling : Partage intelligent des connexions
✅ PgBouncer : Solution de référence, léger et performant
✅ Transaction pooling : Mode recommandé pour web apps (quelques limitations)
✅ Dimensionnement : pool_size = CPU × 2 à 4
✅ Monitoring : Surveiller cl_waiting, connections count, idle in transaction
✅ Timeouts : Configuration critique pour éviter connexions zombies
✅ Tests de charge : Valider avant production avec pgbench
Le connection pooling est essentiel pour toute application PostgreSQL en production. Sans pooling :
- Vous êtes limité à ~200-300 utilisateurs simultanés
- Chaque connexion consomme des ressources précieuses
- Les connection storms peuvent faire tomber votre base
Avec PgBouncer :
- Vous pouvez gérer 10,000+ utilisateurs avec 25 connexions PostgreSQL
- Overhead minimal (~0.1ms de latence)
- Configuration simple et maintenance facile
- Résilience accrue face aux pics de charge
L'investissement temps pour configurer PgBouncer (1-2 heures) est largement compensé par :
- Économies de ressources (RAM, CPU)
- Meilleure scalabilité
- Stabilité accrue en production
- Coûts d'infrastructure réduits
PgBouncer n'est pas une optimisation optionnelle : c'est un composant standard de toute infrastructure PostgreSQL professionnelle.
Fin du chapitre 19.4 - Troubleshooting et Crises