HttpClient с несколькими прокси-серверами при обработке истощения сокетов и перезапуска DNS - PullRequest
1 голос
/ 01 августа 2020

Мы работаем над забавным проектом с другом, и нам нужно выполнить сотни 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 указывает, что локальная конечная точка (эта сторона) закрыла соединение.

Я знаю SocketsHttpHandler и IHttpClientFactory, но они не могут решить разные прокси.

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

Какое наиболее разумное решение можно принять?

Ответы [ 3 ]

3 голосов
/ 01 августа 2020

Смысл повторного использования экземпляров HttpClient (или, более конкретно, повторного использования последнего HttpMessageHandler) заключается в повторном использовании соединений сокетов. Разные прокси-серверы означают разные подключения к сокетам, поэтому нет смысла пытаться повторно использовать HttpClient / HttpMessageHandler на другом прокси, потому что это должно быть другое подключение.

мы должны выполнить сотни HTTP-запросов, все с использованием разных прокси.

Если каждый запрос действительно является уникальным прокси, и никакие прокси не используются совместно с другими запросами, тогда вы можете также просто сохраните отдельные экземпляры HttpClient и живите с TIME_WAIT.

Однако, если несколько запросов могут go через тот же прокси, и вы хотите повторно используйте эти соединения, тогда это, безусловно, возможно.

Я бы рекомендовал использовать 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);
  ...
}
1 голос
/ 03 августа 2020

Прежде всего, я хочу упомянуть, что пример @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 используется для хранения HttpClients, поэтому я могу повторно использовать их, что является точной точкой HttpClient. Я мог бы использовать один и тот же прокси для 5 запросов, прежде чем он будет заблокирован ограничениями API. Я забыл упомянуть и об этом в вопросе.

Как вы видели, есть два решения, решающих проблему нехватки сокетов и повторного использования DNS: IHttpClientFactory и SocketsHttpHandler. Первый не подходит для моего случая, потому что прокси, которые я использую, известны во время выполнения, а не во время компиляции. В приведенном выше решении используется второй способ.

Для тех, у кого такая же проблема, вы можете прочитать следующую проблему на GitHub. Это все объясняет.

Я открыт для улучшений, так что ткните меня.

1 голос
/ 01 августа 2020

Собрал мои комментарии в ответ. Но это предложения по улучшению, а не решение, потому что ваш вопрос сильно зависит от контекста: сколько прокси, сколько запросов в минуту, каково среднее время каждого запроса и т. Д. c.

Disclamer: I Я не знаком с IHttpClientFactory, но, черт возьми, это единственный способ решить проблему нехватки сокетов и DNS.

Примечание: ServicePointManager не влияет на HttpClient in. NET Core, потому что он предназначен для использования с HttpWebRequest, который не используется HttpClient in. NET Core.

По предложению @GuruStron, HttpClient экземпляр для каждого прокси выглядит разумным решением .

HttpResponseMessage равно IDisposable. Подать заявку на использование для этого. Это повлияет на использование сокетов.

Вы можете применить HttpCompletionOption.ResponseHeadersRead к SendAsync, чтобы не читать весь ответ при отправке запроса. Тогда вы не сможете прочитать ответ, если сервер вернул неуспешный код состояния.

Для повышения внутренней производительности вы также можете добавить .ConfigureAwait(false) в строки SendAsync() и ReadAsStringAsync(). В основном это полезно, если текущий 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 метода.

...