DEV Community

Cover image for HttpClient: melhoria de performance ao otimizar o uso de memória
Pode me chamar de Juscélio Reis for DEVz Wiz

Posted on • Edited on

HttpClient: melhoria de performance ao otimizar o uso de memória

Neste último mês fiquei investigando incidentes relacionados a alto consumo de memória por parte das nossas aplicações. Consegui encontrar alguns problemas e quero compartilhar com vocês, vamos começar com a utilização do HttpClient, esse cara pode ser um vilão se não for bem utilizado. As APIs da Wiz têm uma característica de chamar outras APIs e me atrevo a dizer que é mais consumo de API do que utilização de banco de dados. Nesse artigo quero explicar como você pode otimizar o desempenho do HttpClient ao lidar com dados como cargas úteis JSON no HttpResponseMessage.

Teoria

O que eu não sabia era que por padrão, na maioria dos casos, ao usar HttpClient e seus metodos (GetAsync, PostAsync e SendAsync), todo o corpo da resposta é lido em um buffer de memória antes que o método seja concluído. Nesse ponto, a conexão TCP, usada para a solicitação fica inativa e estará disponível para reutilização para outra solicitação, um ponto aqui é que estou falando do dotnet core 3.1, se for falar do dotnet 5, existem opções melhores como é o caso da utilização do protocolo http2, aí a história é outra.

Na maioria dos casos esse comportamento é aceitável, já que evita o uso da conexão tcp pelo período mínimo de tempo necessário. Mas... em casos que não temos memória sobressalente, aí entramos no caminho infeliz da história pois essa abordagem padrão introduz alguma sobrecarga de memória. Já que a resposta da API, vou generalizar aqui, o JSON é armazenado em buffer usando um MemoryStream, podemos acessar esse buffer pela classe HttpResponseMessage. Dependendo do tamanho da carga de resposta, isso pode significar que armazenamos em buffer uma grande quantidade de dados na memória.

Solução

O que eu não sabia era que existe uma sobrecarga desses métodos esperando um enum HttpCompletionOption, esse enum possui 2 valores o padrão é o ResponseContentRead, esse aí informa para o HttpClient que é para ler o corpo do JSON e colocar em memória mesmo que a nossa aplicação não vai usar esse objeto, sim isso é possível. O segundo valor é o que iremos começar a utilizar aqui na Wiz chamado ResponseHeadersRead, esse cara indica para o HttpClient quando os cabeçalhos de resposta forem totalmente lidos. O corpo da resposta pode não ser totalmente recebido neste momento.

O principal benefício é o desempenho. Ao usar esta opção, evitamos o buffer MemoryStream intermediário, em vez de obter o conteúdo diretamente do fluxo exposto no Socket. Isso evita alocações desnecessárias, o que é uma meta em situações altamente otimizadas.

Aqui vai um exemplo, quero serializar uma lista de livros apenas quando receber um status code 200. Se eu receber um 500 vou lançar uma exception e não preciso do conteúdo da API que estou consumindo.

A forma de utilizar esse enum é bem simples, olha como fica:

_httpClient.GetAsync("http://openlibrary.org/search.json?q=tdd", HttpCompletionOption.ResponseHeadersRead);
Enter fullscreen mode Exit fullscreen mode

O normal é analisar o conteúdo de alguma forma. Vou mostrar um código de como podemos escrever nossa chamada para isso.

using var response = await _httpClient.GetAsync("http://openlibrary.org/search.json?q=tdd", HttpCompletionOption.ResponseHeadersRead);

response.EnsureSuccessStatusCode();

if (response.Content is object)
{
      var stream = await response.Content.ReadAsStreamAsync();

      var data = await JsonSerializer.DeserializeAsync<Search>(stream);

      // do something with the data or return it
}
Enter fullscreen mode Exit fullscreen mode

