https://unsplash.com/photos/ImcUkZ72oUs

Pool de conexões com Spring e Postgres

Como funciona e por que é necessário?

Marcelo Rodrigues
Published in
6 min readAug 10, 2022

--

Com o aumento do uso de frameworks perdemos o contato com os detalhes mais técnicos para ganhar velocidade ao resolver problemas de negócio. Apesar de ser essencial para a maioria das necessidades, às vezes encontramos dificuldades de entender certos comportamentos quando precisamos aumentar a capacidade de processamento ou até mesmo identificar por que uma solicitação não está com o desempenho esperado.

Cada sistema de banco de dados relacional pode usar diferentes estratégias para garantir as propriedades ACID e manter alta performance com mais foco em determinadas operações, por isso nessa série vamos falar especificamente sobre o funcionamento do Postgres e a configuração do serviço na AWS (RDS Postgres), para podermos entrar em mais detalhes.

O objetivo desse post é falar sobre o funcionamento do pool de conexões, usado por padrão no Spring a partir da versão 2.0, e por que ele é necessário.

Arquitetura padrão do banco de dados Postgres

Diferente dos serviços de backend, onde costumamos usar múltiplas instâncias de serviço com balanceamento de carga, por padrão o processo do postgres funciona em apenas uma máquina para garantir de forma simples e eficiente o controle de concorrência, mantendo a integridade dos dados com semáforos e memória compartilhada.

Por esse motivo a escalabilidade é limitada e precisamos economizar os recursos de processamento.

Conexão do lado do cliente

Para executarmos uma query no banco de dados precisamos de uma conexão ativa com o servidor e a abertura dessa conexão envolve as etapas no Postgres:

  1. Conexão com o banco de dados
  2. Interpretação da query
  3. Aplicação das regras de transformação (Ex: Transformação das queries sobre views em queries sobre as tabelas reais)
  4. Planejamento e otimizações para execução
  5. Busca da informação

Nesse post focaremos na etapa de conexão.

Conexão do lado do servidor

A cada conexão aberta com o banco de dados do Postgres o servidor executa um fork do processo para manter a independência dos processamentos e evitar que uma falha durante uma execução interfira no serviço como um todo.

O gerenciamento de conexões do banco de dados também consome recursos de processamento e memória para manter os diferentes contextos e permitir o processamento de diversas queries.

Inclusive, para não comprometer a performance do banco de dados, o Postgres limita as conexões conforme o parâmetro max_connections. No caso do RDS Postgres esse valor é configurável em “Parameter groups”, sendo que por padrão é calculado em relação à quantidade de memória RAM, limitado a 5000.

Connection pool

Se precisarmos executar novamente todas as etapas de conexão a cada consulta no banco de dados, toda query será lenta para o cliente e custosa para o servidor. Para evitar isso foi criado o conceito de connection pool. No Spring a lib padrão que gerencia o Connection Pool é a HikariCP.

O HikariCP provê algumas funcionalidades que otimizam bastante esse processo:

  • Reaproveitamento de conexões
  • Limitação da quantidade de conexões simultâneas
  • Identificação de alguns estados de falha da conexão e abertura automática de nova conexão

Detalhes e exemplos práticos das funcionalidades do connection pool

Reaproveitamento de conexões

Sempre que uma aplicação solicitar uma conexão para o HikariCP, uma das conexões do pool será disponibilizada. Quando a query for finalizada e a aplicação solicitar o encerramento da conexão o HikariCP a retornará para o pool, para ser reaproveitada ao invés de encerrá-la de fato.

Observando o reuso de conexões na prática

O exemplo abaixo abre 10 conexões simultâneas com o banco e as fecha, abrindo uma nova conexão no final para observarmos o comportamento de conexões estabelecidas através do PgAdmin.

Script ConnectionPoolConnectionReuseExample.kt

Resultado da execução:

[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[2022-04-22T20:39:59.316Z] Conexão 1 aberta
[2022-04-22T20:39:59.813Z] Conexão 2 aberta
[2022-04-22T20:40:00.314Z] Conexão 3 aberta
[2022-04-22T20:40:00.813Z] Conexão 4 aberta
[2022-04-22T20:40:01.314Z] Conexão 5 aberta
[2022-04-22T20:40:01.814Z] Conexão 6 aberta
[2022-04-22T20:40:02.314Z] Conexão 7 aberta
[2022-04-22T20:40:02.814Z] Conexão 8 aberta
[2022-04-22T20:40:03.314Z] Conexão 9 aberta
[2022-04-22T20:40:03.814Z] Conexão 10 aberta
[2022-04-22T20:40:04.322Z] Conexão 1 fechada
[2022-04-22T20:40:04.814Z] Conexão 2 fechada
[2022-04-22T20:40:05.313Z] Conexão 3 fechada
[2022-04-22T20:40:05.813Z] Conexão 4 fechada
[2022-04-22T20:40:06.313Z] Conexão 5 fechada
[2022-04-22T20:40:06.814Z] Conexão 6 fechada
[2022-04-22T20:40:07.313Z] Conexão 7 fechada
[2022-04-22T20:40:07.813Z] Conexão 8 fechada
[2022-04-22T20:40:08.313Z] Conexão 9 fechada
[2022-04-22T20:40:08.814Z] Conexão 10 fechada

Process finished with exit code 0

O número de conexões permanece estável em 10 (+1 do próprio PGAdmin) do início ao fim do processamento. Ao iniciar o HikariCP ele abre 10 conexões automaticamente e as mantém ativas após o fechamento, mesmo sem nenhum pedido de conexão.

Limitação da quantidade de conexões simultâneas

O benefício em limitar a quantidade de conexões simultâneas é evitar a sobrecarga do banco de dados ou até atingir o limite de conexões do servidor. O processamento do método para obter conexão é interrompido até uma conexão ser liberada ou o limite de tempo de espera é atingido (Nesse caso é lançada uma SQLException).

Observando o limite de conexões e timeout na prática

O exemplo abaixo abre 11 conexões simultâneas com o banco para observarmos o funcionamento do limite.

Resultado:

[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[2022-04-22T17:50:38.682Z] Aguardando conexão
[2022-04-22T17:51:08.690Z] Fim da execução
Exception in thread "main" java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696)
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:197)
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162)
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
at br.com.studylibrary.ConnectionPoolLimitTimeoutExampleKt.main(ConnectionPoolLimitTimeoutExample.kt:25)
at br.com.studylibrary.ConnectionPoolLimitTimeoutExampleKt.main(ConnectionPoolLimitTimeoutExample.kt)

Process finished with exit code 1

O 11º pedido de conexão é colocado em espera, bloqueando a execução do código até que uma conexão seja liberada. Após 30s de espera é lançada uma Exception.

O exemplo abaixo abre 11 conexões e fecha 1 após 2s para observarmos a liberação de conexão.

Resultado:

[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
[2022-04-22T17:57:20.959Z] Aguardando conexão
[2022-04-22T17:57:22.975Z] Conexão liberada após 2s
[2022-04-22T17:57:22.976Z] Fim da execução

Process finished with exit code 0

Como no exemplo anterior, a 11ª abertura entra em espera até que seja liberada uma execução, mas neste exemplo liberamos uma após 2s, o que faz com que a aplicação continue a executar.

Identificação de estado de falha

Quando a aplicação solicita uma conexão o HikariCP verifica o estado da conexão antes de retornar para a aplicação. Se a conexão estiver inválida ele abre uma nova conexão com o banco de dados antes de retornar.

Resumo e recomendações

A execução de queries nos bancos de dados relacionais consome recursos importantes e não conseguimos escalar de forma horizontal por padrão, como a maioria dos serviços de backend. Para otimizarmos o uso desses recursos utilizamos o conceito de connection pool, que reaproveita as conexões que seriam fechadas, além de limitar a abertura de conexões de forma descontrolada por uma aplicação.

O connection pool também nos traz mecanismos de segurança para a aplicação não ficar “congelada”, aguardando uma conexão que nunca é liberada.

Sempre que conectamos em um banco de dados relacional usando um Connection Pool a melhor prática é sempre buscarmos reduzir o tempo de uso de uma conexão, abrindo o mais tarde possível e fechando o mais cedo possível, para retorná-la para o pool.

Todos os exemplos estão disponíveis com os detalhes de configuração no repositório: https://github.com/Marcelo-Rodrigues/connection-pool-study

--

--

Marcelo Rodrigues
Creditas Tech

Senior Software Engineer at Creditas. More than 15 years developing software professionally