Reuso de estruturas
Por que reutilizar estruturas de dados?
Acho que eu já devo ter dito isso antes mas não custa frisar, as vezes o nosso código é limitado por grandezas físicas, ou seja existe um limite máximo de processamento, de memória ou banda de rede. A todo software corresponde um hardware que é físico e existe em algum lugar.
Mesmo as nuvens, aquelas que a gente vê no céu, são formadas por uma quantidade finita de moléculas de água.
Partindo-se desse pressuposto uma das melhores formas de otimização de código sempre será evitar aquilo que não for estritamente necessário e uma boa técnica para isso é a reutilização de estruturas pré construídas.
Pooling
Pooling é uma técnica onde se reutilizam objetos cuja a inicialização implique em custo significativo e consiste em manter um conjunto de objetos pré instanciados e prontos para utilização imediata.
Um exemplo de reuso é o pool de conexões ao banco de dados. Existe um custo envolvido em abrir uma conexão com o banco de dados e esse custo pode ser diluído ao utilizar uma conexão já aberta para executar um novo comando.
É o que fazem os providers de acesso a dados, como o Microsoft.Data.SqlClient por exemplo, ao criar pools de conexão e gerenciar essas conexões em nome dos desenvolvedores.
Ao fechar uma conexão essa conexão não é realmente encerrada e sim mantida aberta e devolvida para o pool de conexões, ao abrir uma nova conexão caso haja uma conexão disponível no pool essa conexão será utilizada ao invés de abrir uma nova reutilizando todo o trabalho envolvido na abertura de uma nova conexão.
No caso do Microsoft.Data.SqlClient os pools são organizados tendo a string de conexão como chave, portanto o ponto mais importante para garantir que conexões ao banco de dados sejam reutilizadas é usar sempre a mesma string de conexão, normalmente definida na configuração.
DbContext Pooling
No EF Core as operações de acesso a dados são feitas através de objetos cujas classes derivam de DbContext e internamente cada instância de DbContext cria uma serie de objetos que suportam as funcionalidades do objeto.
De maneira geral o impacto envolvido em criar e liberar instâncias de DbContext não é significativo para grande parte das aplicações, porém pode ser significativo em aplicações que exijam alta performance onde cada milissegundo poupado faz diferença no resultado geral.
Para esses cenários pode-se utilizar um pool de objetos DbContext para reutilizar instâncias e diminuir o número de vezes em que a inicialização ocorre.
Utilizando-se injeção de dependências basta substituir a chamada para AddDbContext
por AddDbContextPool<T>
:
builder.Services.AddDbContextPool<WeatherForecastContext>(
o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));
O método AddDbContextPool
pode receber um parâmetro poolSize
que determina o número de instâncias mantidas no pool o default desse parâmetro é 1024 para EF Core 6.0 e 128 para versões anteriores.
No EF Core 6.0 é possível também utilizar o pool de DbContext sem utilizar injeção de dependências através da classe PooledDbContextFactory<T>
:
var options = new DbContextOptionsBuilder<PooledBloggingContext>()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True")
.Options;
var factory = new PooledDbContextFactory<PooledBloggingContext>(options);
using (var context = factory.CreateDbContext())
{
var allPosts = context.Posts.ToList();
}
É bom deixar claro que o pooling de DbContext ocorre de forma independente do pooling de conexões que é feito na camada do provedor de acesso a dados, ou seja quando o DbContext solicitar uma conexão de dados essa conexão virá do pool de conexões mantido pela provedor de acesso a dados caso habilitado.
Instâncias de DbContext mantidas em pool são inicializadas apenas uma vez e reutilizadas em cada requisição, o que na prática acaba sendo um singleton reaproveitado em múltiplos escopos de injeção de dependência e o método OnConfiguring
é executado apenas uma vez.
Nesse caso o estado de uma requisição pode vazar para outras requisições caso não seja gerenciado adequadamente, como por exemplo aplicações que utilizam filtros globais em consultas. Na documentação do EF Core 6 temos um exemplo de como implementar um IDbContextFactory
personalizado para garantir o estado correto em cada requisição. Link.
Conclusões
Pooling é uma boa técnica de programação para poupar o uso de processamento, porém não é gratuito visto que manter objetos pré-instanciados implica em maior consumo de memória.
Trade-offs, ou negociações sempre vão existir e para economizar em algum recurso sempre será necessário investir em outro, portanto como tudo o que se refere à otimização de código é necessário tirar medidas e comparar se o resultado obtido compensa o tempo investido.
Como citado no texto, o pooling de DbContexts faz mais sentido a medida que o volume de acesso a dados nas aplicações cresce, já o pooling de conexões é um recurso de uso praticamente mandatório visto que o custo envolvido em abrir conexões com os servidores de dados é significativo na maioria dos cenários.