Estabelecer uma conexão TCP a uma base de dados é uma operação cara — handshake TCP, autenticação, negociação de parâmetros de sessão. No PostgreSQL, cada conexão cria um processo dedicado no servidor. Numa aplicação web com centenas de requests por segundo, abrir e fechar uma conexão por request tornaria a base de dados o bottleneck principal. Um connection pool resolve isto mantendo um conjunto de conexões abertas e reutilizando-as — um request pede uma conexão ao pool, usa-a, e devolve-a. O pool nunca a fecha. HikariCP é o pool de referência no ecossistema Java — é o default do Spring Boot desde a versão 2.0.
-- O que acontece internamente ao chamar DriverManager.getConnection():
--
-- 1. TCP SYN → SYN-ACK → ACK (~0.5ms em rede local, ~50ms wan)
-- 2. TLS handshake (se sslmode=require) (~2-5ms adicional)
-- 3. PostgreSQL startup message (cliente envia parâmetros: user, database, options)
-- 4. Autenticação (MD5 ou SCRAM-SHA-256 challenge-response)
-- 5. PostgreSQL fork() do backend process (o servidor cria um processo Unix dedicado)
-- 6. Negociação de parâmetros de sessão (TimeZone, client_encoding, search_path...)
-- 7. Connection pronta para uso
--
-- Custo total típico: 5-50ms por conexão nova
-- Para comparação: uma query simples por índice: 0.1-1ms
--
-- Sem pool:
-- Request 1: abrir conexão (20ms) + query (1ms) + fechar conexão = 21ms
-- Request 2: abrir conexão (20ms) + query (1ms) + fechar conexão = 21ms
-- 100 requests/s → 2000ms gastos só a abrir/fechar conexões
--
-- Com pool (10 conexões abertas permanentemente):
-- Request 1: obter do pool (0.01ms) + query (1ms) + devolver ao pool = ~1ms
-- Request 2: obter do pool (0.01ms) + query (1ms) + devolver ao pool = ~1ms
-- 100 requests/s → <1ms de overhead de conexão
-- Limite de conexões no PostgreSQL:
-- Cada conexão = 1 processo PostgreSQL = ~5-10MB de RAM
-- postgresql.conf: max_connections = 100 (default)
-- 100 conexões × 8MB = ~800MB só para processos de backend
-- Pool evita que cada thread da aplicação abra a sua própria conexão
-- Estado do pool em qualquer momento:
--
-- ┌─────────────────────────────────────────────────────┐
-- │ HikariCP Pool │
-- │ │
-- │ Conexões IDLE (disponíveis): [C1] [C2] [C3] │
-- │ Conexões ACTIVE (em uso): [C4] [C5] │
-- │ Conexões PENDING (a criar): [ ] │
-- │ │
-- │ minimumIdle = 3 maximumPoolSize = 10 │
-- └─────────────────────────────────────────────────────┘
--
-- Ciclo de vida de uma conexão no pool:
--
-- 1. Request chega → thread pede conexão ao pool
-- 2. Pool tem conexão idle? → entrega imediatamente (C1)
-- 3. Pool vazio + abaixo de maximumPoolSize? → cria nova conexão
-- 4. Pool vazio + no limite? → thread aguarda connectionTimeout ms
-- 5. Timeout excedido → SQLTimeoutException ("Connection is not available")
-- 6. Thread usa a conexão (query, transacção)
-- 7. conn.close() → NÃO fecha a conexão — devolve ao pool como idle
-- (HikariCP faz proxy de Connection.close() para devolver ao pool)
--
-- Validação de conexões idle:
-- O pool valida periodicamente as conexões idle com uma keepalive query.
-- Se o SGBD reiniciou ou a conexão foi fechada por timeout, o pool detecta
-- e cria uma nova — a aplicação nunca recebe uma conexão morta.
-- ── Spring Boot application.properties ──────────────────────────────────────
spring.datasource.url=jdbc:postgresql://db.example.com:5432/loja?sslmode=require
spring.datasource.username=app_user
spring.datasource.password=${DB_PASSWORD} -- nunca hardcodar passwords no código
spring.datasource.driver-class-name=org.postgresql.Driver
-- Pool (HikariCP é o default do Spring Boot):
spring.datasource.hikari.maximum-pool-size=10
-- Número máximo de conexões no pool.
-- NÃO aumentar sem pensar — ver secção "Dimensionamento" abaixo.
spring.datasource.hikari.minimum-idle=5
-- Número mínimo de conexões idle mantidas.
-- HikariCP recomenda igualar a maximum-pool-size para pool de tamanho fixo
-- (evita overhead de criar/destruir conexões dinamicamente)
spring.datasource.hikari.connection-timeout=30000
-- Tempo máximo (ms) que uma thread aguarda por uma conexão do pool.
-- Após este tempo: SQLTimeoutException.
-- 30 segundos é o default — considerar reduzir para 5-10s em produção
-- para falhar rápido em vez de acumular threads bloqueadas.
spring.datasource.hikari.idle-timeout=600000
-- Tempo (ms) após o qual uma conexão idle acima de minimumIdle é fechada.
-- Default: 10 minutos.
spring.datasource.hikari.max-lifetime=1800000
-- Tempo de vida máximo (ms) de uma conexão, mesmo que esteja a ser usada.
-- Default: 30 minutos. Deve ser inferior ao wait_timeout do SGBD e ao
-- timeout do load balancer/firewall (que fecham conexões TCP inactivas).
-- Regra prática: max-lifetime = timeout_do_firewall - 30s
spring.datasource.hikari.keepalive-time=60000
-- Intervalo (ms) para enviar uma keepalive query às conexões idle.
-- Previne que firewalls fechem conexões TCP "inactivas".
-- A keepalive query default é: SELECT 1
spring.datasource.hikari.connection-test-query=SELECT 1
-- Query usada para validar que a conexão está viva.
-- Apenas necessário para drivers antigos que não suportam JDBC4 isValid().
-- Para PostgreSQL moderno: desnecessário (o driver suporta isValid()).
spring.datasource.hikari.pool-name=HikariPool-Loja
-- Nome do pool — aparece nos logs e métricas (JMX, Micrometer).
-- Útil quando a aplicação tem múltiplos pools (ex: read replica + write primary).
spring.datasource.hikari.schema=public
-- Define o search_path da sessão PostgreSQL para todas as conexões do pool.
spring.datasource.hikari.data-source-properties.reWriteBatchedInserts=true
-- Propriedade específica do driver PostgreSQL:
-- Agrupa múltiplos INSERT num único pacote de rede → ~2-3x mais rápido em batch inserts.
-- A intuição errada: "mais conexões = mais performance"
-- A realidade: conexões a mais causam context switching e contention no SGBD.
--
-- Fórmula de referência (Hikari / PostgreSQL):
--
-- pool_size = Ncpu × 2 + N_discos_efectivos
--
-- Para um servidor com 4 CPUs e SSD (1 "disco efectivo"):
-- pool_size = 4 × 2 + 1 = 9 → arredondar para 10
--
-- Intuição: o SGBD só consegue executar Ncpu queries em paralelo real.
-- O factor ×2 cobre o tempo de espera de I/O. Mais conexões acima disto
-- apenas competem entre si pelo CPU — aumentam latência sem aumentar throughput.
--
-- Benchmark empírico HikariCP (PostgreSQL, 4 CPUs):
--
-- Pool size │ Transacções/s │ Latência média
-- ──────────┼───────────────┼───────────────
-- 5 │ 3.500 │ 1.4ms
-- 10 │ 6.200 │ 1.6ms ← ponto óptimo típico
-- 20 │ 6.100 │ 3.2ms ← sem ganho, latência aumenta
-- 50 │ 5.800 │ 8.6ms ← pior que 10!
-- 100 │ 4.200 │ 23ms ← muito pior
--
-- Regra prática para aplicações web:
-- - Começar com maximum-pool-size = 10
-- - Monitorizar pool utilization e connection wait time
-- - Aumentar apenas se houver pool exhaustion confirmada
-- - Verificar sempre o max_connections do PostgreSQL:
-- pool_total_da_app ≤ max_connections_postgres - 5 (reservar para admins)
-- Múltiplas instâncias da aplicação:
-- Se correm 3 instâncias com pool-size=10 → 30 conexões ao PostgreSQL
-- max_connections=100 → seguro com até 9 instâncias
-- Em Kubernetes com autoscaling: usar PgBouncer como proxy de pool externo
// ── Configuração de dois pools: primary (escrita) + replica (leitura) ─────────
// application.properties:
// app.datasource.primary.url=jdbc:postgresql://primary.db:5432/loja
// app.datasource.primary.hikari.maximum-pool-size=10
// app.datasource.replica.url=jdbc:postgresql://replica.db:5432/loja
// app.datasource.replica.hikari.maximum-pool-size=20 ← mais conexões para leituras
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("app.datasource.primary.hikari")
public HikariDataSource primaryDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(env.getProperty("app.datasource.primary.url"));
config.setPoolName("HikariPool-Primary");
config.setMaximumPoolSize(10);
config.setMinimumIdle(10);
config.setMaxLifetime(1800000);
return new HikariDataSource(config);
}
@Bean
@ConfigurationProperties("app.datasource.replica.hikari")
public HikariDataSource replicaDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(env.getProperty("app.datasource.replica.url"));
config.setPoolName("HikariPool-Replica");
config.setMaximumPoolSize(20);
config.setReadOnly(true); // sinaliza ao driver que é read-only
return new HikariDataSource(config);
}
}
// Usar o datasource correcto via @Qualifier:
@Service
public class ProdutoService {
@Autowired @Primary
private DataSource primaryDs;
@Autowired @Qualifier("replicaDataSource")
private DataSource replicaDs;
@Transactional // usa primary por default
public void criarProduto(Produto p) { ... }
@Transactional(readOnly = true) // Spring pode rotear para replica
public List listarProdutos() { ... }
}
-- Pool exhaustion: todas as conexões estão em uso, novas threads ficam à espera.
-- Sintoma na aplicação: requests lentos ou com timeout.
-- Log típico do HikariCP:
-- HikariPool-1 - Connection is not available, request timed out after 30002ms.
-- HikariPool-1 - Pool stats (total=10, active=10, idle=0, waiting=15)
-- ── Causas comuns e diagnóstico ───────────────────────────────────────────────
-- 1. Conexão não devolvida ao pool (connection leak):
-- A thread esqueceu de fechar a Connection (não usou try-with-resources).
-- A conexão fica ACTIVE para sempre até o max-lifetime a forçar fechar.
-- Activar leak detection no HikariCP:
spring.datasource.hikari.leak-detection-threshold=5000
-- Se uma conexão estiver ACTIVE mais de 5s, o HikariCP imprime stack trace:
-- "Connection leak detection triggered for ..."
-- O stack trace mostra exactamente onde a conexão foi obtida e não devolvida.
-- 2. Transacções demoradas a segurar conexões:
-- Uma transacção que demora 10s segura a conexão durante 10s.
-- Com pool-size=10 e 10 transacções longas em simultâneo → exhaustion.
-- Diagnóstico: ver as queries activas no PostgreSQL:
SELECT pid, now() - pg_stat_activity.query_start AS duracao, query, state
FROM pg_stat_activity
WHERE state != 'idle'
AND query_start < NOW() - INTERVAL '5 seconds'
ORDER BY duracao DESC;
-- 3. Pool demasiado pequeno para o tráfego actual:
-- Monitorizar via métricas Micrometer (Spring Boot Actuator):
-- hikaricp.connections.active ← conexões em uso
-- hikaricp.connections.idle ← conexões disponíveis
-- hikaricp.connections.pending ← threads à espera de conexão
-- hikaricp.connections.timeout.total ← contador de timeouts
-- Se pending > 0 com frequência: considerar aumentar o pool (com cuidado).
-- 4. N+1 queries — padrão que esgota o pool rapidamente:
-- Carregar 100 produtos → para cada produto, 1 query para a categoria
-- = 101 queries por request em vez de 1 JOIN
-- Cada query obtém e devolve uma conexão → 101 idas ao pool por request
-- Com 10 requests simultâneos → 1010 idas ao pool
-- Solução: JOIN ou batch loading (ver Hibernate N+1 / jOOQ fetchGroups)
-- ── Ver estado do pool via JMX / Actuator ─────────────────────────────────────
-- Com Spring Boot Actuator:
-- GET /actuator/metrics/hikaricp.connections
-- GET /actuator/metrics/hikaricp.connections.active
-- GET /actuator/metrics/hikaricp.connections.pending
-- Activar endpoint de health com detalhes do pool:
-- management.endpoint.health.show-details=always
-- GET /actuator/health → inclui db.status e pool stats
-- HikariCP é um pool na JVM — cada instância da aplicação tem o seu.
-- Em ambientes com muitas instâncias (Kubernetes, microserviços),
-- o total de conexões ao PostgreSQL pode ultrapassar max_connections.
--
-- PgBouncer é um proxy de connection pooling externo — fica entre a aplicação
-- e o PostgreSQL, multiplexando conexões de múltiplos clientes.
--
-- Topologia com PgBouncer:
--
-- App instância 1 (pool=10) ─┐
-- App instância 2 (pool=10) ─┼→ PgBouncer (pool=25) → PostgreSQL (max_conn=30)
-- App instância 3 (pool=10) ─┘
--
-- 30 conexões de aplicação → 25 conexões reais ao PostgreSQL
--
-- Modos do PgBouncer:
--
-- session pooling (default):
-- Uma conexão PgBouncer é mantida para toda a sessão do cliente.
-- Pouca vantagem sobre pool na aplicação.
--
-- transaction pooling (recomendado):
-- A conexão real ao PostgreSQL é atribuída apenas durante a transacção.
-- Entre transacções, a conexão volta ao pool do PgBouncer.
-- Permite multiplexar 1000 clientes em 25 conexões reais.
-- ⚠️ Incompatível com: prepared statements do lado do servidor,
-- SET SESSION, advisory locks, LISTEN/NOTIFY.
--
-- statement pooling:
-- Pool por statement individual. Raramente usado — incompatível com transacções.
-- pgbouncer.ini:
-- [databases]
-- loja = host=localhost port=5432 dbname=loja
--
-- [pgbouncer]
-- pool_mode = transaction
-- max_client_conn = 1000
-- default_pool_size = 25
-- min_pool_size = 10
-- server_reset_query = DISCARD ALL
-- -- server_reset_query: limpa estado de sessão entre clientes no transaction mode
-- Com PgBouncer + transaction pooling:
-- Desactivar prepared statements no lado do servidor (JDBC):
-- spring.datasource.url=jdbc:postgresql://pgbouncer:5432/loja?prepareThreshold=0
-- prepareThreshold=0: usa simple query protocol em vez de extended query protocol
-- (prepared statements do servidor são incompatíveis com transaction pooling)
maximum-pool-size: começar com Ncpu × 2 + 1. Mais conexões não significa mais performance — aumenta contention no SGBD.max-lifetime deve ser inferior ao timeout de firewalls/load balancers na rede — evita que conexões TCP sejam fechadas silenciosamente.keepalive-time para manter conexões idle vivas através de firewalls que fecham TCP inactivo.try-with-resources para garantir que conn.close() devolve ao pool — connection leaks esgotam o pool silenciosamente.leak-detection-threshold em desenvolvimento — imprime stack trace de onde a conexão foi obtida e não devolvida.hikaricp.connections.pending via Micrometer/Actuator — valores > 0 frequentes indicam pool subdimensionado ou transacções longas.transaction mode como proxy externo para controlar o total de conexões ao PostgreSQL.prepareThreshold=0) — são incompatíveis com este modo.