Performance de código Entity Framework Core – Parte 5

This post has been republished via RSS; it originally appeared at: New blog articles in Microsoft Community Hub.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.