04 - Boas Práticas
video: https://youtu.be/... no frontmatter da nota.Boas Práticas de Pool de Conexão
As boas práticas de pooling convergem para um conjunto pequeno de princípios que, quando aplicados com disciplina, eliminam a maior parte dos incidentes de conexão que se vê em produção. O tema não é configurar um pool uma vez, e sim governar o ciclo de vida das conexões como recurso compartilhado de plataforma: quem pode abrir quantas, por quanto tempo, com que monitoramento e qual resposta a saturação. Este arquivo organiza essas práticas em quatro frentes: o escopo correto de empréstimo da conexão (requisição versus transação), a contabilidade agregada de conexões por serviço, o monitoramento essencial com seus limiares e um checklist objetivo para revisão de produção. O objetivo é que o pool deixe de ser fonte de surpresas e vire componente previsível, com comportamento conhecido sob pico, sob failover e sob deploy.
Escopo do empréstimo: requisição versus transação
A decisão de quando abrir e devolver a conexão define a eficiência de todo o sistema. A prática recomendada é o empréstimo por transação: a conexão é obtida no início da unidade transacional e devolvida no commit ou rollback, nunca antes e nunca depois. O empréstimo por requisição inteira é aceitável em fluxos curtos que tocam o banco uma ou duas vezes, mas torna-se desperdício quando a requisição faz chamadas externas lentas entre acessos ao banco, pois a conexão fica presa sem trabalhar. O padrão a evitar é o empréstimo por método utilitário, com múltiplas aquisições e devoluções dentro da mesma requisição, o que fragmenta transações e esconde problemas de consistência.
- Transação curta e única: obtenha a conexão no limite do caso de uso (o handler do caso de negócio), e não em cada repositório, para que a transação cubra a invariante inteira.
- Sem I/O externo dentro da transação: chamadas HTTP, envio de e-mail e leitura de fila devem ficar fora do escopo da conexão; o padrão outbox existe exatamente para isso.
- Devolução garantida por infraestrutura: context managers, try-with-resources e escopos de transação do framework devolvem a conexão mesmo em exceção; abertura manual é exceção justificada.
- Transações longas como alerta de desenho: se o negócio exige transação de minutos, o problema é de modelagem; repense escopo antes de ajustar timeouts para acomodar.
Um pool por serviço e limites agregados
Cada serviço mantém seu pool, mas o banco enxerga apenas a soma de todos eles, e é esse número agregado que precisa caber no max_connections com folga para operação, migrações e emergências. A contabilidade faz parte do desenho da plataforma, não é detalhe deixado para o primeiro incidente.
| Serviço | Instâncias | Pool por instância | Total direto | Via pooler |
|---|---|---|---|---|
| api-pedidos | 12 | 8 | 96 | 96 |
| api-pagamentos | 6 | 8 | 48 | 48 |
| worker-notificacoes | 4 | 4 | 16 | 16 |
| jobs-batch | 2 | 6 | 12 | 12 |
| funções serverless | variável | 1 | imprevisível | 60 (teto no proxy) |
| Total no banco | 172 direto | 232 com teto arbitrado |
O exemplo ilustra dois desenhos possíveis: sem pooler, o total direto precisa caber no banco; com pooler em transaction mode, o teto de conexões de servidor é arbitrado no pooler, e as funções serverless param de ser ameaça variável. Práticas associadas:
- Reserve uma fração do
max_connectionspara superusuário e operação, e monitore o consumo por usuário de banco, não apenas o total. - Estabeleça cota por serviço no pooler (
max_db_connectionspor papel), de modo que um serviço descontrolado não consuma a fatia dos demais. - Revise a contabilidade a cada novo serviço e a cada mudança de réplicas máxima do auto scaling, tratando a soma como capacidade planejada.
- Prefira muitas instâncias com pools pequenos a poucas instâncias com pools gigantes, pois a contenção muda de lugar, mas o custo de conexão não.
Monitoramento essencial do pool
O conjunto mínimo de métricas responde três perguntas: o pool está saturado, o banco está lento ou há vazamento. Sem elas, o diagnóstico é adivinhação sob pressão.
| Métrica | Fonte | Limiar típico de alerta | Interpretação |
|---|---|---|---|
| Tempo de aquisição (p95/p99) | Pool (Hikari, driver) | p99 > 50 ms sustentado | Fila por conexão; checar causa |
| Threads esperando (wait queue) | Pool | Qualquer valor estável > 0 | Saturação de pool ou vazamento |
| Timeouts de aquisição | Pool | > 0 em regime estável | Incidente imediato |
| Conexões ativas versus idle | Pool e banco | Idle próximo de zero no pico | Pool pequeno para a carga |
| Conexões por banco no agregado | Pooler ou pg_stat_activity | > 80% do max_connections | Risco de recusa de conexão |
| Conexões emprestadas há > 60 s | Leak detection | Qualquer ocorrência | Vazamento: bug de código |
| Transações mais longas | Banco (pg_stat_activity) | > 30 a 60 s | Risco de bloqueio e de vacuum |
# Regras de alerta (Prometheus) para a saúde do pool
groups:
- name: pool-conexoes
rules:
- alert: PoolEsperaAlta
expr: histogram_quantile(0.99, rate(db_pool_acquire_seconds_bucket[5m])) > 0.05
for: 5m
- alert: PoolTimeoutAquisicao
expr: increase(db_pool_acquire_timeout_total[5m]) > 0
for: 1m
- alert: ConexoesNoBancoAcimaDoTeto
expr: pg_stat_database_numbackends / pg_settings_max_connections > 0.8
for: 10m
- alert: LeakSuspeito
expr: increase(db_pool_leak_detected_total[10m]) > 0
for: 1m
Proteção contra bloqueio e transação longa
Transação longa é o inimigo silencioso do pool: ela prende conexão, retém locks, bloqueia vacuum no PostgreSQL e enfileira tudo atrás dela. As práticas de proteção combinam limite, observação e desenho:
- Defina
idle_in_transaction_session_timeoutno banco (por exemplo, 60 segundos) para matar sessões que abriram transação e pararam de trabalhar, o erro clássico de código que esquece commit. - Configure
statement_timeoutelock_timeoutpor papel, conforme o arquivo de configuração deste módulo, e trate exceções por fluxo, não por exceção geral. - Monitore a transação mais longa corrente e alerte quando passar do orçamento; no runbook, a ação é identificar a sessão e terminar com cuidado.
- Padrões de alívio aplicam-se aqui também: bulkhead separa o pool de consultas lentas do pool transacional, e o circuit breaker evita que fila de banco encha durante degradação.
- Operações em massa (backfill, migração de dados) rodam em janela própria, com pool dedicado e limites próprios, nunca no pool do OLTP.
Deploy, failover e eventos que mexem no pool
Dois eventos rotineiros testam o desenho do pool sem que ninguém os tenha configurado para isso: o deploy que recicla todas as instâncias e o failover do banco. No deploy, a rajada de instâncias novas abrindo pools completos somada às antigas derrubando conexões pode dobrar temporariamente o consumo; padrões de proteção incluem rampa no auto scaling, aquecimento do pool antes do health check liberar tráfego e gateways que drenam instâncias antigas com elegância. No failover, conexões presas em transação morrem com erro, e o comportamento esperado é o retry com idempotência no lado da aplicação; o pool deve fechar imediatamente conexões marcadas como quebradas, evitando entregar conexão morta em cascata. Teste os dois eventos periodicamente em staging, medindo o tempo de recuperação do pool e o p99 das rotas críticas durante a transição, pois o desenho que funciona em dia tranquilo é o que costuma falhar exatamente no dia anômalo.
Erros comuns
- Pool gigante como resposta a todo sintoma, invertendo a relação de causa: a contenção no banco piora com cada conexão adicionada.
- Vazamento silencioso de conexões em rotas de erro, onde exceção pula o código de devolução; sem leak detection, o sintoma aparece dias depois como esgotamento.
- Bloqueio em transação longa segurando fatia do pool e disparando espera em cascata, diagnosticado como "problema do banco" sem olhar
pg_stat_activity. - Uma conexão por requisição mantida durante chamadas externas lentas, transformando latência de terceiro em consumo de conexão de banco.
- Limites agregados nunca contabilizados, com a primeira recusa de conexão acontecendo em pleno pico de vendas.
- Falta de alerta para wait queue, com equipe descobrindo saturação por reclamação de usuário, e não por métrica.
- Timeouts desconectados entre camadas (requisição maior que aquisição maior que statement), criando esperas sem valor e retries sem sentido.
- Reuso de um mesmo pool para múltiplos bancos ou papéis, impossibilitando limites e auditoria separados por consumidor.
- Health check do serviço que não valida a cadeia de banco, liberando tráfego para instâncias com pool morto após falha de rede.
Boas práticas
- Padronize o empréstimo por transação como política de código, com revisão automática de exceções e justificativa registrada.
- Mantenha a hierarquia de devolução garantida por infraestrutura: proíba abertura manual de conexão fora da camada de persistência em revisão de código.
- Mantenha a contabilidade de conexões por serviço viva em documento de plataforma, revisada em todo onboarding de sistema e em mudanças de escala.
- Exporte o conjunto completo de métricas do pool e do banco para o mesmo dashboard, com limiares e runbook por alerta definidos antes do go-live.
- Configure a hierarquia de timeouts coerente do maior para o menor (deadline da requisição, aquisição, statement, lock), testada sob falha real.
- Habilite leak detection em produção com alerta, tratando cada ocorrência como bug com stack trace anexado.
- Separe pools e papéis por natureza de carga (OLTP, batch, analítico em réplica), com limites independentes e tolerância de queda separada.
- Inclua o pool no teste de caos: injete atraso e falha no banco e verifique que degradação, retry e recuperação seguem o desenho esperado.
- Espelhe as práticas em ambiente de desenvolvimento, com pool reduzido mas com a mesma hierarquia de timeouts, para que o comportamento em staging preveja o de produção.
- Diferencie o tratamento de falha por tipo de consumidor: requisição de usuário recebe erro rápido com degradação; job batch recebe retry longo com backoff.
- Revise o desenho de pool junto com o plano de capacidade trimestral, usando os últimos três meses de métricas de espera e saturação como evidência.
- Normalize os nomes de métrica de pool entre serviços da plataforma, para que dashboards globais agreguem sinais sem retrabalho por equipe.
- Registre decisões de pooling relevantes (modo do pooler, tamanho, mudanças de limite) em ADR, mantendo o histórico de por que cada número existe.
Checklist de produção
O checklist a seguir resume o estado mínimo esperado de um serviço maduro em pooling; cada item é verificável e vale para revisão de arquitetura e para auditoria de operação:
- Pool configurado explicitamente (tamanho, mínimos, timeouts), sem defaults de framework herdados sem revisão.
- Fórmula de tamanho documentada com a medição que a justificou, e contabilidade agregada compatível com o
max_connectionsdo banco. - Timeouts calibrados em camadas coerentes, incluindo statement e lock timeout no banco por papel de conexão.
- Leak detection ativo com alerta e procedimento de investigação por stack trace.
- Métricas de aquisição, espera e timeouts exportadas, com dashboard e alertas revisados nos últimos seis meses.
- Limites por serviço no pooler (quando houver) e cota agregada monitorada contra o teto do banco.
- Teste de carga com pico real executado no desenho atual do caminho de conexão, incluindo pooler e failover.
- Runbook de saturação e de vazamento publicado, com passos objetivos e dono definido.
- Transações longas monitoradas com limiar de alerta, e padrões de I/O externo dentro de transação proibidos em revisão de código.
- Plano de capacidade revisitado a cada mudança de réplicas máximas, de arquitetura ou de perfil de tráfego.
Serviços que cumprem esse conjunto raramente aparecem em post-mortems com "esgotamento de conexões" como causa raiz, e quando aparecem, o runbook encurta o tempo de restauração a minutos.