【问题标题】:HttpClient with multiple proxies while handling socket exhaustion and DNS recycling在处理套接字耗尽和 DNS 回收时具有多个代理的 HttpClient
【发布时间】:2020-08-01 09:24:08
【问题描述】:

我们正在和朋友一起做一个有趣的项目,我们必须执行数百个 HTTP 请求,所有这些请求都使用不同的代理。想象一下它是这样的:

for (int i = 0; i < 20; i++)
{
    HttpClientHandler handler = new HttpClientHandler { Proxy = new WebProxy(randomProxy, true) };

    using (var client = new HttpClient(handler))
    {
        using (var request = new HttpRequestMessage(HttpMethod.Get, "http://x.com"))
        {
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
            }
        }

        using (var request2 = new HttpRequestMessage(HttpMethod.Get, "http://x.com/news"))
        {
            var response = await client.SendAsync(request2);

            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
            }
        }
    }
}

顺便说一下,我们使用的是 .NET Core(目前是控制台应用程序)。我知道有很多关于套接字耗尽和处理 DNS 回收的线程,但是这个特定的线程是不同的,因为使用了多个代理。

如果我们使用 HttpClient 的单例实例,就像大家建议的那样:

  • 我们不能设置多个代理,因为它是在 HttpClient 实例化期间设置的,之后无法更改。
  • 它不考虑 DNS 更改。重用 HttpClient 的实例意味着它会保留套接字直到它关闭,因此如果服务器上发生 DNS 记录更新,客户端将永远不会知道,直到该套接字关闭。一种解决方法是将keep-alive 标头设置为false,以便在每次请求后关闭套接字。它导致次优性能。第二种方法是使用ServicePoint
ServicePointManager.FindServicePoint("http://x.com")  
    .ConnectionLeaseTimeout = Convert.ToInt32(TimeSpan.FromSeconds(15).TotalMilliseconds);

ServicePointManager.DnsRefreshTimeout = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds);

另一方面,处理 HttpClient(就像我上面的示例一样),即 HttpClient 的多个实例,会导致多个套接字处于 TIME_WAIT 状态。 TIME_WAIT 表示本地端点(这边)已经关闭了连接。

我知道SocketsHttpHandlerIHttpClientFactory,但它们无法解决不同的代理问题。

var socketsHandler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(10),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
    MaxConnectionsPerServer = 10
};

// Cannot set a different proxy for each request
var client = new HttpClient(socketsHandler);

可以做出的最明智的决定是什么?

【问题讨论】:

  • HttpClientFactory 修复了所有的 dns 和 socket 耗尽问题
  • '但他们无法解决不同的代理问题。' - 这是什么意思
  • 为每个代理创建一个单独的 httpclient 实例并重用它?
  • 注意: ServicePointManager 不会影响 .NET Core 中的 HttpClient,因为它打算与 HttpWebRequest 一起使用,而 .NET Core 中的 HttpClint 不使用它。 NET 核心。是的,每个代理的HttpClient 实例看起来是合理的解决方案。 IHttpClientFactory 将同时修复socket和dns问题。
  • @aepot,哦,那是真的。我忘了在 HttpResponseMessage 上添加使用。我经常会收到不成功的代码。我会添加HttpCompletionOption.ResponseHeadersRead

标签: c# .net-core httpclient time-wait


【解决方案1】:

重用HttpClient 实例(或者更具体地说,重用最后一个HttpMessageHandler)的重点是重用套接字连接。不同的代理意味着不同的套接字连接,因此尝试在不同代理上重用HttpClient/HttpMessageHandler 是没有意义的,因为它必须是不同的连接。

我们必须执行数百个 HTTP 请求,都使用不同的代理

如果每个请求都是真正的唯一代理,并且没有在任何其他请求之间共享代理,那么您最好保留单独的 HttpClient 实例并使用 TIME_WAIT

但是,如果多个请求可能通过同一个代理,并且您想重用这些连接,那么这当然是可能的。

我建议使用IHttpClientFactory。它允许您定义可以合并和重用的命名HttpClient 实例(同样,技术上是最后一个HttpMessageHandler 实例)。只需为每个代理制作一个:

var proxies = new Dictionary<string, IWebProxy>(); // TODO: populate with proxies.
foreach (var proxy in proxies)
{
  services.AddHttpClient(proxy.Key)
      .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { Proxy = proxy.Value });
}

ConfigurePrimaryHttpMessageHandler 控制IHttpClientFactory 如何创建池化的主要HttpMessageHandler 实例。我从您问题中的代码中复制了HttpClientHandler,但大多数现代应用程序使用SocketsHttpHandler,它也有Proxy/UseProxy 属性。

然后,当你想使用一个时,调用IHttpClientFactory.CreateClient 并传递你想要的HttpClient 的名称:

for (int i = 0; i < 20; i++)
{
  var client = _httpClientFactory.CreateClient(randomProxyName);
  ...
}

【讨论】:

  • @Electron:虽然这可行,但不寻常; DI 用于一次性设置。该代码不允许跨多个按钮单击共享连接。但是对于像您这样的完全动态的情况,这是一种解决方案。另一种方法是创建自己的IHttpClientFactory 实现(从DefaultHttpClientFactory 的复制/粘贴开始),它可以在运行时添加新的命名客户端。
  • 感谢您的回答,无论如何!我会看看我能想到什么。 HttpClient 的设计不是为此而设计的,IHttpClientFactory 也不是为运行时配置而设计的。 github.com/dotnet/runtime/issues/35992.
  • @Electron: LGTM
  • @ggeorge: HttpClient 在 .NET Framework 上使用旧堆栈。在(现代版本的).NET Core 上,它使用完全托管的堆栈,不再有这种限制。
  • @ggeorge:是的,代理本身肯定会限制你。您可以使用 Fiddler 进行验证。最好提出一个新问题。
