Каковы затраты на создание нового HttpClient для каждого вызова в клиенте WebAPI?

162

Каким должен быть HttpClientсрок службы клиента WebAPI?
Лучше иметь один экземпляр 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, если бы все эти экземпляры использовались в одном и том же контексте.
Мэтью

Ответы:

215

HttpClientбыл разработан для многократного использования . Даже через несколько потоков. HttpClientHandlerИмеет полномочия и Куки, которые предназначены , чтобы быть повторно использованы между вызовами. Наличие нового HttpClientэкземпляра требует переустановки всего этого. Также DefaultRequestHeadersсвойство содержит свойства, которые предназначены для нескольких вызовов. Необходимость сбрасывать эти значения в каждом запросе побеждает точку.

Еще одним важным преимуществом HttpClientявляется возможность добавления HttpMessageHandlersв конвейер запросов / ответов для применения сквозных задач. Это могут быть записи, аудит, регулирование, обработка перенаправления, автономная обработка, получение метрик. Все виды разных вещей. Если новый HttpClient создается при каждом запросе, то все эти обработчики сообщений должны быть настроены для каждого запроса, и каким-либо образом должно быть предоставлено любое состояние уровня приложения, которое совместно используется запросами для этих обработчиков.

Чем больше вы используете функции HttpClient, тем больше вы увидите, что повторное использование существующего экземпляра имеет смысл.

Однако самая большая проблема, на мой взгляд, заключается в том, что когда HttpClientкласс удаляется, он удаляется HttpClientHandler, что затем принудительно закрывает TCP/IPсоединение в пуле соединений, которым управляет ServicePointManager. Это означает, что каждый запрос с новым HttpClientтребует восстановления нового TCP/IPсоединения.

Из моих тестов, использующих простой HTTP в локальной сети, снижение производительности довольно незначительно. Я подозреваю, что это потому, что есть основной протокол активности TCP, который удерживает соединение открытым, даже когда HttpClientHandlerпытается его закрыть.

По запросам, которые идут через интернет, я видел другую историю. Я видел снижение производительности на 40% из-за необходимости каждый раз заново открывать запрос.

Я подозреваю, что попадание в HTTPSсоединение будет еще хуже.

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

Даррел Миллер
источник
5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManagerНасколько вы уверены в этом утверждении? В это трудно поверить. HttpClientвыглядит для меня как единица работы, которая должна часто создаваться.
USR
2
@vkelman Да, вы все еще можете повторно использовать экземпляр HttpClient, даже если вы создали его с новым HttpClientHandler. Также обратите внимание, что есть специальный конструктор для HttpClient, который позволяет вам повторно использовать HttpClientHandler и утилизировать HttpClient, не разрушая соединение.
Даррел Миллер
2
@vkelman Я предпочитаю держать HttpClient рядом, но если вы предпочитаете держать HttpClientHandler, он будет держать соединение открытым, когда второй параметр имеет значение false.
Даррел Миллер
2
@DarrelMiller Похоже, что соединение связано с HttpClientHandler. Я знаю, что для масштабирования я не хочу разрушать соединение, поэтому мне нужно либо сохранить HttpClientHandler и создать все мои экземпляры HttpClient из этого ИЛИ создать статический экземпляр HttpClient. Однако, если CookieContainer привязан к HttpClientHandler, и мои cookie-файлы должны различаться для каждого запроса, что вы порекомендуете? Я хотел бы избежать синхронизации потоков в статическом HttpClientHandler, изменяя его CookieContainer для каждого запроса.
Дейв Блэк,
2
@ Sana.91 Ты можешь. Было бы лучше зарегистрировать его как одноэлементное в коллекции сервисов и получить к нему доступ таким образом.
Даррел Миллер
69

Если вы хотите, чтобы ваше приложение масштабировалось, разница огромна! В зависимости от нагрузки вы увидите очень разные показатели производительности. Как упоминает Даррел Миллер, HttpClient был разработан для повторного использования в запросах. Это подтвердили ребята из команды BCL, которые написали это.

Недавний проект, который у меня был, состоял в том, чтобы помочь очень крупному и известному интернет-продавцу компьютеров в масштабировании для Черной пятницы / праздничного трафика для некоторых новых систем. Мы столкнулись с некоторыми проблемами производительности, связанными с использованием HttpClient. Поскольку он реализуется IDisposable, разработчики сделали то, что вы обычно делаете, создав экземпляр и поместив его внутри using()оператора. Как только мы начали нагрузочное тестирование, приложение поставило сервер на колени - да, сервер не только приложение. Причина в том, что каждый экземпляр HttpClient открывает порт на сервере. Из-за недетерминированного завершения GC и того факта, что вы работаете с компьютерными ресурсами, которые охватывают несколько уровней OSI , закрытие сетевых портов может занять некоторое время. На самом деле сама ОС WindowsМожет потребоваться до 20 секунд, чтобы закрыть порт (для Microsoft). Мы открывали порты быстрее, чем они могли быть закрыты - истощение портов сервера, которое загружало процессор до 100%. Мое исправление состояло в том, чтобы изменить HttpClient на статический экземпляр, который решил проблему. Да, это одноразовый ресурс, но любые накладные расходы значительно перевешиваются разницей в производительности. Я рекомендую вам провести нагрузочное тестирование, чтобы увидеть, как ваше приложение ведет себя.

Вы также можете проверить страницу руководства WebAPI с документацией и примером на https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

Обратите особое внимание на этот призыв:

HttpClient предназначен для создания экземпляра один раз и повторного использования в течение всего жизненного цикла приложения. Особенно в серверных приложениях, создание нового экземпляра HttpClient для каждого запроса приведет к исчерпанию количества сокетов, доступных при больших нагрузках. Это приведет к ошибкам SocketException.

Если вы обнаружите, что вам нужно использовать статический элемент HttpClientс разными заголовками, базовым адресом и т. Д., Вам нужно будет создать HttpRequestMessageвручную и установить эти значения в HttpRequestMessage. Затем используйтеHttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

ОБНОВЛЕНИЕ для .NET Core . IHttpClientFactoryДля создания HttpClientэкземпляров следует использовать внедрение через зависимости . Он будет управлять временем жизни для вас, и вам не нужно явно распоряжаться им. См. Создание HTTP-запросов с использованием IHttpClientFactory в ASP.NET Core.

Дэйв Блэк
источник
1
Этот пост содержит полезную информацию для тех, кто будет проводить стресс-тестирование ..!
Sana.91
9

Как говорится в других ответах, HttpClientпредназначен для повторного использования. Однако повторное использование одного HttpClientэкземпляра в многопоточном приложении означает, что вы не можете изменять значения его свойств с состоянием, таких как BaseAddressи DefaultRequestHeaders(поэтому вы можете использовать их, только если они постоянны в вашем приложении).

Один из способов обойти это ограничение - обернуть 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) 
       } 
    };
}
Охад Шнайдер
источник
2
Лучший способ - сохранить один экземпляр HttpClient, а затем создать свои собственные локальные экземпляры HttpRequestMessage и затем использовать метод .SendAsync () в HttpClient. Таким образом, он все еще будет потокобезопасным. Каждое HttpRequestMessage будет иметь свои собственные значения аутентификации / URL.
Тим П.
@TimP. почему лучше? SendAsyncгораздо менее удобно, чем специальные методы, такие как PutAsyncи PostAsJsonAsyncт. д.
Охад Шнайдер
2
SendAsync позволяет вам изменять URL-адрес и другие свойства, такие как заголовки, и при этом поддерживать потокобезопасность.
Тим П.
2
Да, обработчик является ключом. Пока это разделяется между экземплярами HttpClient, у вас все в порядке. Я неправильно прочитал ваш предыдущий комментарий.
Дейв Блэк
1
Если мы сохраняем общий обработчик, нам все еще нужно заботиться об устаревшей проблеме DNS?
Шанти
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); к (DevLang-CSharp) & й = TRUE

«Это свойство можно использовать, чтобы активные соединения объекта ServicePoint не оставались открытыми бесконечно. Это свойство предназначено для сценариев, в которых следует периодически прерывать и восстанавливать подключения, например, для сценариев балансировки нагрузки.

По умолчанию, когда KeepAlive имеет значение true для запроса, свойство MaxIdleTime устанавливает время ожидания для закрытия соединений ServicePoint из-за неактивности. Если у ServicePoint есть активные соединения, MaxIdleTime не имеет никакого эффекта, и соединения остаются открытыми бесконечно.

Если для свойства ConnectionLeaseTimeout установлено значение, отличное от -1, и по истечении указанного времени активное соединение ServicePoint закрывается после обслуживания запроса, установив для KeepAlive значение false в этом запросе. Установка этого значения влияет на все соединения, управляемые объектом ServicePoint. "

Если у вас есть службы за CDN или другой конечной точкой, которые вы хотите переключать при сбое, тогда этот параметр помогает вызывающим абонентам следовать за вами к вашему новому месту назначения. В этом примере через 60 секунд после аварийного переключения все абоненты должны повторно подключиться к новой конечной точке. Это требует, чтобы вы знали ваши зависимые сервисы (те сервисы, которые вы вызываете) и их конечные точки.

Нет возврата Нет возврата
источник
Вы по-прежнему сильно загружаете сервер, открывая и закрывая соединения. Если вы используете основанные на экземплярах HttpClients с основанными на экземплярах HttpClientHandlers, вы все равно столкнетесь с истощением портов, если не будете осторожны.
Дейв Блэк
Не согласен. Все это компромисс. Для нас переадресация CDN или DNS - это деньги в банке против упущенной выгоды.
Нет возврата
1

Вы также можете обратиться к этому сообщению в блоге Саймона Тиммса: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

Но HttpClientотличается. Хотя он реализует IDisposableинтерфейс, на самом деле это общий объект. Это означает, что под крышками это повторно входящий) и потокобезопасный. Вместо того, чтобы создавать новый экземпляр HttpClientдля каждого выполнения, вы должны совместно использовать один экземпляр HttpClientдля всего времени жизни приложения. Давайте посмотрим на почему.

SvenAelterman
источник
1

Стоит отметить, что ни одно из примечаний блога «не использовать, используя», это то, что вам нужно учитывать не только BaseAddress и DefaultHeader. Как только вы сделаете HttpClient статическим, существуют внутренние состояния, которые будут передаваться через запросы. Пример: вы проходите проверку подлинности для третьей стороны с помощью HttpClient для получения токена FedAuth (игнорируйте, почему не используете OAuth / OWIN / и т. Д.), В этом ответном сообщении есть заголовок Set-Cookie для FedAuth, он добавляется в ваше состояние HttpClient. Следующий пользователь, который войдет в ваш API, будет отправлять последний файл cookie FedAuth, если вы не управляете этими файлами cookie при каждом запросе.

escapismc
источник
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"]);
});

документация и пример здесь

Реза Дженаби
источник