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

Posted

技术标签:

【中文标题】在 WebAPI 客户端中每次调用创建一个新的 HttpClient 的开销是多少?【英文标题】:What is the overhead of creating a new HttpClient per call in a WebAPI client? 【发布时间】:2014-03-21 14:01:57 【问题描述】:

WebAPI 客户端的HttpClient 生命周期应该是多少? 将HttpClient 的一个实例用于多个调用是否更好?

每个请求创建和处理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\t2", product.Name, product.Price, product.Category);
    

【问题讨论】:

我不确定,但是您可以使用 Stopwatch 类对其进行基准测试。我的估计是拥有一个HttpClient 更有意义,假设所有这些实例都在相同的上下文中使用。 相关:***.com/questions/11178220/…,***.com/questions/15705092/…。 【参考方案1】:

HttpClient设计为可重复用于多次调用。甚至跨多个线程。 HttpClientHandler 具有旨在跨调用重复使用的凭据和 Cookie。拥有一个新的HttpClient 实例需要重新设置所有这些东西。 此外,DefaultRequestHeaders 属性包含用于多次调用的属性。必须在每个请求上重置这些值是不正确的。

HttpClient 的另一个主要好处是能够将HttpMessageHandlers 添加到请求/响应管道中以应用横切关注点。这些可以用于日志记录、审计、节流、重定向处理、离线处理、捕获指标。各种不同的东西。如果在每个请求上创建一个新的 HttpClient,则需要在每个请求上设置所有这些消息处理程序,并且还需要提供在这些处理程序的请求之间共享的任何应用程序级状态。

您使用HttpClient 的功能越多,您就越会发现重用现有实例是有意义的。

但是,在我看来,最大的问题是当一个HttpClient 类被释放时,它会释放HttpClientHandler,然后强制关闭ServicePointManager 管理的连接池中的TCP/IP 连接.这意味着每个带有新HttpClient 的请求都需要重新建立一个新的TCP/IP 连接。

根据我的测试,在 LAN 上使用纯 HTTP,性能影响可以忽略不计。我怀疑这是因为即使HttpClientHandler 试图关闭它,也有一个底层 TCP keepalive 保持连接打开。

对于通过互联网发送的请求,我看到了不同的故事。由于每次都必须重新打开请求,我发现性能下降了 40%。

我怀疑HTTPS 连接上的命中会更糟。

我的建议是在应用程序的整个生命周期内为您连接的每个不同的 API 保留一个 HttpClient 实例

【讨论】:

which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManager 您对此声明的把握程度如何?这很难相信。 HttpClient 在我看来就像一个应该经常实例化的工作单元。 @vkelman 是的,即使您使用新的 HttpClientHandler 创建了 HttpClient 实例,您仍然可以重用它。另请注意,HttpClient 有一个特殊的构造函数,它允许您重用 HttpClientHandler 并在不终止连接的情况下处理 HttpClient。 @vkelman 我更喜欢保留 HttpClient,但如果您更喜欢保留 HttpClientHandler,当第二个参数为 false 时,它​​将保持连接打开。 @DarrelMiller 所以听起来连接绑定到 HttpClientHandler。我知道要进行扩展,我不想破坏连接,所以我需要保留一个 HttpClientHandler 并从中创建我的所有 HttpClient 实例,或者创建一个静态 HttpClient 实例。但是,如果 CookieContainer 绑定到 HttpClientHandler,并且我的 cookie 需要根据请求不同,您有什么建议?我想通过为每个请求修改其 CookieContainer 来避免静态 HttpClientHandler 上的线程同步。 @Sana.91 你可以。最好将它注册为服务集合中的单例并以这种方式访问​​它。【参考方案2】:

如果您希望您的应用程序可扩展,那么差异是巨大的!根据负载,您将看到非常不同的性能数字。正如 Darrel Miller 提到的,HttpClient 旨在跨请求重用。编写它的 BCL 团队的人证实了这一点。

我最近的一个项目是帮助一家非常大型的知名在线计算机零售商扩展一些新系统的黑色星期五/假日流量。我们在使用 HttpClient 时遇到了一些性能问题。由于它实现了IDisposable,因此开发人员通过创建一个实例并将其放置在using() 语句中来完成您通常会做的事情。一旦我们开始对应用程序进行负载测试,服务器就会崩溃——是的,服务器不仅仅是应用程序。原因是每个 HttpClient 实例都会在服务器上打开一个端口。由于 GC 的非确定性最终确定以及您正在使用跨越多个 OSI layers 的计算机资源这一事实,关闭网络端口可能需要一段时间。事实上,Windows 操作系统本身最多可能需要 20 秒才能关闭一个端口(根据 Microsoft)。我们打开端口的速度比关闭端口的速度快——服务器端口耗尽导致 CPU 达到 100%。我的解决方法是将 HttpClient 更改为解决问题的静态实例。是的,它是一种一次性资源,但性能差异远远超过了任何开销。我鼓励您进行一些负载测试,以了解您的应用的行为方式。

您还可以查看 WebAPI 指南页面以获取文档和示例,网址为 https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

特别注意这个标注:

HttpClient 旨在被实例化一次并在应用程序的整个生命周期中重复使用。特别是在服务器应用程序中,为每个请求创建一个新的 HttpClient 实例将耗尽重负载下可用的套接字数量。这将导致 SocketException 错误。

如果您发现需要使用具有不同标头、基地址等的静态HttpClient,您需要手动创建HttpRequestMessage 并在HttpRequestMessage 上设置这些值。然后,使用HttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

