HttpClient 单一实例 in .NetCore —— AddHttpClient (转载 + 加工)

httpclient : 应该是一个全局共享变量 =》static  or 单一实例(services.AddHttpClient)

为每个请求实例化 HttpClient 类 在高负载下 将耗尽可用的套接字数,这可能会导致 SocketException 错误。

解决此问题的可能方法基于将 HttpClient 对象创建为单例或静态对象

HttpClient 实例化一次,并在应用程序的整个生命周期内重用。

 

 

 

HttpClient虽然实现了IDisposable接口,但它应该用作单个全局共享对象,而不是在使用block(using)中使用

 

You're using HttpClient wrong and it is destabilizing your software | ASP.NET Monsters (aspnetmonsters.com)

 

The typical usage pattern looked a little bit like this:

using(var client = new HttpClient())
{
//do something with http client
}

Here’s the Rub

The using statement is a C# nicity for dealing with disposable objects. Once the using block is complete then the disposable object, in this case HttpClient, goes out of scope and is disposed. The dispose method is called and whatever resources are in use are cleaned up. This is a very typical pattern in .NET and we use it for everything from database connections to stream writers. Really any object which has external resources that must be clean up uses the IDisposable interface.

 

And you can’t be blamed for wanting to wrap it with the using. First of all, it’s considered good practice to do so. In fact, the official docs for using state:

As a rule, when you use an IDisposable object, you should declare and instantiate it in a using statement.

Secondly, all code you may have seen since…the inception of HttpClient would have told you to use a using statement block, including recent docs on the ASP.NET site itself. The internet is generally in agreement as well.

But HttpClient is different. Although it implements the IDisposable interface it is actually a shared object. This means that under the covers it is reentrant and thread safe. Instead of creating a new instance of HttpClient for each execution you should share a single instance of HttpClient for the entire lifetime of the application. Let’s look at why.

using System;
using System.Net.Http;

namespace ConsoleApplication
{
    public class Program
    {
        public static async Task Main(string[] args) 
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                using(var client = new HttpClient())
                {
                    var result = await client.GetAsync("http://aspnetmonsters.com");
                    Console.WriteLine(result.StatusCode);
                }
            }
            Console.WriteLine("Connections done");
        }
    }
}

  

C:\code\socket> dotnet run
Project socket (.NETCoreApp,Version=v1.0) will be compiled because inputs were modified
Compiling socket for .NETCoreApp,Version=v1.0

Compilation succeeded.
    0 Warning(s)
    0 Error(s)

Time elapsed 00:00:01.2501667


Starting connections
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
Connections done

  

But Wait, There’s More!

All work and everything is right with the world. Except that it isn’t. If we pull out the netstat tool and look at the state of sockets on the machine running this we’ll see:

C:\code\socket>NETSTAT.EXE
...
  Proto  Local Address          Foreign Address        State
  TCP    10.211.55.6:12050      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12051      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12053      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12054      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12055      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12056      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12057      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12058      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12059      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12060      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12061      waws-prod-bay-017:http  TIME_WAIT
  TCP    10.211.55.6:12062      waws-prod-bay-017:http  TIME_WAIT
  TCP    127.0.0.1:1695         SIMONTIMMS742B:1696    ESTABLISHED
...

  

Huh, that’s weird…the application has exited and yet there are still a bunch of these connections open to the Azure machine which hosts the ASP.NET Monsters website. They are in the TIME_WAIT state which means that the connection has been closed on one side (ours) but we’re still waiting to see if any additional packets come in on it because they might have been delayed on the network somewhere. Here is a diagram of TCP/IP states I stole from

TCP State Transition Diagram (fau.de)

 

 

Windows will hold a connection in this state for 240 seconds (It is set by [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay]). There is a limit to how quickly Windows can open new sockets so if you exhaust the connection pool then you’re likely to see error like:

Unable to connect to the remote server
System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.

Searching for that in the Googles will give you some terrible advice about decreasing the connection timeout. In fact, decreasing the timeout can lead to other detrimental consequences when applications that properly use HttpClient or similar constructs are run on the server. We need to understand what “properly” means and fix the underlying problem instead of tinkering with machine level variables.

 

The Fix is In

I really must thank Harald S. Ulrksen and Darrel Miller for pointing me to The Patterns and Practices documents on this.

performance-optimization/ImproperInstantiation at master · mspnp/performance-optimization (github.com)

 

If we share a single instance of HttpClient then we can reduce the waste of sockets by reusing them:

using System;
using System.Net.Http;

namespace ConsoleApplication
{
    public class Program
    {
        private static HttpClient Client = new HttpClient();
        public static async Task Main(string[] args) 
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                var result = await Client.GetAsync("http://aspnetmonsters.com");
                Console.WriteLine(result.StatusCode);
            }
            Console.WriteLine("Connections done");
            Console.ReadLine();
        }
    }
}

  

Note here that we have just one instance of HttpClient shared for the entire application. Eveything still works like it use to (actually a little faster due to socket reuse). Netstat now just shows:

TCP    10.211.55.6:12254      waws-prod-bay-017:http  ESTABLISHED

In the production scenario I had the number of sockets was averaging around 4000, and at peak would exceed 5000, effectively crushing the available resources on the server, which then caused services to fall over. After implementing the change, the sockets in use dropped from an average of more than 4000 to being consistently less than 400, and usually around 100.

 

This is dramatic. If you have any kind of load at all you need to remember these two things:

  1. Make your HttpClient static.(singleInstance)
  2. Do not dispose of or wrap your HttpClient in a using unless you explicitly are looking for a particular behaviour (such as causing your services to fail).

Wrapping Up

The socket exhaustion problems we had been struggling with for months disapeared and our client threw a virtual parade. I cannot understate how unobvious this bug was. For years we have been conditioned to dispose of objects that implement IDisposable and many refactoring tools like R# and CodeRush actually warn if you don’t. In this case disposing of HttpClient was the wrong thing to do. It is unfortunate that HttpClient implements IDisposable and encourages the wrong behaviour

 

 

 

 

Singleton HttpClient 问题:DNS 变动(负载均衡)

Byte Rot: Singleton HttpClient? Beware of this serious behaviour and how to fix it

posted @ 2022-07-11 15:20  PanPan003  阅读(837)  评论(0编辑  收藏  举报