DevGuideTK
← Trilha Backend

Pool de conexões

Por que abrir uma conexão por pedido não escala e como dimensionar o pool.

O problema: conectar custa caro

Cada consulta ao banco precisa de uma conexão. Abrir uma conexão com o PostgreSQL envolve uma conexão TCP, às vezes TLS, a troca de credenciais e um processo novo no servidor só para ela. Medimos num PostgreSQL 18, na mesma rede Docker, com autenticação por senha (scram-sha-256), fazendo 300 vezes um SELECT 1:

FormaTempo por consulta
Conexão nova a cada consulta8,0 ms
A mesma conexão reaproveitada0,052 ms

Abrir e fechar a conexão custou 150 vezes mais que a consulta. Com TLS e o banco em outra máquina, a diferença cresce.

O pool de conexões resolve isso: a aplicação abre algumas conexões e as mantém abertas. Cada pedido pega uma conexão emprestada, faz suas consultas e a devolve. A próxima consulta não paga a abertura.

Mais conexões não é mais vazão

A reação natural a uma aplicação lenta é aumentar o pool. O banco, porém, tem um número fixo de núcleos e de discos. Além de certo ponto, mais conexões só disputam os mesmos recursos.

Medimos com o pgbench, a ferramenta de carga do PostgreSQL, com o banco preso a 2 núcleos de CPU, variando o número de conexões ativas ao mesmo tempo:

Conexões ativasSó leitura: transações/sSó leitura: latência médiaMisto (leitura e escrita): transações/sMisto: latência média
112.8360,08 ms1.0880,92 ms
224.2290,08 ms2.1180,94 ms
425.2800,16 ms2.5501,6 ms
823.0040,35 ms2.9542,7 ms
1622.5010,71 ms3.3374,8 ms
3221.0921,5 ms3.13610,2 ms
6419.4563,3 ms2.80322,8 ms
12818.0987,1 ms2.50351,1 ms

Dois padrões aparecem:

  • A vazão para de crescer cedo. Na leitura, que só usa CPU, o pico veio com 4 conexões, o dobro dos núcleos. No teste misto, com escritas que esperam o disco confirmar, o pico veio com 16: enquanto umas esperam o disco, outras usam a CPU.
  • Depois do pico, só a latência cresce. De 16 para 128 conexões no teste misto, a vazão caiu 25% e a latência média foi de 4,8 ms para 51 ms. As conexões a mais não fazem mais trabalho: esperam a vez, disputam travas e memória, e o banco gasta tempo trocando entre elas.

Os números são de um banco pequeno numa máquina de desenvolvimento, e o ponto exato do pico depende do hardware e da carga. O formato da curva é o que se repete.

Na primeira tentativa, o banco foi limitado com --cpus=2, uma cota de CPU do Docker. Com essa cota, a vazão de leitura despencou de 23 mil para 5 mil transações por segundo ao passar de 2 para 4 conexões: a cota pausa o processo inteiro quando ele gasta a fatia, e as pausas dominam o resultado. Prender o banco a 2 núcleos (--cpuset-cpus) deu a curva acima, que se comporta como uma máquina com 2 núcleos de verdade. Limitar a CPU de um banco por cota distorce o desempenho.

A conta das instâncias

O PostgreSQL aceita um número máximo de conexões (max_connections, 100 por padrão), e cada uma ocupa um processo e memória no servidor. O pool é configurado por instância da aplicação, e a conta soma:

10 instâncias × pool de 20 = 200 conexões
+ jobs, migrações, ferramentas de administração

Com max_connections = 200, um teste com 300 clientes direto no banco falhou a partir do cliente 251 com FATAL: sorry, too many clients already. Em produção, isso aparece no dia em que o escalonamento automático sobe mais instâncias por causa de um pico de acessos, e o banco começa a recusar conexões justo no pico.

Um intermediário: o PgBouncer

O PgBouncer fica entre a aplicação e o banco e multiplexa: aceita milhares de conexões de clientes e as atende com poucas conexões reais no banco. Testamos com 300 clientes e o PgBouncer em modo transação, com 16 conexões no banco:

Conexões no bancoResultado
300 clientes direto no banco(limite de 200)Falhou: too many clients already
300 clientes pelo PgBouncer1618.074 transações/s, latência média de 16,6 ms

O PgBouncer atendeu os 300 clientes com 16 conexões, com vazão perto do pico medido sem ele. A latência média de 16,6 ms é a espera na fila do próprio PgBouncer: 300 clientes disputando 16 conexões.

