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

Что должно быть 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);
    }
}

6 ответов

Решение

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

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

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

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

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

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

Я подозреваю, что удар по HTTPS связь была бы еще хуже.

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

Если вы хотите, чтобы ваше приложение масштабировалось, разница огромна! В зависимости от нагрузки вы увидите очень разные показатели производительности. Как отмечает Даррел Миллер, 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, ...)

Как говорится в других ответах, 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) 
       } 
    };
}

Связанный с большими объемами веб-сайтов, но не напрямую с 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, и по истечении указанного времени активное соединение ServicePoint закрывается после обслуживания запроса, установив для KeepAlive значение false в этом запросе. Установка этого значения влияет на все соединения, управляемые объектом ServicePoint."

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

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

Во-первых, хотя этот класс одноразовый, его можно использовать с 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"]);
});

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

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

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

Другие вопросы по тегам