Яка витрата на створення нового 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 навколо, він зберігатиме з'єднання відкритим, коли другий параметр хибний.
Даррел Міллер

2
@DarrelMiller Отже, це здається, що з'єднання пов'язане з HttpClientHandler. Я знаю, що в масштабі я не хочу руйнувати з'єднання, тому мені потрібно або тримати HttpClientHandler навколо і створювати всі мої екземпляри HttpClient з цього АБО створювати статичний екземпляр HttpClient. Однак якщо CookieContainer прив’язаний до HttpClientHandler, і мої файли cookie повинні відрізнятися залежно від запиту, що ви рекомендуєте? Я хотів би уникнути синхронізації потоків на статичному HttpClientHandler, змінивши його CookieContainer для кожного запиту.
Дейв Блек

2
@ Sana.91 Ти міг. Краще було б зареєструвати його як сингл в колекції послуг та отримати доступ до нього таким чином.
Даррел Міллер

69

Якщо ви хочете, щоб ваша програма масштабувалась, різниця ВЕЛИЧЕЗНА! Залежно від навантаження, ви побачите дуже різні показники продуктивності. Як згадує Даррел Міллер, HttpClient був розроблений для повторного використання в запитах. Це підтвердили хлопці з команди BCL, які це написали.

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

Ви також можете ознайомитись із сторінкою "Веб-веб-інструкції" щодо документації та прикладу на 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
Цей пост містить корисну інформацію для тих, хто буде робити стрес-тестування ..!
Сана.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т.д.
Ohad Шнайдер

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; k (DevLang-csharp) & rd = вірно

"Ви можете використовувати це властивість для того, щоб активні з'єднання об'єкта ServicePoint не залишалися відкритими нескінченно. Це властивість призначене для сценаріїв, коли з'єднання слід періодично відкидати та відновлювати, наприклад, сценарії балансування навантаження.

За замовчуванням, коли KeepAlive відповідає запиту, властивість MaxIdleTime встановлює тайм-аут для закриття з'єднань ServicePoint через неактивність. Якщо ServicePoint має активні з'єднання, MaxIdleTime не має ефекту, і з'єднання залишаються відкритими на невизначений термін.

Коли для властивості ConnectionLeaseTimeout встановлено значення, відмінне від -1, і після закінчення зазначеного часу, активне з'єднання ServicePoint закривається після обслуговування запиту, встановивши KeepAlive на помилковий у цьому запиті. Встановлення цього значення впливає на всі з'єднання, якими керує об’єкт ServicePoint. "

Якщо у вас є послуги, що знаходяться за CDN або іншою кінцевою точкою, яку ви хочете відмовити, тоді цей параметр допомагає абонентам переслідувати вас до вашого нового пункту призначення. У цьому прикладі через 60 секунд після виходу з ладу всі абоненти повинні знову підключитися до нової кінцевої точки. Це вимагає, щоб ви знали свої залежні служби (ті служби, які ви телефонуєте) та їх кінцеві точки.


Ви все ще покладаєте велике навантаження на сервер, відкриваючи та закриваючи з'єднання. Якщо ви використовуєте HttpClients на основі екземпляра з HttpClientHandlers на основі екземпляра, ви все одно зіткнетесь з виснаженням портів, якщо не будете обережні.
Дейв Блек

Не погоджуючись. Все є компромісом. Для нас після перенаправленого CDN або DNS - це гроші в банку проти втрачених доходів.
Без повернення грошей не повертається

1

Ви також можете посилатися на це повідомлення у блозі Саймона Тіммса: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

Але HttpClientрізна. Хоча він реалізує IDisposableінтерфейс, він насправді є спільним об'єктом. Це означає, що під ковдрами він є рентгеном) і безпечний для ниток. Замість того, щоб створювати новий екземпляр HttpClientдля кожного виконання, вам слід поділитися одним екземпляром HttpClientпротягом усього життя програми. Давайте розберемося, чому.


1

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


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

Налаштування сервісів

services.AddHttpClient<IBuyService, BuyService>(client =>
{
     client.BaseAddress = new Uri(Configuration["BaseUrl"]);
});

документація та приклад тут

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.