.NET Core 更新: 您应该通过依赖注入使用IHttpClientFactory 来创建HttpClient 实例。它将为您管理生命周期,您无需显式处置它。见Make HTTP requests using IHttpClientFactory in ASP.NET Core

【讨论】:

这篇文章包含对那些将要进行压力测试的人有用的见解......!【参考方案3】:

正如其他答案所述,HttpClient 旨在重复使用。但是,在多线程应用程序中重用单个 HttpClient 实例意味着您无法更改其有状态属性的值,例如 BaseAddressDefaultRequestHeaders(因此,如果它们在您的应用程序中保持不变,则只能使用它们)。

解决此限制的一种方法是将HttpClient 包装在一个类中,该类复制您需要的所有HttpClient 方法(GetAsyncPostAsync 等)并将它们委托给单例HttpClient。然而,这非常乏味(您还需要包装extension methods),幸运的是there is another way - 继续创建新的HttpClient 实例,但重用底层HttpClientHandler。只要确保你不处置处理程序:

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) 
        
    ;

【讨论】:

更好的方法是保留一个 HttpClient 实例,然后创建自己的本地 HttpRequestMessage 实例,然后在 HttpClient 上使用 .SendAsync() 方法。这样它仍然是线程安全的。每个 HttpRequestMessage 都有自己的 Authentication/URL 值。 @TimP。为什么更好? SendAsync 远不如PutAsyncPostAsJsonAsync 等专用方法方便。 SendAsync 让您更改 URL 和其他属性(例如标头)并且仍然是线程安全的。 是的,处理程序是关键。只要在 HttpClient 实例之间共享它就可以了。我误读了您之前的评论。 如果我们保留一个共享处理程序,我们还需要处理过时的 DNS 问题吗?【参考方案4】:

与大容量网站有关,但与 HttpClient 没有直接关系。我们在所有服务中都有以下代码的 sn-p。

        // 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;

来自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

“您可以使用此属性来确保 ServicePoint 对象的活动连接不会无限期保持打开状态。此属性适用于应定期删除和重新建立连接的场景,例如负载平衡场景。

默认情况下,当请求的 KeepAlive 为 true 时,MaxIdleTime 属性设置因不活动而关闭 ServicePoint 连接的超时时间。如果 ServicePoint 有活动连接,则 MaxIdleTime 无效,连接将无限期保持打开状态。

如果 ConnectionLeaseTimeout 属性设置为 -1 以外的值,并且经过指定的时间后,活动的 ServicePoint 连接会在服务请求后关闭,方法是在该请求中将 KeepAlive 设置为 false。 设置此值会影响 ServicePoint 对象管理的所有连接。"

当您想要故障转移的 CDN 或其他端点后面有服务时,此设置可帮助呼叫者跟随您到您的新目的地。在此示例中,故障转移后 60 秒,所有呼叫者应重新连接到新端点。它确实要求您了解您的依赖服务(您调用的那些服务)及其端点。

【讨论】:

打开和关闭连接仍然会给服务器带来很大的负载。如果使用基于实例的 HttpClients 和基于实例的 HttpClientHandlers,一不小心仍然会遇到端口耗尽的情况。 不反对。一切都是权衡。对我们来说,遵循重新路由的 CDN 或 DNS 就是银行存款与收入损失。【参考方案5】:

您可能还想参考 Simon Timms 的这篇博文:https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

HttpClient 不同。虽然它实现了IDisposable 接口,但它实际上是一个共享对象。这意味着在幕后它是可重入的)和线程安全的。您应该在应用程序的整个生命周期内共享一个 HttpClient 实例,而不是为每次执行创建一个新的 HttpClient 实例。让我们看看为什么。

【讨论】:

【参考方案6】:

需要指出的一点是,“不要使用 using”的博客说明都不是您需要考虑的不仅仅是 BaseAddress 和 DefaultHeader。一旦将 HttpClient 设为静态,就会有跨请求传递的内部状态。一个示例:您正在使用 HttpClient 向第 3 方进行身份验证以获取 FedAuth 令牌(忽略为什么不使用 OAuth/OWIN/etc),该响应消息具有用于 FedAuth 的 Set-Cookie 标头,这已添加到您的 HttpClient 状态。下一个登录您的 API 的用户将发送最后一个人的 FedAuth cookie,除非您在每个请求上都管理这些 cookie。

【讨论】:

【参考方案7】:

作为第一个问题,虽然此类是一次性的,但将其与 using 语句一起使用并不是最佳选择,因为即使您处置 HttpClient 对象,底层套接字也不会立即释放,并可能导致严重问题命名为'套接字耗尽。

但是当您将HttpClient 用作单例或静态对象时,您可能会遇到第二个问题。在这种情况下,单例或静态 HttpClient 不尊重 DNS 更改。

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

配置服务

services.AddHttpClient<IBuyService, BuyService>(client =>

     client.BaseAddress = new Uri(Configuration["BaseUrl"]);
);

here的文档和示例

【讨论】:

以上是关于在 WebAPI 客户端中每次调用创建一个新的 HttpClient 的开销是多少?的主要内容,如果未能解决你的问题,请参考以下文章

如何调用带有认证服务的webapi

每次为新的 API 调用创建新的繁琐连接?

HttpClient 使用自签名证书调用 HTTPS WebApi

调用 WebApi 时出错

webApi项目中的问题

.NET WebAPI 和 http 基本身份验证