Usamos o EnsureSuccessStatusCode para garantir que o status code recebido é um 2xx. Em caso afirmativo, verificamos se há conteúdo disponível na resposta. Agora podemos acessar o fluxo do conteúdo de resposta usando ReadAsStreamAsync.

O problema dessa abordagem é que assumimos mais responsabilidade em relação aos recursos do sistema, uma vez que a conexão com o servidor remoto fica presa até decidirmos que terminaremos com o conteúdo. A maneira como sinalizamos isso é descartando o HttpResponseMessage, que então libera a conexão para ser usada para outras solicitações. Por isso não esquecer do using.

using var response = await _httpClient.GetAsync
Enter fullscreen mode Exit fullscreen mode

Outra forma de garantir isso é usar o try/finaly, veja um exemplo:

var response = await _httpClient.GetAsync("http://openlibrary.org/search.json?q=tdd", HttpCompletionOption.ResponseHeadersRead);

response.EnsureSuccessStatusCode();

Search data = null;

try
{

      if (response.Content is object)
      {
            var stream = await response.Content.ReadAsStreamAsync();
            data = await JsonSerializer.DeserializeAsync<Search>(stream);
      }
}
finally
{
      response.Dispose();
}

if (data is object)
{
      // intensive and slow processing of books list. We don't want this to delay releasing the connection.
}
Enter fullscreen mode Exit fullscreen mode

Estudo de caso

Fiz um programa de teste para ter um benchmark da performance do que citei nesse artigo. Essa analise é feita tanto no Windows como no Linux, usando o dotnet core 3.1. Vocês podem acessar aqui.

Observe a coluna Allocated, usando um sistema Windows com o método WithHttpCompletionOption chegamos a uma performance de 26,87% em comparação com WithoutHttpCompletionOption que representa 73,13% do consumo de memória.

Agora usando um sistema Linux com o método WithHttpCompletionOption chegamos a uma performance de 28,62% em comparação com o método WithoutHttpCompletionOption que representa 71,38% do consumo de memória.

Windows


BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=3.1.404
  [Host]           : .NET Core 3.1.10 (CoreCLR 4.700.20.51601, CoreFX 4.700.20.51901), X64 RyuJIT
  Server           : .NET Core 3.1.10 (CoreCLR 4.700.20.51601, CoreFX 4.700.20.51901), X64 RyuJIT
  ServerForce      : .NET Core 3.1.10 (CoreCLR 4.700.20.51601, CoreFX 4.700.20.51901), X64 RyuJIT
  Workstation      : .NET Core 3.1.10 (CoreCLR 4.700.20.51601, CoreFX 4.700.20.51901), X64 RyuJIT
  WorkstationForce : .NET Core 3.1.10 (CoreCLR 4.700.20.51601, CoreFX 4.700.20.51901), X64 RyuJIT

IterationCount=15  LaunchCount=2  WarmupCount=10  

Enter fullscreen mode Exit fullscreen mode
Method Job Force Server Mean Error StdDev Median Gen 0 Gen 1 Gen 2 Allocated
WithoutHttpCompletionOption Server False True 477.6 ms 65.79 ms 96.43 ms 515.4 ms - - - 404.48 KB
WithHttpCompletionOption Server False True 673.4 ms 73.56 ms 103.12 ms 723.6 ms - - - 162.16 KB
WithGetStreamAsync Server False True 703.2 ms 149.15 ms 213.91 ms 662.3 ms - - - 162.66 KB
WithoutHttpCompletionOption ServerForce True True 414.7 ms 65.04 ms 93.27 ms 363.7 ms - - - 394.98 KB
WithHttpCompletionOption ServerForce True True 642.0 ms 81.61 ms 106.12 ms 642.8 ms - - - 163.63 KB
WithGetStreamAsync ServerForce True True 707.8 ms 97.39 ms 139.68 ms 727.4 ms - - - 162.81 KB
WithoutHttpCompletionOption Workstation True False 659.7 ms 96.77 ms 132.46 ms 643.2 ms - - - 394.62 KB
WithHttpCompletionOption Workstation True False 530.2 ms 9.43 ms 12.59 ms 528.1 ms - - - 162.35 KB
WithGetStreamAsync Workstation True False 439.2 ms 65.54 ms 98.10 ms 452.9 ms - - - 161.59 KB
WithoutHttpCompletionOption WorkstationForce False False 499.3 ms 52.65 ms 77.17 ms 517.6 ms - - - 394.89 KB
WithHttpCompletionOption WorkstationForce False False 626.4 ms 69.05 ms 94.52 ms 600.1 ms - - - 162.98 KB
WithGetStreamAsync WorkstationForce False False 508.5 ms 57.64 ms 82.67 ms 524.8 ms - - - 162.39 KB

