Como usar connection pooling para otimizar acesso ao banco

1. Fundamentos do Connection Pooling

Connection pooling é uma técnica de cache de conexões de banco de dados que elimina o custo de abrir e fechar conexões repetidamente. Em aplicações modernas, cada nova conexão ao banco envolve handshake TCP, autenticação e alocação de recursos no servidor — operações que podem consumir de 10ms a 100ms. Com pooling, essas conexões são mantidas abertas e reutilizadas, reduzindo drasticamente a latência.

A diferença prática é imediata: sem pool, uma aplicação que precisa de 100 consultas simultâneas criaria 100 conexões do zero, sobrecarregando o banco. Com pool, apenas 10 a 20 conexões são mantidas ativas e reutilizadas, aumentando o throughput e a concorrência sem degradar o servidor.

Métricas-chave para entender o impacto:
- Latência de conexão: tempo para estabelecer nova conexão vs. adquirir do pool (tipicamente 1-2ms no pool)
- Throughput: número de transações por segundo, que pode aumentar 5x com pool bem configurado
- Concorrência: número de threads simultâneas que podem acessar o banco sem estouro de conexões

2. Arquitetura e Funcionamento Interno de um Pool

O ciclo de vida de uma conexão em um pool segue quatro estados:

  1. Criação: conexões são abertas na inicialização do pool até o tamanho mínimo configurado
  2. Empréstimo: uma thread solicita uma conexão; o pool retorna uma disponível ou aguarda até o timeout
  3. Devolução: após o uso, a conexão é retornada ao pool, não fechada
  4. Destruição: conexões ociosas além do tempo máximo de idle são fechadas para liberar recursos

Parâmetros fundamentais de configuração:
- minimumIdle: número mínimo de conexões ociosas mantidas
- maximumPoolSize: limite máximo de conexões no pool
- idleTimeout: tempo máximo que uma conexão pode ficar ociosa antes de ser fechada
- connectionTimeout: tempo máximo que uma thread aguarda por uma conexão

Estratégias de balanceamento comuns:
- FIFO: conexões mais antigas são reutilizadas primeiro (padrão HikariCP)
- LIFO: conexões mais recentes são preferidas (útil para pools com validação frequente)
- Pools particionados: múltiplos pools para diferentes tipos de query (OLTP vs. OLAP)

3. Principais Implementações de Connection Pooling

HikariCP (Java)

HikariCP é o pool mais rápido para Java, com overhead inferior a 1 microssegundo por aquisição. Sua configuração otimizada por padrão já inclui:
- Tamanho máximo automaticamente ajustado (fórmula: (cores * 2) + effective_spindle_count)
- Validação assíncrona de conexões
- Sem bloqueios internos (lock-free implementation)

PgBouncer (PostgreSQL)

PgBouncer é um proxy leve que gerencia conexões para múltiplos serviços. Opera em três modos:
- Session: uma conexão de banco por sessão de aplicação
- Transaction: conexão adquirida por transação (mais eficiente)
- Statement: conexão por statement (uso exclusivo para consultas curtas)

Pool Genérico (Python/Node.js)

Implementação simples em Python:

class ConnectionPool:
    def __init__(self, min_size=2, max_size=10):
        self._pool = [criar_conexao() for _ in range(min_size)]
        self._max_size = max_size
        self._lock = threading.Lock()

    def acquire(self):
        with self._lock:
            if self._pool:
                return self._pool.pop()
            if len(self._pool) < self._max_size:
                conn = criar_conexao()
                return conn
            raise TimeoutError("Pool esgotado")

    def release(self, conn):
        with self._lock:
            if conn.is_valid():
                self._pool.append(conn)
            else:
                conn.close()

4. Configuração Otimizada do Pool para Diferentes Cargas

O ajuste do tamanho do pool depende do tipo de carga e hardware disponível:

Para cargas OLTP (transações curtas e frequentes):
- Tamanho do pool: (número de CPUs * 2) + 1
- maxLifetime: 30 minutos (evita conexões zumbis)
- connectionTimeout: 5 segundos (evita espera infinita)

Para cargas OLAP (consultas longas e complexas):
- Tamanho do pool: número de CPUs + 1 (cada consulta consome mais recurso)
- maxLifetime: 60 minutos (consultas longas precisam de conexões estáveis)
- connectionTimeout: 30 segundos (consultas demoram mais para iniciar)

Estratégias específicas:
- Evitar conexões zumbis: configurar maxLifetime menor que o timeout de firewall/banco
- Validar conexões: usar connectionTestQuery (SELECT 1) periodicamente
- Pool particionado: separar pools para leitura e escrita, cada um com tamanho otimizado

5. Monitoramento e Diagnóstico de Problemas no Pool

Métricas essenciais para monitoramento:
- Taxa de aquisição: conexões adquiridas por segundo (ideal: alta e estável)
- Tempo de espera: tempo médio para adquirir conexão (deve ser < 5ms)
- Conexões ativas vs. ociosas: ativas devem ser ≤ 70% do pool em pico

Ferramentas de monitoramento:
- JMX: expõe métricas do HikariCP via MBeans
- Prometheus + Grafana: dashboards com métricas de pool, latência e erros
- Logs de pooling: habilitar DEBUG no logger do pool para diagnosticar vazamentos

Sintomas de pool mal configurado:
- Lentidão progressiva: pool muito pequeno para a carga
- Estouro de conexões: maxPoolSize muito alto para o banco
- Deadlocks: threads competindo por conexões e recursos simultaneamente

6. Boas Práticas e Armadilhas Comuns

Sempre fechar conexões

Use try-with-resources (Java) ou finally (Python) para garantir devolução ao pool:

// Java - try-with-resources
try (Connection conn = dataSource.getConnection()) {
    // executar query
} catch (SQLException e) {
    // tratar erro
}
// Conexão automaticamente devolvida ao pool

Nunca misturar pool com transações longas

Transações que duram minutos bloqueiam conexões no pool, causando escassez. Separe transações curtas (OLTP) de longas (OLAP) em pools diferentes.

Lidar com failover e conexões corrompidas

  • Configurar validationTimeout para testar conexões antes do uso
  • Implementar retry com backoff exponencial em caso de falha
  • Usar healthCheckRegistry (HikariCP) para reabertura automática

7. Exemplos Práticos de Configuração

Configuração de HikariCP em Spring Boot

# application.yml
spring:
  datasource:
    hikari:
      pool-name: MainPool
      minimum-idle: 5
      maximum-pool-size: 20
      connection-timeout: 5000
      idle-timeout: 300000
      max-lifetime: 1800000
      connection-test-query: SELECT 1
      validation-timeout: 3000
      leak-detection-threshold: 60000

Uso de PgBouncer como pool externo

# pgbouncer.ini
[databases]
mydb = host=localhost port=5432 dbname=mydb

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = transaction
default_pool_size = 25
max_client_conn = 100
max_db_connections = 50
server_idle_timeout = 600
query_timeout = 30

Script de validação e reabertura automática

import psycopg2
from hikaricp import HikariCP

def validar_conexao(conn):
    try:
        cursor = conn.cursor()
        cursor.execute("SELECT 1")
        return True
    except Exception:
        return False

pool = HikariCP(
    jdbc_url="jdbc:postgresql://localhost:5432/mydb",
    maximum_pool_size=20,
    connection_test_query="SELECT 1",
    validation_timeout=3000
)

# Tenta reabrir conexões corrompidas
for conn in pool.get_idle_connections():
    if not validar_conexao(conn):
        conn.close()
        pool.add_connection()

Referências