简体   繁体   English

在 WebAPI 客户端中为每次调用创建一个新的 HttpClient 的开销是多少?

[英]What is the overhead of creating a new HttpClient per call in a WebAPI client?

What should be the HttpClient lifetime of a WebAPI client? WebAPI 客户端的HttpClient生命周期应该是多少?
Is it better to have one instance of the HttpClient for multiple calls?为多个调用使用一个HttpClient实例是否更好?

What's the overhead of creating and disposing a HttpClient per request, like in example below (taken from http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from-a-net-client ):每个请求创建和处理HttpClient的开销是多少,如下例所示(取自http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from -a-net-client ):

using (var client = new HttpClient())
{
    client.BaseAddress = new Uri("http://localhost:9000/");
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    // New code:
    HttpResponseMessage response = await client.GetAsync("api/products/1");
    if (response.IsSuccessStatusCode)
    {
        Product product = await response.Content.ReadAsAsync<Product>();
        Console.WriteLine("{0}\t${1}\t{2}", product.Name, product.Price, product.Category);
    }
}

HttpClient has been designed to be re-used for multiple calls . HttpClient设计为可重复用于多次调用 Even across multiple threads.甚至跨多个线程。 The HttpClientHandler has Credentials and Cookies that are intended to be re-used across calls. HttpClientHandler具有旨在跨调用重复使用的凭据和 Cookie。 Having a new HttpClient instance requires re-setting up all of that stuff.拥有一个新的HttpClient实例需要重新设置所有这些东西。 Also, the DefaultRequestHeaders property contains properties that are intended for multiple calls.此外, DefaultRequestHeaders属性包含用于多次调用的属性。 Having to reset those values on each request defeats the point.必须在每个请求上重置这些值会失败。

Another major benefit of HttpClient is the ability to add HttpMessageHandlers into the request/response pipeline to apply cross cutting concerns. HttpClient另一个主要好处是能够将HttpMessageHandlers添加到请求/响应管道中以应用横切关注点。 These could be for logging, auditing, throttling, redirect handling, offline handling, capturing metrics.这些可能用于日志记录、审计、限制、重定向处理、离线处理、捕获指标。 All sorts of different things.各种不同的东西。 If a new HttpClient is created on each request, then all of these message handlers need to be setup on each request and somehow any application level state that is shared between requests for these handlers also needs to be provided.如果在每个请求上创建一个新的 HttpClient,那么所有这些消息处理程序都需要在每个请求上设置,并且以某种方式在这些处理程序的请求之间共享的任何应用程序级别状态也需要提供。

The more you use the features of HttpClient , the more you will see that reusing an existing instance makes sense.您使用HttpClient的功能越多,您就越会发现重用现有实例是有意义的。

However, the biggest issue, in my opinion is that when a HttpClient class is disposed, it disposes HttpClientHandler , which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManager .但是,在我看来,最大的问题是当HttpClient类被释放时,它会释放HttpClientHandler ,然后它会强制关闭ServicePointManager管理的连接池中的TCP/IP连接。 This means that each request with a new HttpClient requires re-establishing a new TCP/IP connection.这意味着每个带有新HttpClient请求都需要重新建立新的TCP/IP连接。

From my tests, using plain HTTP on a LAN, the performance hit is fairly negligible.从我的测试来看,在 LAN 上使用纯 HTTP,性能影响可以忽略不计。 I suspect this is because there is an underlying TCP keepalive that is holding the connection open even when HttpClientHandler tries to close it.我怀疑这是因为即使HttpClientHandler尝试关闭连接,也有一个底层 TCP keepalive 保持连接打开。

On requests that go over the internet, I have seen a different story.对于通过互联网发送的请求,我看到了一个不同的故事。 I have seen a 40% performance hit due to having to re-open the request every time.由于每次都必须重新打开请求,我看到了 40% 的性能下降。

I suspect the hit on a HTTPS connection would be even worse.我怀疑对HTTPS连接的攻击会更糟。

My advice is to keep an instance of HttpClient for the lifetime of your application for each distinct API that you connect to.我的建议是在应用程序的整个生命周期内为您连接到的每个不同的 API保留一个 HttpClient 实例

If you want your application to scale, the difference is HUGE!如果您希望您的应用程序可扩展,那么差异是巨大的! Depending on the load, you will see very different performance numbers.根据负载的不同,您会看到非常不同的性能数据。 As Darrel Miller mentions, the HttpClient was designed to be re-used across requests.正如 Darrel Miller 所提到的,HttpClient 旨在跨请求重复使用。 This was confirmed by folks on the BCL team who wrote it.这一点得到了 BCL 团队中编写它的人的证实。

A recent project I had was to help a very large and well-known online computer retailer scale out for Black Friday/holiday traffic for some new systems.我最近的一个项目是帮助一家非常大的知名在线计算机零售商扩展某些新系统的黑色星期五/假日流量。 We ran into some performance issues around the usage of HttpClient.我们在使用 HttpClient 时遇到了一些性能问题。 Since it implements IDisposable , the devs did what you would normally do by creating an instance and placing it inside of a using() statement.由于它实现了IDisposable ,因此开发人员通过创建一个实例并将其放置在using()语句中来完成您通常会做的事情。 Once we started load testing the app brought the server to its knees - yes, the server not just the app.一旦我们开始负载测试,应用程序就会让服务器屈服 - 是的,服务器不仅仅是应用程序。 The reason is that every instance of HttpClient opens a port on the server.原因是 HttpClient 的每个实例都会在服务器上打开一个端口。 Because of non-deterministic finalization of GC and the fact that you are working with computer resources that span across multiple OSI layers , closing network ports can take a while.由于 GC 的不确定性最终确定以及您正在使用跨越多个OSI 层的计算机资源这一事实,关闭网络端口可能需要一段时间。 In fact Windows OS itself can take up to 20 secs to close a port (per Microsoft).事实上,Windows 操作系统本身可能需要长达 20 秒才能关闭端口(根据 Microsoft)。 We were opening ports faster than they could be closed - server port exhaustion which hammered the CPU to 100%.我们打开端口的速度比关闭它们的速度要快——服务器端口耗尽使 CPU 达到 100%。 My fix was to change the HttpClient to a static instance which solved the problem.我的解决方法是将 HttpClient 更改为解决问题的静态实例。 Yes, it is a disposable resource, but any overhead is vastly outweighed by the difference in performance.是的,它是一种一次性资源,但性能差异大大超过了任何开销。 I encourage you to do some load testing to see how your app behaves.我鼓励您进行一些负载测试,以查看您的应用程序的行为。

You can also check out the WebAPI Guidance page for documentation and example at https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client您还可以在https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client查看 WebAPI 指南页面以获取文档和示例

Pay special attention to this call-out:请特别注意此调用:

HttpClient is intended to be instantiated once and re-used throughout the life of an application. HttpClient 旨在实例化一次并在应用程序的整个生命周期中重复使用。 Especially in server applications, creating a new HttpClient instance for every request will exhaust the number of sockets available under heavy loads.特别是在服务器应用程序中,为每个请求创建一个新的 HttpClient 实例会耗尽重负载下可用的套接字数量。 This will result in SocketException errors.这将导致 SocketException 错误。

If you find that you need to use a static HttpClient with different headers, base address, etc. what you will need to do is to create the HttpRequestMessage manually and set those values on the HttpRequestMessage .如果您发现需要使用具有不同标头、基地址等的静态HttpClient ,您需要做的是手动创建HttpRequestMessage并在HttpRequestMessage上设置这些值。 Then, use the HttpClient:SendAsync(HttpRequestMessage requestMessage, ...)然后,使用HttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

UPDATE for .NET Core : You should use the IHttpClientFactory via Dependency Injection to create HttpClient instances. .NET Core 更新:您应该通过依赖注入使用IHttpClientFactory来创建HttpClient实例。 It will manage the lifetime for you and you do not need to explicitly dispose it.它将为您管理生命周期,您无需明确处置它。 See Make HTTP requests using IHttpClientFactory in ASP.NET Core请参阅在 ASP.NET Core 中使用 IHttpClientFactory 发出 HTTP 请求

As the other answers state, HttpClient is meant for reuse.正如其他答案所述, HttpClient旨在重用。 However, reusing a single HttpClient instance across a multi-threaded application means you can't change the values of its stateful properties, like BaseAddress and DefaultRequestHeaders (so you can only use them if they are constant across your application).但是,在多线程应用程序中重用单个HttpClient实例意味着您无法更改其有状态属性的值,例如BaseAddressDefaultRequestHeaders (因此,只有当它们在您的应用程序中保持不变时,您才能使用它们)。

One approach for getting around this limitation is wrapping HttpClient with a class that duplicates all the HttpClient methods you need ( GetAsync , PostAsync etc) and delegates them to a singleton HttpClient .解决此限制的一种方法是使用一个类包装HttpClient ,该类复制您需要的所有HttpClient方法( GetAsyncPostAsync等)并将它们委托给单独的HttpClient However that's pretty tedious (you will need to wrap the extension methods too), and fortunately there is another way - keep creating new HttpClient instances, but reuse the underlying HttpClientHandler .然而,这非常乏味(您也需要包装扩展方法),幸运的是还有另一种方法- 继续创建新的HttpClient实例,但重用底层HttpClientHandler Just make sure you don't dispose the handler:只要确保你不处置处理程序:

HttpClientHandler _sharedHandler = new HttpClientHandler(); //never dispose this
HttpClient GetClient(string token)
{
    //client code can dispose these HttpClient instances
    return new HttpClient(_sharedHandler, disposeHandler: false)         
    {
       DefaultRequestHeaders = 
       {
            Authorization = new AuthenticationHeaderValue("Bearer", token) 
       } 
    };
}

Related to high-volume web sites but not directly to HttpClient.与大容量网站相关,但与 HttpClient 不直接相关。 We have the snippet of code below in all of our services.我们在所有服务中都有下面的代码片段。

        // number of milliseconds after which an active System.Net.ServicePoint connection is closed.
        const int DefaultConnectionLeaseTimeout = 60000;

        ServicePoint sp =
                ServicePointManager.FindServicePoint(new Uri("http://<yourServiceUrlHere>"));
        sp.ConnectionLeaseTimeout = DefaultConnectionLeaseTimeout;

From https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Net.ServicePoint.ConnectionLeaseTimeout);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.5.2);k(DevLang-csharp)&rd=true来自https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Net.ServicePoint.ConnectionLeaseTimeout);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.5.2); k(DevLang-csharp)&rd=true

"You can use this property to ensure that a ServicePoint object's active connections do not remain open indefinitely. This property is intended for scenarios where connections should be dropped and reestablished periodically, such as load balancing scenarios. “您可以使用此属性来确保 ServicePoint 对象的活动连接不会无限期地保持打开状态。此属性适用于应定期删除和重新建立连接的场景,例如负载平衡场景。

By default, when KeepAlive is true for a request, the MaxIdleTime property sets the time-out for closing ServicePoint connections due to inactivity.默认情况下,当请求的 KeepAlive 为 true 时,MaxIdleTime 属性设置因不活动而关闭 ServicePoint 连接的超时时间。 If the ServicePoint has active connections, MaxIdleTime has no effect and the connections remain open indefinitely.如果 ServicePoint 具有活动连接,则 MaxIdleTime 无效并且连接无限期地保持打开状态。