O modo transação e o estado da sessão

No modo transação, o PgBouncer empresta a conexão do banco só durante cada transação. A consulta seguinte do mesmo cliente pode cair numa conexão diferente, e outro cliente pode pegar a conexão que ele acabou de usar. Tudo o que fica guardado na sessão do banco vaza. Testamos com duas aplicações e um PgBouncer com uma única conexão no banco:

PassoResultado
A aplicação A faz SET app.empresa_id = '1'—
A aplicação B, que nunca definiu nada, lê a configuração'1', a empresa da aplicação A
B faz SET app.empresa_id = '2', e A lê de novo'2', a empresa da aplicação B
A usa SET LOCAL dentro de uma transação, e B lê depois'': nada vazou

É a mesma armadilha do tópico de autorização, agora entre aplicações diferentes. No modo transação, configurações de sessão (SET), travas consultivas de sessão, tabelas temporárias e LISTEN não funcionam como esperado. Use SET LOCAL dentro de transações. Consultas preparadas, que versões antigas do PgBouncer quebravam no modo transação, funcionaram no teste com o PgBouncer 1.26.

Por que isso importa quando a IA escreve o código

Um assistente que conecta a aplicação ao banco costuma acertar a conexão e errar o dimensionamento:

  • Uma conexão nova por pedido, aberta e fechada dentro da rota.
  • Um pool enorme "para aguentar o pico", que só aumenta a latência e estoura o limite do banco quando as instâncias se multiplicam.
  • Nenhum tempo limite para conseguir uma conexão: com o pool cheio, os pedidos esperam para sempre.
  • Conexões pegas manualmente e não devolvidas em todos os caminhos.
  • SET de sessão atrás de um PgBouncer em modo transação.
  • Um pool por módulo ou por requisição, em vez de um por processo.

O próximo nível compara as formas de organizar as conexões e como calcular o tamanho do pool.

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Pool de conexões em cinco minutos

    Explique em no máximo cinco minutos de leitura o que é um pool de conexões com o banco, para alguém que já escreveu uma API que consulta um banco, mas nunca configurou um pool.
    
    Cubra só o essencial: quanto custa abrir uma conexão, como o pool reaproveita, por que mais conexões não significa mais vazão, a conta das instâncias contra o limite do banco e o papel de um intermediário como o PgBouncer.
    
    Use o exemplo de uma API com várias instâncias. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
  • Por que mais conexões deixam o banco mais lento

    Quero entender por que aumentar o número de conexões com o banco, a partir de certo ponto, piora o desempenho.
    
    Aja como um mentor e vá por etapas, esperando a minha resposta em cada uma:
    1. O que cada conexão consome no servidor do PostgreSQL.
    2. Por que a vazão para de crescer perto do número de núcleos, e o papel da espera por disco.
    3. Por que a latência continua crescendo depois do pico.
    4. A Lei de Little aplicada ao pool: quantas conexões uma carga realmente ocupa.
    5. Como medir o ponto ideal com o pgbench.
    
    Em cada etapa, me dê um cenário numérico e peça que eu calcule ou preveja o resultado.
  • PgBouncer e os modos de pool

    Quero entender o PgBouncer e os modos de pool.
    
    Explique:
    1. O que o PgBouncer faz entre a aplicação e o banco.
    2. Os modos sessão, transação e comando, e o que cada um empresta.
    3. O que quebra no modo transação: SET, travas consultivas, tabelas temporárias, LISTEN, e por quê.
    4. Como as consultas preparadas funcionam nas versões recentes.
    5. Quando usar um intermediário e quando o pool da aplicação basta.
    
    Depois me descreva três trechos de código e pergunte se cada um funciona atrás de um PgBouncer em modo transação.
  • Pool esgotado e vazamento de conexões

    Quero entender o que acontece quando o pool de conexões se esgota.
    
    Explique, com exemplos:
    1. Pool esgotado por carga, por consultas lentas e por conexões não devolvidas.
    2. Por que, sem tempo limite de espera, a aplicação inteira para.
    3. Como reconhecer cada caso pelas métricas do pool.
    4. Os erros de código que vazam conexões, em várias linguagens.
    5. As formas de pegar uma conexão que a devolvem sozinhas.
    
    Depois me mostre três trechos de código e peça que eu encontre o vazamento.