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


162

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

我不确定,您可以使用Stopwatch该类对其进行基准测试。我的估计是HttpClient,假设所有这些实例都在同一上下文中使用,则拥有一个就更有意义。
马修

Answers:


215

HttpClient已被设计用于多个呼叫。即使跨多个线程。该HttpClientHandler有打算重新使用在调用凭据和饼干。拥有新HttpClient实例需要重新设置所有这些东西。此外,该DefaultRequestHeaders属性包含用于多个调用的属性。必须在每个请求上重置这些值都无法解决问题。

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

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

但是,我认为最大的问题是,当HttpClient处置一个类时,会处置HttpClientHandler它,然后强行关闭TCP/IP由管理的连接池中的连接ServicePointManager。这意味着每个带有新请求的请求都HttpClient需要重新建立新TCP/IP连接。

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

根据互联网上的请求,我看到了一个不同的故事。由于每次必须重新打开请求,我发现性能下降了40%。

我怀疑HTTPS连接受到的打击会更糟。

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


5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManager您对此声明有多确定?很难相信。HttpClient在我看来,它就像应该经常实例化的工作单元。
usr

2
@vkelman是的,即使您使用新的HttpClientHandler创建了HttpClient实例,您仍然可以重用它。还要注意,有一个特殊的HttpClient构造函数,它使您可以重用HttpClientHandler并处置HttpClient而不终止连接。
Darrel Miller

2
@vkelman我更喜欢保留HttpClient,但是如果您喜欢保留HttpClientHandler,则当第二个参数为false时,它将保持连接打开。
Darrel Miller 2015年

2
@DarrelMiller因此,听起来连接已绑定到HttpClientHandler。我知道扩大规模并不想破坏连接,因此我需要保留一个HttpClientHandler并从中创建我的所有HttpClient实例,或者创建一个静态HttpClient实例。但是,如果CookieContainer绑定到HttpClientHandler,并且每个请求的cookie都不同,那么您有什么建议?我想通过为每个请求修改其CookieContainer来避免在静态HttpClientHandler上进行线程同步。
Dave Black

2
@ Sana.91您可以。最好将其注册为服务集合中的单例并以这种方式访问​​它。
Darrel Miller

69

如果您想扩展您的应用程序,那么差别就很大!根据负载,您将看到非常不同的性能数字。正如Darrel Miller所提到的,HttpClient被设计为可在请求之间重复使用。这是由BCL团队中撰写此文件的人确认的。

我最近的一个项目是帮助一家非常知名的大型在线计算机零售商扩展黑色星期五/假日期间的某些新系统的流量。我们在使用HttpClient时遇到了一些性能问题。既然实现了IDisposable,开发人员就可以通过创建实例并将其放在using()语句中来完成您通常要做的事情。一旦我们开始进行负载测试,该应用程序便使服务器屈服-是的,服务器不只是该应用程序。原因是每个HttpClient实例都会在服务器上打开一个端口。由于GC的确定性不确定,并且您正在使用跨多个OSI层的计算机资源,因此关闭网络端口可能需要一段时间。实际上Windows OS 本身最多可能需要20秒才能关闭端口(每个Microsoft)。我们打开端口的速度比关闭端口的速度快-服务器端口耗尽将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, ...)

.NET Core的UPDATE:您应该使用IHttpClientFactoryvia依赖注入来创建HttpClient实例。它会为您管理生命周期,您无需显式处置它。请参阅在ASP.NET Core中使用IHttpClientFactory发出HTTP请求


1
这篇文章为那些进行压力测试的人提供了有用的见解。
萨那。91年

9

正如其他答案所述,HttpClient是指可重复使用。但是,HttpClient在多线程应用程序中重用单个实例意味着您无法更改其有状态属性的值,例如BaseAddressDefaultRequestHeaders(因此,仅当它们在应用程序中是恒定的时才可以使用它们)。

用于获取解决此限制的一种方法是包装HttpClient与复制所有的一类HttpClient,你需要的方法(GetAsyncPostAsync等),和代表他们一单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) 
       } 
    };
}

2
更好的方法是保留一个HttpClient实例,然后创建自己的本地HttpRequestMessage实例,然后在HttpClient上使用.SendAsync()方法。这样,它仍然是线程安全的。每个HttpRequestMessage都有自己的Authentication / URL值。
Tim P.

@TimP。为什么更好呢?SendAsync是方便比专用方法如少得多PutAsyncPostAsJsonAsync等等
辖施耐德

2
SendAsync让我们更改URL和其他属性(例如标头),并且仍然是线程安全的。
Tim P.

2
是的,处理程序是关键。只要在HttpClient实例之间共享就可以了。我误读了您先前的评论。
戴夫·布莱克

1
如果我们保留一个共享的处理程序,是否还需要解决过时的DNS问题?
尚蒂'18

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;

来自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秒,所有呼叫者都应重新连接到新端点。它确实需要您了解您的依赖服务(您称为的那些服务)及其端点。


通过打开和关闭连接,您仍然在服务器上增加了很多负载。如果您将基于实例的HttpClient与基于实例的HttpClientHandlers结合使用,那么如果不小心,仍然会遇到端口耗尽的情况。
戴夫·布莱克

不反对。一切都是权衡。对于我们而言,重新路由CDN或DNS就是银行中的钱与收入的损失。
不退款

1

您可能还想参考Simon Timms撰写的此博客文章:https : //aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

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


1

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


0

首先,尽管此类是可抛弃的,但将其与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"]);
});

文档和示例在这里

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.