SQL Performance HikariCP
Acesso a Dados

Connection Pooling

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.

Porquê as Conexões São Caras

-- 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

Como um Pool Funciona

-- 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.

HikariCP — Configuração

-- ── 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.

Dimensionamento do Pool

-- 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

Múltiplos DataSources (Read Replica)

// ── 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() { ... }
}

Diagnosticar Pool Exhaustion

-- 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

PgBouncer — Pool Externo

-- 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)

Checklist Connection Pooling