Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

01 - Conceito e Benefícios

#connection-pool#latencia#pgbouncer#postgresql#throughput
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Conceito e Benefícios do Pool de Conexões

O pool de conexões é o padrão que mantém um conjunto de conexões de banco de dados abertas e reutilizadas por muitas requisições, em vez de abrir e fechar uma conexão nova para cada unidade de trabalho. Ele existe porque estabelecer uma conexão é caro: handshake de rede, autenticação, autorização e alocação de memória do lado do servidor, tudo isso antes da primeira consulta útil. Em produção, o pool é a diferença entre um serviço que escala e um que colapsa sob pico, pois o banco de dados tem um limite duro de conexões simultâneas que nenhuma aplicação deveria consumir sozinha. Todo serviço que conversa com PostgreSQL, MySQL ou SQL Server em produção precisa de uma estratégia de pooling explícita, configurada, monitorada e dimensionada. Este arquivo cobre o conceito, o custo real da conexão, o funcionamento interno do pool, a pilha de pools que sistemas maiores adotam e os benefícios mensuráveis em latência e throughput.

Conceito

Um pool é um cache de conexões vivas com gestão de ciclo de vida própria. Na inicialização, o serviço abre um número mínimo de conexões e mantém o conjunto entre min_idle e maximum_pool_size; cada requisição pede uma conexão ao pool, usa e devolve, sem nunca fechá-la de fato. O pool valida a conexão antes de entregar (evitando entregar uma conexão morta), aplica timeout de espera quando o pool está esgotado e recicla conexões antigas por tempo de vida ou por idle. O modelo mental correto é o de semáforo: a conexão é um recurso escasso emprestado por tempo curtíssimo, não uma propriedade da requisição. Quando o pool opera bem, a aplicação nem percebe que ele existe; quando opera mal, o sintoma é p99 degradando em fileiras de threads esperando por conexão. O dimensionamento e os knobs de timeout são tema do arquivo seguinte; aqui o foco é o porquê e o como do mecanismo.

Que problema resolve

O custo de estabelecer uma conexão nova é frequentemente subestimado porque é invisível em testes locais. No PostgreSQL, cada conexão é um processo do lado do servidor, com memória alocada, catálogo carregado e autenticação completa, tipicamente dezenas de milissegundos e megabytes de RAM por conexão. Multiplicado por centenas de requisições por segundo, o custo de abrir conexão por requisição domina a latência e consome a capacidade do banco com overhead, não com trabalho. O pool resolve o problema em quatro frentes:

  • Latência: elimina o handshake do caminho crítico da requisição, pois a conexão já está pronta e autenticada no pool.
  • Limite de conexões do banco: o PostgreSQL suporta dezenas a poucas centenas de conexões saudáveis por nó; o pool faz milhares de requisições conviverem com poucas dezenas de conexões físicas.
  • Memória do servidor: menos processos no banco significa menos memória, menos troca de contexto e planos de consulta servidos com mais cache.
  • Estabilidade sob pico: o pool vira válvula de contenção, enfileirando excesso de demanda na aplicação, onde é barato, em vez de derrubar o banco, onde é caro.

Sem pool, o padrão de falha é conhecido: o pico de tráfego abre centenas de conexões, o banco degrada por memória e context switch, os tempos de resposta sobem, os clientes reabrem conexões por timeout, e o sistema entra em avalanche de conexões.

Funcionamento interno

O ciclo de vida de uma conexão no pool passa por estados bem definidos, e o comportamento do serviço depende de quão rápido as conexões circulam. Quando a requisição chega, o pool tenta entregar uma conexão idle imediatamente; havendo connection timeout configurado, espera até esse limite antes de falhar. A conexão entregue executa o trabalho da unidade de operação (requisição, transação ou statement, conforme o modo) e retorna ao pool na devolução; nesse retorno, o pool executa reset do estado da sessão (rollback pendente, variáveis de sessão, prepared statements) para a próxima consumidora não herdar sujeira. Em paralelo, tarefas de manutenção expulsam conexões acima do mínimo que ficaram ociosas além do idle timeout, substituem conexões que passaram do max lifetime e preenchem o pool até o mínimo após perdas. A validação antes do uso varia de um SELECT 1 barato a mecanismos do próprio protocolo, e o custo da validação pesa em latência, motivo pelo qual timeouts e validações se configuram juntos.

