Connection pooling: evitando esgotamento de recursos no DB
1. O Problema Fundamental: Conexões como Recurso Finito
1.1. Limites de conexões simultâneas no banco de dados
Todo banco de dados possui um limite máximo de conexões simultâneas, definido pelo parâmetro max_connections. No PostgreSQL, o valor padrão é 100; no MySQL, 151. Esse limite existe porque cada conexão consome recursos do servidor: memória para buffers, estruturas de estado de sessão e descritores de arquivo.
Quando uma aplicação ultrapassa esse limite, novas conexões são rejeitadas com erros como "too many connections" ou "FATAL: sorry, too many clients already". Em sistemas críticos, isso pode causar indisponibilidade total do serviço.
1.2. Custo de overhead por conexão
Estabelecer uma conexão com o banco envolve:
- Handshake TCP (3 vias)
- Autenticação (troca de credenciais)
- Negociação de parâmetros de sessão
- Alocação de memória no servidor (tipicamente 2-10 MB por conexão ociosa)
Em benchmarks reais, abrir uma conexão PostgreSQL leva de 10 a 50 milissegundos. Em um sistema com 1000 requisições por segundo, criar uma conexão para cada requisição resultaria em 10 a 50 segundos apenas em overhead de conexão.
1.3. Cenário clássico de esgotamento
Considere uma aplicação web que cria uma nova conexão para cada requisição HTTP:
// Exemplo problemático: nova conexão por requisição
function getUserData(userId) {
Connection conn = DriverManager.getConnection(url, user, password);
try {
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE id = " + userId);
return processResult(rs);
} finally {
conn.close();
}
}
Em um pico de 200 requisições simultâneas, se o banco tem max_connections=100, 100 requisições falharão imediatamente. As conexões bem-sucedidas competirão por recursos, degradando o desempenho geral.
2. O que é Connection Pooling e como funciona
2.1. Definição
Connection pooling é uma técnica onde um conjunto de conexões com o banco é mantido aberto e reutilizado. Em vez de criar e destruir conexões a cada operação, a aplicação "empresta" uma conexão do pool e a devolve após o uso.
2.2. Ciclo de vida
O fluxo típico é:
1. Inicialização: pool cria N conexões (tamanho mínimo)
2. Checkout: aplicação solicita uma conexão
- Se há conexão ociosa: retorna imediatamente
- Se todas estão ocupadas e pool < máximo: cria nova conexão
- Se pool no máximo: aguarda até timeout
3. Uso: aplicação executa queries
4. Check-in: aplicação fecha a conexão (devolve ao pool)
5. Pool mantém conexão aberta para reuso futuro
2.3. Pool estático vs. dinâmico
Pool estático: tamanho fixo definido na configuração. Simples e previsível, mas pode subutilizar recursos em horários de baixa carga ou causar contenção em picos.
Pool dinâmico: varia entre mínimo e máximo conforme demanda. Mais flexível, mas requer configuração cuidadosa para evitar criação excessiva de conexões.
3. Parâmetros Críticos de Configuração do Pool
3.1. Tamanho mínimo vs. máximo
Uma regra prática para dimensionamento: pool_size = (num_cores * 2) + 1 para bancos de dados relacionais tradicionais. Exceder esse valor geralmente causa contenção no banco, não melhora throughput.
Exemplo de configuração HikariCP:
# Configuração HikariCP para aplicação com 4 cores
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
3.2. Timeouts
connectionTimeout: tempo máximo para aguardar uma conexão do pool (30s é padrão)idleTimeout: tempo que uma conexão ociosa permanece no pool (10 min)maxLifetime: tempo máximo de vida de uma conexão (30 min para evitar conexões obsoletas)
3.3. Estratégias de validação
# Validação de conexão no HikariCP
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.validation-timeout=5000
testOnBorrow valida a conexão antes de entregar ao usuário, garantindo que conexões "mortas" não sejam usadas. testWhileIdle valida conexões ociosas periodicamente.
4. Estratégias de Gerenciamento de Contenção
4.1. Filas de espera vs. fail-fast
Blocking queue: threads aguardam em fila quando todas as conexões estão ocupadas. Atraso previsível, mas pode causar acúmulo de requisições.
Fail-fast: rejeita imediatamente quando pool está vazio. Útil em sistemas onde latência é crítica e prefere-se erro rápido a espera longa.
4.2. Políticas de backpressure
Implementar backpressure no nível da aplicação:
// Exemplo de backpressure com semáforo
Semaphore connectionPermits = new Semaphore(maxPoolSize);
public Connection getConnection() throws InterruptedException {
if (!connectionPermits.tryAcquire(100, TimeUnit.MILLISECONDS)) {
throw new TooManyRequestsException("Pool esgotado");
}
try {
return pool.getConnection();
} catch (Exception e) {
connectionPermits.release();
throw e;
}
}
4.3. Monitoramento de métricas
Métricas essenciais para expor:
- activeConnections: conexões em uso
- idleConnections: conexões disponíveis
- pendingRequests: threads aguardando conexão
- timeoutCount: requisições que excederam connectionTimeout
5. Padrões de Implementação em Diferentes Cenários
5.1. Aplicações web síncronas
HikariCP é o pool padrão no Spring Boot por sua performance superior:
// Configuração programática HikariCP
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(10);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
HikariDataSource ds = new HikariDataSource(config);
5.2. Aplicações reativas/assíncronas
Para reativo, R2DBC oferece pool não bloqueante:
// Configuração R2DBC Pool
ConnectionPoolConfiguration poolConfig = ConnectionPoolConfiguration.builder()
.connectionFactory(connectionFactory)
.maxSize(20)
.maxIdleTime(Duration.ofMinutes(5))
.maxLifeTime(Duration.ofMinutes(30))
.validationQuery("SELECT 1")
.build();
ConnectionPool pool = new ConnectionPool(poolConfig);
5.3. Microserviços e bancos compartilhados
Cada microsserviço deve ter seu próprio pool isolado:
# Configuração por serviço
# Serviço A: alta taxa de leitura
serviceA.datasource.hikari.maximum-pool-size=15
# Serviço B: operações batch pesadas
serviceB.datasource.hikari.maximum-pool-size=5
serviceB.datasource.hikari.connection-timeout=60000
6. Armadilhas Comuns e Como Evitá-las
6.1. Vazamento de conexões
Conexões não devolvidas ao pool (connection leak) é a armadilha mais comum. Use wrappers com detecção:
// Detecção de vazamento com HikariCP
spring.datasource.hikari.leak-detection-threshold=60000
Isso loga um stack trace se uma conexão ficar emprestada por mais de 60 segundos.
6.2. Pool muito grande
Pool com 50 conexões para um banco PostgreSQL em hardware modesto pode causar "thundering herd" — todas as conexões competem por locks, causando degradação pior que pool pequeno.
6.3. Ressuscitação de conexões mortas
Firewalls e balanceadores de carga podem matar conexões ociosas. Configure maxLifetime menor que o timeout do firewall (tipicamente 30 min vs. 60 min do firewall).
7. Monitoramento e Ajuste Contínuo
7.1. Métricas essenciais
# Métricas Prometheus para HikariCP
hikaricp_connections_active{pool="main"} 8
hikaricp_connections_idle{pool="main"} 2
hikaricp_connections_pending{pool="main"} 3
hikaricp_connections_timeout_total{pool="main"} 5
7.2. Ferramentas de observabilidade
Integre métricas do pool com:
- Prometheus para coleta
- Grafana para dashboards
- APM (New Relic, Datadog) para correlação com performance da aplicação
7.3. Estratégias de tuning
Para cargas mistas (leitura + escrita):
- Pool separado para operações de escrita (menor, prioridade)
- Pool maior para leituras (podem ser distribuídas em réplicas)
Ajuste baseado em perfil:
- Se timeouts de aquisição > 10%: aumentar pool ou otimizar queries
- Se conexões ociosas sempre em 0: pool muito pequeno
- Se conexões ativas sempre < 50% do pool: pool superdimensionado
Referências
- HikariCP Documentation — Documentação oficial do pool JDBC mais performante para Java, com configurações detalhadas e boas práticas.
- PostgreSQL Connection Pooling with PgBouncer — Guia oficial do PgBouncer, pooler externo para PostgreSQL, com estratégias de pooling transaction-level e session-level.
- MySQL Connection Pooling Best Practices — Documentação oficial MySQL sobre configuração de pools JDBC, incluindo parâmetros de validação e timeouts.
- R2DBC Pooling Specification — Especificação oficial do pool reativo R2DBC para aplicações reativas não bloqueantes.
- HikariCP: Configuration Knobs and Their Effects — Artigo técnico detalhando como dimensionar pools corretamente e o impacto de cada parâmetro de configuração.