Configuración Avanzada de pgBouncer para PostgreSQL

pgBouncer es un proxy de pool de conexiones ligero para PostgreSQL que resuelve uno de los problemas más comunes en aplicaciones de alta concurrencia: el coste de crear y destruir conexiones al servidor PostgreSQL. Correctamente configurado, pgBouncer puede manejar miles de conexiones de aplicación usando solo unas decenas de conexiones al servidor PostgreSQL, reduciendo drásticamente el uso de memoria y mejorando el rendimiento global.

Requisitos Previos

  • Ubuntu 22.04/20.04, Debian 11, o CentOS/Rocky Linux 8+
  • PostgreSQL 13+ en el servidor de base de datos
  • Al menos 512 MB de RAM para pgBouncer (consume muy poca memoria)
  • Acceso de red entre la aplicación → pgBouncer → PostgreSQL

Instalación de pgBouncer

# Ubuntu/Debian
apt update && apt install -y pgbouncer

# CentOS/Rocky Linux
dnf install -y pgbouncer

# Verificar la versión instalada
pgbouncer --version

# Directorio de configuración
ls -la /etc/pgbouncer/

# Habilitar e iniciar el servicio
systemctl enable pgbouncer
systemctl start pgbouncer

Modos de Pool de Conexiones

pgBouncer ofrece tres modos con diferentes compromisos entre compatibilidad y eficiencia:

Session mode: la conexión al servidor se mantiene durante toda la sesión del cliente. Menos eficiente pero 100% compatible.

Transaction mode: la conexión al servidor solo se ocupa durante la transacción. Muy eficiente pero algunas características de PostgreSQL no son compatibles.

Statement mode: la conexión se reutiliza entre statements. Mayor eficiencia pero prácticamente incompatible con transacciones multi-statement.

# En la mayoría de aplicaciones de producción, transaction mode es el óptimo
# Verificar la compatibilidad de tu aplicación:

# Estas características NO funcionan en transaction mode:
# - SET SESSION commands que persisten entre transacciones
# - LISTEN/NOTIFY
# - PREPARE statements del lado servidor
# - Advisory locks con pg_try_advisory_lock y similares
# - WITH HOLD cursors

Configuración para Producción

cat > /etc/pgbouncer/pgbouncer.ini << 'EOF'
[databases]
# Formato: nombre_db = host=servidor port=puerto dbname=nombre_real
miapp = host=postgres.miempresa.local port=5432 dbname=miapp
miapp_readonly = host=postgres-replica.miempresa.local port=5432 dbname=miapp

# Wildcard: redirigir cualquier base de datos al servidor PostgreSQL
# * = host=postgres.miempresa.local port=5432

[pgbouncer]
# Dirección y puerto donde escucha pgBouncer
listen_addr = 0.0.0.0
listen_port = 5432

# Número máximo de conexiones al servidor PostgreSQL por pool
# Regla general: max_server_conn = (número de núcleos * 2) + número de discos
server_pool_size = 20

# Modo de pooling: session, transaction o statement
pool_mode = transaction

# Número máximo de conexiones de CLIENTES (aplicaciones)
# Puede ser muy alto ya que pgBouncer las gestiona eficientemente
max_client_conn = 1000

# Número por defecto de conexiones al servidor por pool
default_pool_size = 20

# Conexiones de reserva para situaciones de alta demanda
reserve_pool_size = 5

# Tiempo (segundos) antes de usar las conexiones de reserva
reserve_pool_timeout = 3

# Conexiones mínimas al servidor (para tenerlas listas)
min_pool_size = 5

# Tiempo máximo de vida de una conexión al servidor (segundos)
server_lifetime = 3600

# Tiempo de inactividad antes de cerrar una conexión al servidor
server_idle_timeout = 600

# Tiempo de inactividad antes de cerrar una conexión del cliente
client_idle_timeout = 300

# Tiempo máximo para que un cliente espere una conexión del pool
query_wait_timeout = 30

# Tiempo máximo para ejecutar una consulta (0 = sin límite)
query_timeout = 0

# Archivo de autenticación de usuarios
auth_file = /etc/pgbouncer/userlist.txt
auth_type = md5

# Archivo PID
pidfile = /var/run/pgbouncer/pgbouncer.pid

# Logs
logfile = /var/log/pgbouncer/pgbouncer.log
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1

# Estadísticas
stats_period = 60

# Ignorar los parámetros de startup que pueden causar problemas
# (útil cuando la aplicación envía parámetros que pgBouncer no entiende)
ignore_startup_parameters = extra_float_digits,application_name

# Verificar el estado del servidor PostgreSQL
server_check_query = SELECT 1
server_check_delay = 30

# Tiempo para establecer la conexión al servidor
server_connect_timeout = 15

# Tiempo máximo para la autenticación al servidor
server_login_retry = 15

# Usuario administrador para la consola de pgBouncer
admin_users = pgbouncer_admin
stats_users = pgbouncer_stats, prometheus_user
EOF
# Crear el archivo de usuarios (userlist.txt)
# Las contraseñas deben estar hasheadas en md5 o SCRAM-SHA-256

# Generar el hash md5 de la contraseña (formato requerido por pgBouncer)
# El hash es md5(contraseña + usuario)
echo -n "contraseña_seguramiapp_user" | md5sum
# Resultado: abc123def456...

# Formato del archivo userlist.txt
cat > /etc/pgbouncer/userlist.txt << 'EOF'
"miapp_user" "md5abc123def456abc123def456abc123"
"readonly_user" "md5def456abc123def456abc123def456"
"pgbouncer_admin" "md5admin_hash_aqui"
EOF

# Alternativa: usar la autenticación HBA de PostgreSQL
# En pgbouncer.ini cambiar:
# auth_type = hba
# auth_hba_file = /etc/pgbouncer/pg_hba.conf

# O usar auth_query para que pgBouncer verifique credenciales contra PostgreSQL
# En pgbouncer.ini:
# auth_type = scram-sha-256
# auth_user = pgbouncer_auth
# auth_query = SELECT usename, passwd FROM pg_shadow WHERE usename = $1

TLS y Autenticación

# Configurar TLS para las conexiones cliente → pgBouncer
cat >> /etc/pgbouncer/pgbouncer.ini << 'EOF'

[tls]
# TLS para conexiones de clientes (aplicación → pgBouncer)
client_tls_sslmode = require
client_tls_cert_file = /etc/pgbouncer/tls/pgbouncer.crt
client_tls_key_file = /etc/pgbouncer/tls/pgbouncer.key
client_tls_ca_file = /etc/pgbouncer/tls/ca.crt

# TLS para conexiones al servidor (pgBouncer → PostgreSQL)
server_tls_sslmode = require
server_tls_ca_file = /etc/pgbouncer/tls/postgres-ca.crt
EOF

# Generar certificados autofirmados para desarrollo
mkdir -p /etc/pgbouncer/tls
openssl req -new -x509 -days 3650 -nodes \
  -out /etc/pgbouncer/tls/pgbouncer.crt \
  -keyout /etc/pgbouncer/tls/pgbouncer.key \
  -subj "/CN=pgbouncer.miempresa.local"

chown -R pgbouncer:pgbouncer /etc/pgbouncer/tls
chmod 600 /etc/pgbouncer/tls/pgbouncer.key

Prepared Statements en Transaction Mode

# En transaction mode, los prepared statements del servidor no funcionan directamente
# Solución 1: Usar la opción max_prepared_statements (pgBouncer 1.21+)
cat >> /etc/pgbouncer/pgbouncer.ini << 'EOF'
# Número máximo de prepared statements por cliente que pgBouncer trackea
max_prepared_statements = 200
EOF

# Solución 2: Deshabilitar prepared statements en la aplicación
# En Django (Python):
# DATABASES = {'OPTIONS': {'options': '-c statement_timeout=30000'}}
# Y usar CONN_MAX_AGE = 0 para no reutilizar conexiones

# En psycopg2/psycopg3:
# conn = psycopg2.connect(dsn, prepared_statement_cache_size=0)

# En Node.js (node-postgres):
# const pool = new Pool({max: 20, statement_timeout: 30000})
# pool.query({text: '...', values: [...]})  # Nunca usar .prepare()

# Verificar que no hay prepared statements problemáticos
# Conectarse a la consola de pgBouncer
psql -h localhost -p 5432 -U pgbouncer_admin pgbouncer -c "SHOW PREPARED;"

Monitoreo y Estadísticas

# Conectarse a la consola de administración de pgBouncer
psql -h localhost -p 5432 -U pgbouncer_admin pgbouncer

# Comandos de la consola de pgBouncer:

# Ver estadísticas de los pools
SHOW POOLS;

# Ver estadísticas globales
SHOW STATS;

# Ver las conexiones de clientes
SHOW CLIENTS;

# Ver las conexiones al servidor PostgreSQL
SHOW SERVERS;

# Ver los databases configurados
SHOW DATABASES;

# Ver la configuración actual
SHOW CONFIG;

# Suspender/reanudar pgBouncer (para mantenimiento)
SUSPEND;
RESUME;