Ciclo de vida de uma conexão em pool
  criada ----> idle ----> emprestada ----> devolvida/resetada ----> idle
                 |              |
                 |              +-- falha na validacao: reciclada
                 +-- idle timeout atingido: fechada (se acima do minimo)
# Exemplo conceitual de configuração de pool (HikariCP)
maximum_pool_size: 20       # teto de conexões físicas deste serviço
minimum_idle: 5             # pool encolhe até aqui em vagas frias
connection_timeout: 3000    # espera máxima por conexão livre (ms)
idle_timeout: 300000        # devolve conexões ociosas após 5 min
max_lifetime: 1800000       # recicla conexões a cada 30 min
validation_timeout: 1000    # tempo para validar antes de entregar

Anatomia do custo de conexão

Vale decompor o que acontece dentro de um connection.open(), porque cada etapa explica um benefício do pool. A resolução DNS do host do banco custa de alguns milissegundos até centenas em falha de cache; o handshake TCP adiciona pelo menos um round trip de rede, frequentemente dois ou três com TLS, e cada round trip em rede intra-região custa tipicamente entre 0,5 e 2 milissegundos. A autenticação depende do método: trust é instantânea, senha exigirá trocas de mensagens, e certificados adicionam validação criptográfica. Do lado do PostgreSQL, o fork de processo carrega catálogos, aloca work_mem por operação e registra o backend nos catálogos de sistema; do lado do MySQL, é um thread, mais leve, mas ainda custoso. Somando tudo, abrir conexão nova para o PostgreSQL costuma custar de 20 a 100 milissegundos ida e volta, e gigabytes de memória somados em centenas de conexões. O pool paga esse custo uma vez por conexão, distribui o pagamento pelo tempo de vida e entrega a conexão em microssegundos para cada requisição seguinte.

Etapa da abertura Custo típico Pago pelo pool?
Resolução DNS 1 a 50 ms Uma vez por reciclagem
Handshake TCP + TLS 1 a 5 round trips Uma vez por reciclagem
Autenticação 1 a 10 ms Uma vez por reciclagem
Alocação no servidor 20 a 80 ms (PostgreSQL) Uma vez por reciclagem
Obtenção do pool Microssegundos A cada requisição

Limites do banco e observação

O limite de conexões é um recurso finito e mensurável do banco, e a operação precisa enxergá-lo em tempo real. No PostgreSQL, max_connections define o teto, e cada conexão reserva memória que soma rápido; as visões pg_stat_activity e pg_stat_database mostram conexões ativas, idle e o histórico de conexões recusadas. Em MySQL, Threads_connected e max_connections contam a mesma história. A consulta a seguir é o básico do runbook:

-- PostgreSQL: quem está segurando as conexões agora
SELECT state, count(*) AS conexoes,
       max(now() - xact_start) AS transacao_mais_longa
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state
ORDER BY conexoes DESC;

-- Teto e consumo atual
SHOW max_connections;
SELECT numbackends, xact_commit FROM pg_stat_database
 WHERE datname = current_database();

A pilha de pools

Sistemas de produção reais costumam ter mais de uma camada de pooling, e entender quem faz o quê evita configurações que se anulam. A primeira camada é o pool na aplicação (HikariCP em Java, pgxpool em Go, pool embutido nos drivers Node e Python), que atende o processo do serviço. A segunda camada aparece quando muitas instâncias, funções serverless ou linguagens sem bom pooling local convergem no banco: um pooler externo como o PgBouncer fica na frente do PostgreSQL, multiplexando milhares de conexões de clientes em dezenas de conexões de servidor. A terceira camada, em nuvens gerenciadas, são proxies oferecidos pelo provedor (RDS Proxy no AWS, Azure Database proxies), que agregam pooling, failover transparente e autenticação IAM. A pilha completa é comum e saudável, desde que o dimensionamento considere a multiplicação: vinte instâncias com pool de 20 chegam a 400 conexões no pooler, e o pooler precisa limitar conexões de servidor bem abaixo do max_connections do banco.

Camada Exemplo Quem atende Papel principal
Pool na aplicação HikariCP, pgxpool, SQLAlchemy pool Processos do serviço Reuso local e baixa latência
Pooler externo PgBouncer, Odyssey Muitos clientes e instâncias Multiplexação e proteção do banco
Proxy gerenciado RDS Proxy, Azure Proxy Serviços cloud Pooling com failover e IAM
Sem pool Conexão por requisição Ninguém Antipadrão em produção

Benefícios mensuráveis

