Performance de código Entity Framework Core - Parte 5
Published Oct 11 2022 05:46 AM 2,650 Views
Microsoft

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.

Referências

Co-Authors
Version history
Last update:
‎Oct 11 2022 05:46 AM
Updated by: