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

Bru*_*nha 152 c# asp.net web-services asp.net-web-api dotnet-httpclient

HttpClientWebAPI客户端的生命周期应该是多少?为多个调用
设置一个实例是否更好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}\t{2}", product.Name, product.Price, product.Category);
    }
}
Run Code Online (Sandbox Code Playgroud)

Dar*_*ler 201

HttpClient已被设计为可重复用于多个呼叫.甚至跨多个线程.该HttpClientHandler有凭据和意在调用被重新使用的饼干.拥有一个新HttpClient实例需要重新设置所有这些东西.此外,该DefaultRequestHeaders属性包含用于多个调用的属性.必须在每个请求上重置这些值都会失败.

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

您使用的功能HttpClient越多,您就会越多地看到重用现有实例是有意义的.

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

从我的测试来看,在局域网上使用普通的HTTP,性能损失可以忽略不计.我怀疑这是因为有一个底层的TCP keepalive,即使在HttpClientHandler尝试关闭它时也会保持连接打开.

在通过互联网提出的请求中,我看到了一个不同的故事.由于每次都必须重新打开请求,我看到了40%的性能损失.

我怀疑HTTPS连接上的打击会更糟.

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

  • `然后强制关闭由ServicePointManager管理的连接池中的TCP/IP连接.你对这个语句有多确定?这很难相信.`HttpClient`在我看来就像一个应该经常实例化的工作单元. (5认同)
  • @vkelman是的,即使您使用新的HttpClientHandler创建它,您仍然可以重用HttpClient的实例.另请注意,HttpClient有一个特殊的构造函数,允许您重用HttpClientHandler并在不中断连接的情况下处置HttpClient. (2认同)
  • @vkelman我更喜欢保持HttpClient,但如果你喜欢保持HttpClientHandler,它会在第二个参数为false时保持连接打开. (2认同)
  • @DarrelMiller所以听起来连接绑定到HttpClientHandler.我知道要扩展我不想破坏连接所以我需要保持一个HttpClientHandler并从中创建我的所有HttpClient实例或创建一个静态HttpClient实例.但是,如果CookieContainer绑定到HttpClientHandler,并且我的cookie需要根据请求而有所不同,那么您推荐什么?我想通过修改每个请求的CookieContainer来避免静态HttpClientHandler上的线程同步. (2认同)
  • @Sana.91 你可以。最好将其注册为服务集合中的单例并以这种方式访问​​它。 (2认同)

Dav*_*ack 64

如果您希望您的应用程序扩展,差异是巨大的!根据负载,您将看到非常不同的性能数字.正如Darrel Miller所提到的,HttpClient被设计为跨请求重用.这是由BCL团队的人员证实的.

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

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

特别注意这个标注:

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

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


Oha*_*der 9

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

为解决此限制越来越一种方法是包装HttpClient与复制所有的一类HttpClient,你需要的方法(GetAsync,PostAsync等),和代表他们一单HttpClient.然而,这非常繁琐(您还需要包装扩展方法),幸运的是还有另一种方法 - 继续创建新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) 
       } 
    };
}
Run Code Online (Sandbox Code Playgroud)

  • 更好的方法是保留一个HttpClient实例,然后创建自己的本地HttpRequestMessage实例,然后在HttpClient上使用.SendAsync()方法。这样,它仍然是线程安全的。每个HttpRequestMessage将具有其自己的Authentication / URL值。 (2认同)
  • SendAsync 让您可以更改 URL 和其他属性(例如标头),并且仍然是线程安全的。 (2认同)
  • 是的,处理程序是关键。只要在 HttpClient 实例之间共享就可以了。我误读了您之前的评论。 (2认同)

No *_*rns 5

与大容量网站相关,但与 HttpClient 不直接相关。我们在所有服务中都有下面的代码片段。

        // 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;
Run Code Online (Sandbox Code Playgroud)

来自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 以外的值时,并且在指定的时间过后,通过在该请求中将 KeepAlive 设置为 false 为请求提供服务后,将关闭活动的 ServicePoint 连接。设置此值会影响 ServicePoint 对象管理的所有连接。”

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