Linux


BenchmarkDotNet=v0.12.1, OS=ubuntu 20.04
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 2 logical cores and 1 physical core
.NET Core SDK=3.1.405
  [Host]           : .NET Core 3.1.11 (CoreCLR 4.700.20.56602, CoreFX 4.700.20.56604), X64 RyuJIT
  Server           : .NET Core 3.1.11 (CoreCLR 4.700.20.56602, CoreFX 4.700.20.56604), X64 RyuJIT
  ServerForce      : .NET Core 3.1.11 (CoreCLR 4.700.20.56602, CoreFX 4.700.20.56604), X64 RyuJIT
  Workstation      : .NET Core 3.1.11 (CoreCLR 4.700.20.56602, CoreFX 4.700.20.56604), X64 RyuJIT
  WorkstationForce : .NET Core 3.1.11 (CoreCLR 4.700.20.56602, CoreFX 4.700.20.56604), X64 RyuJIT

IterationCount=15  LaunchCount=2  WarmupCount=10  

Enter fullscreen mode Exit fullscreen mode
Method Job Force Server Mean Error StdDev Median Gen 0 Gen 1 Gen 2 Allocated
WithoutHttpCompletionOption Server False True 426.4 ms 73.53 ms 100.64 ms 409.1 ms - - - 389120 B
WithHttpCompletionOption Server False True 360.1 ms 53.91 ms 77.31 ms 331.1 ms - - - 152472 B
WithGetStreamAsync Server False True 328.8 ms 12.37 ms 17.74 ms 323.7 ms - - - 149624 B
WithoutHttpCompletionOption ServerForce True True 421.0 ms 71.88 ms 103.09 ms 426.8 ms - - - 406960 B
WithHttpCompletionOption ServerForce True True 515.3 ms 71.17 ms 104.32 ms 520.8 ms - - - 149936 B
WithGetStreamAsync ServerForce True True 525.4 ms 142.11 ms 203.81 ms 515.6 ms - - - 150136 B
WithoutHttpCompletionOption Workstation True False NA NA NA NA - - - -
WithHttpCompletionOption Workstation True False 523.6 ms 94.41 ms 138.38 ms 515.3 ms - - - 149520 B
WithGetStreamAsync Workstation True False 582.9 ms 288.63 ms 404.62 ms 354.0 ms - - - 150072 B
WithoutHttpCompletionOption WorkstationForce False False 523.4 ms 44.34 ms 62.16 ms 527.0 ms - - - 389112 B
WithHttpCompletionOption WorkstationForce False False 523.0 ms 11.63 ms 16.68 ms 518.3 ms - - - 150680 B
WithGetStreamAsync WorkstationForce False False 427.7 ms 78.15 ms 104.33 ms 497.9 ms - - - 151472 B

Top comments (1)

Collapse
 
uirapeixoto profile image
uirapeixoto

Parabéns Juscelio pela abordagem, realmente faz muita diferença considerar a forma como é tratado o buffer de memória na resposta da requisição do httpClient, aprendi muito com esse artigo, abraço...