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

时间:2014-03-21 14:01:57

标签: c# asp.net web-services asp.net-web-api dotnet-httpclient

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}\t{2}", product.Name, product.Price, product.Category);
    }
}

7 个答案:

答案 0 :(得分:190)

HttpClient 旨在重复用于多次通话。甚至跨多个线程。 HttpClientHandler具有旨在跨呼叫重复使用的凭据和Cookie。拥有一个新的HttpClient实例需要重新设置所有这些内容。 此外,DefaultRequestHeaders属性包含用于多个调用的属性。必须在每个请求上重置这些值都会失败。

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

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

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

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

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

我怀疑HTTPS连接上的点击会更糟。

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

答案 1 :(得分:61)

如果您希望您的应用程序扩展,差异是巨大的!根据负载,您将看到非常不同的性能数字。正如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并在{上设置这些值{1}}。然后,使用HttpRequestMessage

答案 2 :(得分:8)

正如其他答案所述,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) 
       } 
    };
}

答案 3 :(得分:4)

与高容量网站相关但不直接与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秒,所有呼叫者都应重新连接到新端点。它确实需要您知道您的从属服务(您调用的服务)及其端点。

答案 4 :(得分:1)

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

  

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

答案 5 :(得分:0)

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

答案 6 :(得分:0)

作为第一个问题,尽管此类是可抛弃的,但将其与using语句一起使用不是最佳选择,因为即使处置HttpClient对象,底层套接字也不会立即释放,并且可以导致一个严重的问题,称为“套接字耗尽”。

但是HttpClient还有另一个问题,当您将其用作单例或静态对象时,可能会遇到该问题。在这种情况下,单例或静态HttpClient不会考虑DNS的更改。

.net核心中,您可以对 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"]);
});

文档和示例here