No último trimestre realizei algumas análises de performance em Web API'S desenvolvidas utilizando o ASP.NET Core, e a maioria estava com o mesmo problema: O uso incorreto dos objetos de tipo HttpClient. Percebi então que havia um padrão problemático na sua utilização.
Portanto resolvi escrever este artigo, com o intuito de ajudar os desenvolvedores a utilizarem corretamente esse tipo de objeto.
Realizar chamadas HTTP
é uma tarefa simples, você só precisa criar instanciar de um objeto do tipo HttpClient, configurar algumas propriedades e pronto, você está apto a realizar sua chamada HTTP
. A classe HttpClient implementa a interface IDisposable, o que significa que se você é um desenvolvedor atento, invocará o método Dispose
, para que o GC possa liberar os recursos nativos apropriadamente quando essa instância for descartada.
A implementação mais comum segue um padrão parecido com:
using HttpClient client = new HttpClient();
client.BaseAddress = new Uri("https://google.com");
var response = await client.GetAsync("");
A palavra reservada
using
garante que o métodoDispose
da instância denominadaclient
, será invocada no final do escopo do seu contexto execução. Essa sintaxe dousing
é relativamente nova, você pode se deparar com o seu uso da seguinte forma:using(HttpClient client = new HttpClient()){}
.
O código acima deve funcionar em alguns cenários, porém, em um momento de alto volume de requisições, sua aplicação pode apresentar um aumento no tempo de resposta, chegando até a ficar indisponível, e você pode começar a observar exceções do tipo System.Net.Sockets.SocketException
sendo lançadas com a mensagem: Only one usage of each socket address (protocol/network address/port) is normally permitted.
Para que você possa entender melhor esse comportamento, uma boa estratégia é reproduzi-lo em um ambiente controlado. Para esse exemplo, você precisará do .NET Core 6.0.
Em uma pasta de sua preferência crie um projeto utilizando o template de Console
do .NET CLI, através do comando dotnet new console
.
No arquivo Program.cs
copie o código abaixo:
while(true)
{
using (var client = new HttpClient())
{
var result = await client.GetAsync("http://techcommunity.microsoft.com/t5/desenvolvedores-br/bg-p/DesenvolvedoresBR");
Console.WriteLine(result.StatusCode);
}
}
O código acima implementa um laço infinito, onde cada iteração instancia um novo HttpClient
e realiza uma requisição ao endereço [http://techcommunity.microsoft.com/t5/desenvolvedores-br/bg-p/DesenvolvedoresBR].
Execute o programa de teste com o comando: dotnet run
.
Primeiro precisamos descobrir o endereço IP do nome techcommunity.microsoft.com
. Para tal, abra um prompt de comando, e rode o comando nslookup techcommunity.microsoft.com
. Você verá um resultado parecido com:
Server: UnKnown
Address: 192.168.86.1
Non-authoritative answer:
Name: e8318.dsca.akamaiedge.net
Addresses: 2600:1419:4e00:286::207e
2600:1419:4e00:28c::207e
96.6.215.78
Aliases: techcommunity.microsoft.com
gxcuf89792.lithium.com
techcommunity.microsoft.com.edgekey.net
Os endereços IPs que precisamos estão no campo Addresses (2600:1419:4e00:286::207e
, 2600:1419:4e00:28c::207e
e 96.6.215.78
), guarde-os para usarmos na análise.
Deixe a aplicação rodando por alguns minutos, e termine o processo em seguida.
Utilizaremos a ferramenta netstat para visualizarmos as conexões TCP
, em seguida realizaremos um filtro utilizando os endereços de IP que capturamos anteriormente. Para isso rode o comando:
netstat -an | findstr /c:"2600:1419:4e00:286::207e" /c:"2600:1419:4e00:28c::207e" /c:"96.6.215.78"
No meu caso, o resultado foi:
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65293 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65295 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65297 [2600:1419:1e00:582::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65301 [2600:1419:1e00:582::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65303 [2600:1419:1e00:582::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65305 [2600:1419:1e00:582::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65307 [2600:1419:1e00:582::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65309 [2600:1419:1e00:582::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65311 [2600:1419:1e00:582::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65313 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65315 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65318 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65320 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65322 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65324 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65326 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65328 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65330 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65332 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65336 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65338 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65371 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65373 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65375 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65377 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65380 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65382 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
TCP [2804:431:d728:afa7:61bf:a077:45be:68b3]:65384 [2600:1419:1e00:58b::207e]:80 TIME_WAIT
Repare que mesmo após parar o processo ainda existem 27 conexões no estado de: TIME_WAIT
.
Este problema é conhecido como: exaustão de portas TCP
ou TCP port exhaustion
em inglês. Significa que todas as portas elegíveis à estabelecer a conexão TCP
, estão em uso.
Vamos revisar o código da aplicação, para entender melhor o seu comportamento:
while(true)
{
using (var client = new HttpClient())
{
var result = await client.GetAsync("http://techcommunity.microsoft.com/t5/desenvolvedores-br/bg-p/DesenvolvedoresBR");
Console.WriteLine(result.StatusCode);
}
}
Quando a instrução new HttpClient()
é executada, uma nova conexão TCP
é criada, porém quando o método Dispose
é executado no final do bloco using
, a porta TCP
usada pela aplicação não é liberada instantaneamente, ao invés disso, ela entra em um estado de: TIME_WAIT
.
O SO
mantém essas conexões por um tempo pré-definido. Por padrão, o estado de TIME_WAIT
é mantido por 240 segundos, sendo configurável através da chave de registro: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay]
. Mas não é uma boa ideia modificar essa chave, a não ser que você saiba muito bem o que está fazendo.
Para evitar o problema de exaustão de sockets
, devemos criar uma conexão, e reutilizá-la o máximo de vezes possível. Uma saída é criar os objetos HttpClient
como singleton
ou estáticos
.
No nosso exemplo o código ficaria assim:
var client = new HttpClient();
while(true)
{
var result = await client.GetAsync("http://techcommunity.microsoft.com/t5/desenvolvedores-br/bg-p/DesenvolvedoresBR");
Console.WriteLine(result.StatusCode);
}
Essa segunda abordagem, evitaria o problema, porém, no contexto de uma aplicação Web, ela pode trazer alguns problemas, se acontecer alguma mudança em relação a resolução de DNS
, o código acima não será resiliente a ponto de resolvê-la novamente.
Para endereçar esse e outros problemas, o time do ASP.NET Core implementou a interface IHttpClientFactory.
Basicamente essa interface funciona como uma fábrica de HttpClient
, ela será responsável por implementar um pool
de objetos do tipo HttpMessageHandler's
, e os reutilizará.
Você pode usar essa fábrica com diferentes estratégias, a minha preferida é a nomeada de: Typed Clients
ou Cliente tipado
em português.
Com essa estratégia podemos criar classes de serviços específicos, e injetar um HttpClient
no seu construtor através do container de injeção de dependências, por exemplo:
public class CatalogService : ICatalogService
{
private readonly HttpClient _httpClient;
private readonly string _remoteServiceBaseUrl;
public CatalogService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<Catalog> GetCatalogItems(int page, int take,
int? brand, int? type)
{
var uri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl,
page, take, brand, type);
var responseString = await _httpClient.GetStringAsync(uri);
var catalog = JsonConvert.DeserializeObject<Catalog>(responseString);
return catalog;
}
}
Agora bastaria configurar o nosso container de injeção, na classe Program.cs
, ou na classe Startup.cs
em versões anteriores ao .NET 6, da seguinte forma:
services.AddHttpClient<ICatalogService, CatalogService>();
Se você tem dúvidas de como funciona a injeção de dependência no ASP.NET core, sugiro a breve leitura do artigo de um dos meus mestres aqui na Microsoft: Entendendo Injeção de Dependência com .NET.
Particularmente acho essa última abordagem muito elegante, pois, podemos isolar os pontos de integração com outros serviços, de forma desacoplada do resto do projeto. Recomendo fortemente a ler sobre as outras estratégias de criação de objetos HttpClient, viabilizadas pela IHttpClientFactory: Use o IHttpClientFactory para implementar solicitações HTTP resilientes.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.