# Recargar la configuración sin reiniciar
RELOAD;

# Matar una conexión específica
KILL miapp;
# Exportar métricas en formato Prometheus (pgBouncer Exporter)
docker run -d \
  --name pgbouncer-exporter \
  -e DATA_SOURCE_NAME="postgresql://pgbouncer_stats:contraseña@localhost:5432/pgbouncer" \
  -p 9127:9127 \
  prometheuscommunity/pgbouncer-exporter

# Verificar las métricas
curl -s http://localhost:9127/metrics | grep pgbouncer_pool

# Métricas importantes:
# pgbouncer_pool_client_active_connections  - Clientes con transacción activa
# pgbouncer_pool_client_waiting_connections - Clientes esperando conexión
# pgbouncer_pool_server_active_connections  - Conexiones al servidor en uso
# pgbouncer_pool_server_idle_connections    - Conexiones al servidor libres
# pgbouncer_stats_total_requests            - Total de peticiones procesadas
# pgbouncer_stats_avg_query_time            - Tiempo medio de consulta

Alta Disponibilidad

# Configurar múltiples instancias de pgBouncer con HAProxy para HA
cat > /etc/haproxy/haproxy.cfg << 'EOF'
frontend postgresql_frontend
  bind *:5432
  mode tcp
  default_backend postgresql_backend

backend postgresql_backend
  mode tcp
  balance leastconn
  
  # Chequeo de salud de pgBouncer
  option tcp-check
  
  server pgbouncer-1 pgbouncer-1.miempresa.local:5432 check inter 3s
  server pgbouncer-2 pgbouncer-2.miempresa.local:5432 check inter 3s backup
EOF

# Configurar keepalived para la IP virtual flotante
cat > /etc/keepalived/keepalived.conf << 'EOF'
vrrp_instance pgbouncer_vip {
  state MASTER
  interface eth0
  virtual_router_id 51
  priority 100
  advert_int 1
  
  authentication {
    auth_type PASS
    auth_pass secreto123
  }
  
  virtual_ipaddress {
    192.168.1.200/24
  }
  
  track_script {
    check_pgbouncer
  }
}

vrrp_script check_pgbouncer {
  script "pg_isready -h localhost -p 5432 -U pgbouncer_admin -d pgbouncer"
  interval 2
}
EOF

Solución de Problemas

Los clientes esperan demasiado tiempo para obtener conexión:

# Ver cuántos clientes están esperando
psql -h localhost -p 5432 -U pgbouncer_admin pgbouncer \
  -c "SHOW POOLS;" | grep -v "|  0 |  0 |  0 |"

# Si cl_waiting > 0, aumentar el tamaño del pool
# En pgbouncer.ini: aumentar default_pool_size o max_server_conn

# Ver el tiempo de espera de los clientes
psql -h localhost -p 5432 -U pgbouncer_admin pgbouncer -c "SHOW CLIENTS;"

Error: prepared statement already exists:

# Estás en transaction mode y la aplicación usa prepared statements del servidor
# Opciones:
# 1. Cambiar a session mode (menos eficiente pero compatible)
# 2. Configurar max_prepared_statements en pgBouncer 1.21+
# 3. Deshabilitar prepared statements en la aplicación

# Verificar el modo actual
psql -h localhost -p 5432 -U pgbouncer_admin pgbouncer \
  -c "SHOW CONFIG;" | grep pool_mode

pgBouncer no se conecta al servidor PostgreSQL:

# Verificar que PostgreSQL acepta conexiones desde pgBouncer
# En pg_hba.conf del servidor PostgreSQL:
# host  all  all  <ip-pgbouncer>/32  md5

# Probar la conexión directamente
psql -h postgres.miempresa.local -p 5432 -U miapp_user miapp

# Ver los logs de pgBouncer
tail -f /var/log/pgbouncer/pgbouncer.log

# Activar logs verbose temporalmente
psql -h localhost -p 5432 -U pgbouncer_admin pgbouncer \
  -c "SET log_connections=1; SET log_disconnections=1;"

Conclusión

pgBouncer es una pieza fundamental en cualquier arquitectura PostgreSQL de producción con alta concurrencia. La diferencia entre session mode y transaction mode es crítica: el modo transaction puede reducir las conexiones activas al servidor PostgreSQL en un factor de 10x o más, pero requiere que la aplicación sea compatible. Con TLS, alta disponibilidad mediante HAProxy y monitoreo con Prometheus, pgBouncer proporciona una capa de proxy robusta y observable que protege PostgreSQL del coste de la gestión de conexiones directas.