Os ganhos do pool aparecem em números diretos e devem ser medidos, não presumidos:

  • Latência média: cai pela eliminação do handshake, com ganho típico de milissegundos por chamada e efeito maior em funções de vida curta.
  • Throughput: sobe porque o banco gasta ciclos executando consultas, e não autenticando conexões novas.
  • P99: melhora de forma não linear, pois no regime sem pool o p99 sofre a avalanche de conexões do pico de tráfego, e o pool remove exatamente esse modo de falha.
  • Resiliência: a fila do pool é o ponto único para aplicar backpressure, e métricas de espera por conexão viram sinal claro de saturação.
  • Custo de infraestrutura: um banco que sustentava a carga com centenas de conexões passa a operar com dezenas bem aproveitadas, e o mesmo nó atende mais tráfego.
  • Diagnóstico mais simples: com número fixo de conexões, correlacionar espera por conexão com latência do banco deixa de envolver variável desconhecida.

A regra é medir antes e depois: capturar p50, p95 e p99 das rotas mais quentes, além de conexões simultâneas no banco, e comparar os dois mundos em teste de carga controlada. O resultado costuma converter céticos mais rápido do que qualquer argumento teórico.

Erros comuns

  • Conexão aberta por requisição "por simplicidade", colocando handshake, autenticação e memória no caminho crítico de todo request.
  • Pool não configurado usando default do driver, que muitas vezes não faz pool real ou cresce sem teto, estourando o banco no primeiro pico.
  • Um pool por função serverless sem limite global, onde cada invocação fria abre conexões próprias e o banco vê milhares de conexões.
  • Falta de reset na devolução da conexão, com transações abertas ou variáveis de sessão vazando para a próxima requisição.
  • Timeout de espera por conexão sem tratamento de erro, gerando exceções genéricas em cascata em vez de backpressure e retry orquestrado.
  • Pool infinito na prática, com maximum_pool_size igual ao dobro do necessário, causando contenção no banco em vez de resolvê-la.
  • Conexões mantidas durante chamadas lentas a serviços externos, segurando o recurso por segundos em requisições que não precisavam do banco nesse intervalo.
  • Pools de múltiplas aplicações somados sem contabilidade, descobrindo o limite do banco só quando a primeira conexão é recusada em produção.
  • Deploy canário com pool mais agressivo que o estável, saturando o banco apenas na rota do canário e dificultando o diagnóstico.

Boas práticas

  • Torne o pool obrigatório por política: nenhum serviço de produção conversa com banco sem pool configurado explicitamente e revisado em code review.
  • Dimensione pensando em multiplexação: o pool da aplicação deve ser bem menor que o instinto inicial, e o tema do próximo arquivo detalha as fórmulas.
  • Exponha métricas do pool (espera por conexão, ativas, idle, timeouts) no observabilidade padrão do serviço, com alerta de saturação.
  • Defina um dono para limites agregados: quando várias instâncias somam conexões, alguém precisa conhecer o número que chega no banco.
  • Trate falha de aquisição como sinal de saturação e reaja com retry curto, degradação ou resposta 429, nunca com aumentar o pool de primeira.
  • Valide a pilha completa em teste de carga, incluindo pooler externo e proxy, pois o gargalo pode estar em qualquer camada.
  • Documente o contrato de conexões: quantas cada serviço pode abrir, em qual pooler, com quais timeouts, em ADR e no runbook de operação.
  • Carregue o pool em saúde no startup (mínimo de conexões prontas) para evitar rajada de autenticação no primeiro pico pós-deploy.
  • Revise o dimensionamento a cada mudança relevante de carga ou de arquitetura, pois o tamanho certo do pool acompanha o desenho do serviço, não é valor fixo eterno.
  • Reaproveite o mesmo padrão de configuração entre serviços da plataforma, com variações por variável de ambiente e revisão centralizada do template.
  • Acompanhe a taxa de uso do pool em janelas de pico: pool sempre no teto indica falta de capacidade, pool quase vazio indica overprovisioning.
  • Separe pools por natureza de carga quando o volume justificar: leitura de catálogo, escrita transacional e jobs batch com limites próprios.
  • Inclua o custo de conexão no teste de frio: meça o tempo do primeiro request pós-deploy e garanta que o pool aquece antes de receber tráfego real.
  • Audite sessões órfãs semanalmente com a consulta de pg_stat_activity, caçando conexões idle longas que indicam vazamento entre camadas.