【解决方案2】:

首先,我想提一下,如果代理在编译时已知,@Stephen Cleary 的示例可以正常工作,但在我的情况下,它们在运行时已知。我忘了在问题中提到这一点,所以这是我的错。

感谢 @aepot 指出这些内容。

这就是我想出的解决方案(学分@mcont):

/// <summary>
/// A wrapper class for <see cref="FlurlClient"/>, which solves socket exhaustion and DNS recycling.
/// </summary>
public class FlurlClientManager
{
    /// <summary>
    /// Static collection, which stores the clients that are going to be reused.
    /// </summary>
    private static readonly ConcurrentDictionary<string, IFlurlClient> _clients = new ConcurrentDictionary<string, IFlurlClient>();

    /// <summary>
    /// Gets the available clients.
    /// </summary>
    /// <returns></returns>
    public ConcurrentDictionary<string, IFlurlClient> GetClients()
        => _clients;

    /// <summary>
    /// Creates a new client or gets an existing one.
    /// </summary>
    /// <param name="clientName">The client name.</param>
    /// <param name="proxy">The proxy URL.</param>
    /// <returns>The <see cref="FlurlClient"/>.</returns>
    public IFlurlClient CreateOrGetClient(string clientName, string proxy = null)
    {
        return _clients.AddOrUpdate(clientName, CreateClient(proxy), (_, client) =>
        {
            return client.IsDisposed ? CreateClient(proxy) : client;
        });
    }

    /// <summary>
    /// Disposes a client. This leaves a socket in TIME_WAIT state for 240 seconds but it's necessary in case a client has to be removed from the list.
    /// </summary>
    /// <param name="clientName">The client name.</param>
    /// <returns>Returns true if the operation is successful.</returns>
    public bool DeleteClient(string clientName)
    {
        var client = _clients[clientName];
        client.Dispose();
        return _clients.TryRemove(clientName, out _);
    }

    private IFlurlClient CreateClient(string proxy = null)
    {
        var handler = new SocketsHttpHandler()
        {
            Proxy = proxy != null ? new WebProxy(proxy, true) : null,
            PooledConnectionLifetime = TimeSpan.FromMinutes(10)
        };

        var client = new HttpClient(handler);

        return new FlurlClient(client);
    }
}

每个请求一个代理意味着每个请求都有一个额外的套接字(另一个 HttpClient 实例)。

在上面的解决方案中,ConcurrentDictionary 用于存储 HttpClient,所以我可以重复使用它们,这就是 HttpClient 的确切点。在它被 API 限制阻止之前,我可以对 5 个请求使用相同的代理。我也忘了在问题中提到这一点。

如您所见,解决套接字耗尽和 DNS 回收问题的解决方案有两种:IHttpClientFactorySocketsHttpHandler。第一个不适合我的情况,因为我使用的代理在运行时是已知的,而不是在编译时。上面的解决方案使用了第二种方式。

对于那些有同样问题的人,你可以阅读 GitHub 上的following issue。它解释了一切。

我对改进持开放态度,所以戳我。

【讨论】:

    【解决方案3】:

    将我的 cmets 收集到答案中。但这些是改进建议,而不是解决方案,因为您的问题与上下文密切相关:有多少代理,每分钟有多少请求,每个请求的平均时间是多少,等等。

    声明:我不熟悉IHttpClientFactory,但我知道,这是解决 Socket 耗尽和 DNS 问题的唯一方法。

    注意:ServicePointManager 不会影响 .NET Core 中的 HttpClient,因为它旨在与 .NET Core 中的 HttpClient 不使用的 HttpWebRequest 一起使用。

    正如@GuruStron 所建议的,每个代理的HttpClient 实例看起来是合理的解决方案。

    HttpResponseMessageIDisposable。应用 using 声明。它会影响套接字的使用行为。

    您可以将HttpCompletionOption.ResponseHeadersRead 申请到SendAsync 以在发送请求时不阅读整个响应。如果服务器返回不成功的状态码,您可能无法阅读响应。

    为了提高内部性能,您还可以在SendAsync()ReadAsStringAsync() 行附加.ConfigureAwait(false)。如果当前的 SynchronizationContext 不是 null(例如,它不是控制台应用程序),这将非常有用。

    这里有一些经过优化的代码(C# 8.0):

    private static async Task<string> GetHttpResponseAsync(HttpClient client, string url)
    {
        using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        if (response.IsSuccessStatusCode)
        {
            return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        }
        return null;
    }
    

    将池化的HttpClient 和 URL 传递给方法。

    【讨论】:

    • 感谢您指出这些内容。关于上下文依赖:你可以想象一千个代理,这意味着一千个 HttpClient 实例,每个 HttpClient 实例都有不同的代理设置,每个实例将执行两个HttpRequestMessages。就像上面的例子一样,不多也不少
    • @Electron 在IHttpClientFactory 的情况下,您可能无法让所有客户端保持活动状态,但类似于缓存 100 个最后使用的客户端。或任何其他情况。您还可以设置连接以在收到响应后立即关闭套接字,并小心地为每个请求使用HttpClient。只有您自己决定如何准确实施,这里没有灵丹妙药和稳定的最佳实践来解决这个问题,因为目标太复杂了。
    • @Electron 将执行两个 HttpRequestMessages 所示方法的代码将是相同的。干燥 - 不要重复自己。您可以轻松地管理方法之外的客户端。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-29
    • 1970-01-01
    相关资源
    最近更新 更多