When the ConnectionLeaseTimeout property is set to a value other than -1, and after the specified time elapses, an active ServicePoint connection is closed after servicing a request by setting KeepAlive to false in that request.当 ConnectionLeaseTimeout 属性设置为 -1 以外的值时,并且在指定的时间过后,通过在该请求中将 KeepAlive 设置为 false 为请求提供服务后,将关闭活动的 ServicePoint 连接。 Setting this value affects all connections managed by the ServicePoint object."设置此值会影响 ServicePoint 对象管理的所有连接。”

When you have services behind a CDN or other endpoint that you want to failover then this setting helps callers follow you to your new destination.当您在 CDN 或其他端点后面有要进行故障转移的服务时,此设置可帮助呼叫者跟随您到达新目的地。 In this example 60 seconds after a failover all callers should re-connect to the new endpoint.在此示例中,故障转移后 60 秒,所有调用方都应重新连接到新端点。 It does require that you know your dependent services (those services that YOU call) and their endpoints.它确实要求您了解您的依赖服务(您调用的那些服务)及其端点。

You may also want to refer to this blog post by Simon Timms: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/您可能还想参考 Simon Timms 的这篇博文: https : //aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

But HttpClient is different.但是HttpClient不同。 Although it implements the IDisposable interface it is actually a shared object.尽管它实现了IDisposable接口,但它实际上是一个共享对象。 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.而不是创建一个新的实例HttpClient每次执行,你应该共享的单个实例HttpClient为应用程序的整个生命周期。 Let's look at why.让我们来看看为什么。

One thing to point out, that none of the "don't use using" blogs note is that it is not just the BaseAddress and DefaultHeader that you need to consider.需要指出的一件事是,“不要使用”的博客说明中没有一个是您需要考虑的不仅仅是 BaseAddress 和 DefaultHeader。 Once you make HttpClient static, there are internal states that will be carried across requests.一旦您将 HttpClient 设为静态,就会在请求中携带内部状态。 An example: You are authenticating to a 3rd party with HttpClient to get a FedAuth token (ignore why not using OAuth/OWIN/etc), that Response message has a Set-Cookie header for FedAuth, this is added to your HttpClient state.一个例子:您正在使用 HttpClient 向第 3 方进行身份验证以获取 FedAuth 令牌(忽略为什么不使用 OAuth/OWIN/等),该响应消息具有用于 FedAuth 的 Set-Cookie 标头,这将添加到您的 HttpClient 状态。 The next user to login to your API will be sending the last person's FedAuth cookie unless you are managing these cookies on each request.下一个登录到您的 API 的用户将发送最后一个人的 FedAuth cookie,除非您在每个请求上管理这些 cookie。

As a first issue, while this class is disposable, using it with the using statement is not the best choice because even when you dispose HttpClient object, the underlying socket is not immediately released and can cause a serious issue named 'sockets exhaustion.作为第一个问题,虽然这个类是一次性的,但将它与using语句一起using并不是最好的选择,因为即使您处理HttpClient对象,底层套接字也不会立即释放,并可能导致名为“套接字耗尽”的严重问题。

But there's a second issue with HttpClient that you can have when you use it as singleton or static object.但是,当您将HttpClient用作单例或静态对象时,您可能会遇到第二个问题。 In this case, a singleton or static HttpClient doesn't respect DNS changes.在这种情况下,单例或静态HttpClient不考虑DNS更改。

in .net core you can do the same with HttpClientFactory something like this:.net core 中,你可以用HttpClientFactory做同样的事情:

public interface IBuyService
{
    Task<Buy> GetBuyItems();
}
public class BuyService: IBuyService
{
    private readonly HttpClient _httpClient;

    public BuyService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Buy> GetBuyItems()
    {
        var uri = "Uri";

        var responseString = await _httpClient.GetStringAsync(uri);

        var buy = JsonConvert.DeserializeObject<Buy>(responseString);
        return buy;
    }
}

ConfigureServices配置服务

services.AddHttpClient<IBuyService, BuyService>(client =>
{
     client.BaseAddress = new Uri(Configuration["BaseUrl"]);
});

documentation and example at here文档和示例在这里

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM