Entenda o que é e como evitar exaustão de portas TCP utilizando a classe HttpClient no .NET Core
Published Feb 06 2023 09:22 AM 3,951 Views
Microsoft

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.

 

O problema

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étodo Dispose da instância denominada client, será invocada no final do escopo do seu contexto execução. Essa sintaxe do using é 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.

 

Reprodução do problema

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.

 

Projeto

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.

 

Netstat

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.

 

Time Wait

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.

 

Conclusão

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á.

client-application-code

 

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.

2 Comments
Co-Authors
Version history
Last update:
‎Feb 06 2023 09:18 AM
Updated by: