Недавно я создал простое приложение для тестирования пропускной способности HTTP-вызовов, которое можно сгенерировать асинхронно по сравнению с классическим многопоточным подходом.
Приложение может выполнять заранее определенное количество HTTP-вызовов и в конце отображает общее время, необходимое для их выполнения. Во время моих тестов все HTTP-вызовы были сделаны на мой локальный сервер IIS, и они получили небольшой текстовый файл (размером 12 байт).
Наиболее важная часть кода для асинхронной реализации приведена ниже:
public async void TestAsync()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
ProcessUrlAsync(httpClient);
}
}
private async void ProcessUrlAsync(HttpClient httpClient)
{
HttpResponseMessage httpResponse = null;
try
{
Task<HttpResponseMessage> getTask = httpClient.GetAsync(URL);
httpResponse = await getTask;
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
finally
{
if(httpResponse != null) httpResponse.Dispose();
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
}
Наиболее важная часть реализации многопоточности приведена ниже:
public void TestParallel2()
{
this.TestInit();
ServicePointManager.DefaultConnectionLimit = 100;
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
Task.Run(() =>
{
try
{
this.PerformWebRequestGet();
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
});
}
}
private void PerformWebRequestGet()
{
HttpWebRequest request = null;
HttpWebResponse response = null;
try
{
request = (HttpWebRequest)WebRequest.Create(URL);
request.Method = "GET";
request.KeepAlive = true;
response = (HttpWebResponse)request.GetResponse();
}
finally
{
if (response != null) response.Close();
}
}
Проведение тестов показало, что многопоточная версия работает быстрее. Для выполнения 10 тыс. Запросов потребовалось около 0,6 секунды, в то время как асинхронный запрос занял около 2 секунд для того же объема нагрузки. Это было немного неожиданно, потому что я ожидал, что асинхронный будет быстрее. Возможно, это было из-за того, что мои HTTP-вызовы были очень быстрыми. В реальном сценарии, когда сервер должен выполнять более значимую операцию и где также должна быть некоторая задержка в сети, результаты могут быть обратными.
Однако меня больше всего беспокоит поведение HttpClient при увеличении нагрузки. Поскольку для доставки 10 тыс. Сообщений требуется около 2 секунд, я думал, что для доставки 10-кратного количества сообщений потребуется около 20 секунд, но запуск теста показал, что для доставки 100 тыс. Сообщений требуется около 50 секунд. Кроме того, доставка 200 тыс. Сообщений обычно занимает более 2 минут, и часто несколько тысяч из них (3-4 тыс.) Не работают за следующим исключением:
Невозможно выполнить операцию с сокетом, так как в системе недостаточно места в буфере или очередь заполнена.
Я проверил журналы IIS, и операции, которые не удались, никогда не доходили до сервера. Они потерпели неудачу внутри клиента. Я провел тесты на машине с Windows 7 с диапазоном временных портов по умолчанию от 49152 до 65535. Запуск netstat показал, что во время тестов использовалось около 5-6k портов, поэтому теоретически должно было быть доступно намного больше. Если отсутствие портов действительно было причиной исключений, это означает, что либо netstat не сообщил должным образом о ситуации, либо HttClient использует только максимальное количество портов, после чего он начинает генерировать исключения.
Напротив, многопоточный подход к генерации HTTP-вызовов вел себя очень предсказуемо. Я взял около 0,6 секунды для 10 тыс. Сообщений, около 5,5 секунды для 100 тыс. Сообщений и, как ожидалось, около 55 секунд для 1 миллиона сообщений. Ни одно из сообщений не прошло успешно. Более того, во время работы он никогда не использовал более 55 МБ ОЗУ (согласно диспетчеру задач Windows). Память, используемая при асинхронной отправке сообщений, росла пропорционально нагрузке. Во время тестирования 200 тыс. Сообщений он использовал около 500 МБ ОЗУ.
Я думаю, что есть две основные причины таких результатов. Во-первых, HttpClient кажется очень жадным в создании новых соединений с сервером. Большое количество используемых портов, о которых сообщает netstat, означает, что он, вероятно, не сильно выиграет от HTTP keep-alive.
Во-вторых, у HttpClient, похоже, нет механизма регулирования. На самом деле это общая проблема, связанная с асинхронными операциями. Если вам нужно выполнить очень большое количество операций, все они будут запущены одновременно, а затем их продолжения будут выполняться по мере их доступности. Теоретически это должно быть нормально, потому что при асинхронных операциях нагрузка ложится на внешние системы, но, как показано выше, это не совсем так. Одновременный запуск большого количества запросов приведет к увеличению использования памяти и замедлению всего выполнения.
Мне удалось получить лучшие результаты с точки зрения памяти и времени выполнения, ограничив максимальное количество асинхронных запросов с помощью простого, но примитивного механизма задержки:
public async void TestAsyncWithDelay()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
if (_activeRequestsCount >= MAX_CONCURENT_REQUESTS)
await Task.Delay(DELAY_TIME);
ProcessUrlAsyncWithReqCount(httpClient);
}
}
Было бы действительно полезно, если бы HttpClient включал механизм ограничения количества одновременных запросов. При использовании класса Task (который основан на пуле потоков .Net) регулирование автоматически достигается за счет ограничения количества параллельных потоков.
Для полного обзора я также создал версию асинхронного теста на основе HttpWebRequest, а не HttpClient, и мне удалось получить гораздо лучшие результаты. Для начала, он позволяет установить ограничение на количество одновременных подключений (с помощью ServicePointManager.DefaultConnectionLimit или через config), что означает, что он никогда не исчерпывал порты и никогда не терпел сбоев ни при одном запросе (HttpClient по умолчанию основан на HttpWebRequest , но похоже, что он игнорирует настройку лимита подключения).
Асинхронный подход HttpWebRequest все еще был примерно на 50-60% медленнее, чем многопоточный, но он был предсказуемым и надежным. Единственным недостатком было то, что он использовал огромный объем памяти при большой нагрузке. Например, для отправки 1 миллиона запросов требовалось около 1,6 ГБ. Ограничив количество одновременных запросов (как я сделал выше для HttpClient), мне удалось уменьшить используемую память до 20 МБ и получить время выполнения всего на 10% медленнее, чем при многопоточном подходе.
После этой пространной презентации у меня возникают следующие вопросы: является ли класс HttpClient из .Net 4.5 плохим выбором для приложений с интенсивной нагрузкой? Есть ли способ уменьшить его, чтобы решить проблемы, о которых я упоминал? Как насчет асинхронного вкуса HttpWebRequest?
Обновление (спасибо @Stephen Cleary)
Как оказалось, HttpClient, как и HttpWebRequest (на котором он основан по умолчанию), может иметь количество одновременных подключений на одном и том же хосте, ограниченное ServicePointManager.DefaultConnectionLimit. Странно то, что, согласно MSDN , значение по умолчанию для ограничения подключения равно 2. Я также проверил это на своей стороне с помощью отладчика, который указал, что действительно 2 является значением по умолчанию. Однако кажется, что, если явно не задать значение ServicePointManager.DefaultConnectionLimit, значение по умолчанию будет проигнорировано. Поскольку я явно не устанавливал для него значение во время тестов HttpClient, я думал, что его проигнорировали.
После установки ServicePointManager.DefaultConnectionLimit на 100 HttpClient стал надежным и предсказуемым (netstat подтверждает, что используются только 100 портов). Он по-прежнему медленнее, чем асинхронный HttpWebRequest (примерно на 40%), но, как ни странно, использует меньше памяти. Для теста, который включает 1 миллион запросов, он использовал максимум 550 МБ по сравнению с 1,6 ГБ в асинхронном HttpWebRequest.
Таким образом, хотя HttpClient в сочетании с ServicePointManager.DefaultConnectionLimit, похоже, обеспечивает надежность (по крайней мере, для сценария, когда все вызовы выполняются к одному и тому же хосту), похоже, что на его производительность отрицательно влияет отсутствие надлежащего механизма регулирования. Что-то, что ограничивало бы количество одновременных запросов до настраиваемого значения, а остальные помещало бы в очередь, сделало бы его более подходящим для сценариев с высокой масштабируемостью.
источник
HttpClient
следует уважатьServicePointManager.DefaultConnectionLimit
.SemaphoreSlim
, как уже упоминалось, илиActionBlock<T>
из TPL Dataflow.Ответы:
Помимо тестов, упомянутых в вопросе, я недавно создал несколько новых, включающих гораздо меньше HTTP-вызовов (5000 по сравнению с 1 миллионом ранее), но для запросов, выполнение которых занимало гораздо больше времени (500 миллисекунд по сравнению с примерно 1 миллисекундой ранее). Оба приложения-тестера, синхронно многопоточное (на основе HttpWebRequest) и приложение с асинхронным вводом-выводом (на основе HTTP-клиента), дали одинаковые результаты: около 10 секунд на выполнение с использованием около 3% ЦП и 30 МБ памяти. Единственная разница между двумя тестерами заключалась в том, что в многопоточном использовалось 310 потоков для выполнения, а в асинхронном - всего 22.
В заключение моих тестов, асинхронные HTTP-вызовы - не лучший вариант при работе с очень быстрыми запросами. Причина в том, что при выполнении задачи, содержащей вызов асинхронного ввода-вывода, поток, в котором запускается задача, завершается, как только выполняется асинхронный вызов, а остальная часть задачи регистрируется как обратный вызов. Затем, когда операция ввода-вывода завершается, обратный вызов ставится в очередь для выполнения в первом доступном потоке. Все это создает накладные расходы, что делает быстрые операции ввода-вывода более эффективными при выполнении в потоке, который их запустил.
Асинхронные HTTP-вызовы - хороший вариант при работе с длинными или потенциально долгими операциями ввода-вывода, поскольку он не заставляет какие-либо потоки загружаться в ожидании завершения операций ввода-вывода. Это уменьшает общее количество потоков, используемых приложением, позволяя тратить больше времени ЦП на операции, связанные с ЦП. Кроме того, в приложениях, которые выделяют только ограниченное количество потоков (например, в случае с веб-приложениями), асинхронный ввод-вывод предотвращает истощение пула потоков потоков, что может произойти при синхронном выполнении вызовов ввода-вывода.
Таким образом, асинхронный HttpClient не является узким местом для приложений с интенсивной нагрузкой. Просто по своей природе он не очень хорошо подходит для очень быстрых HTTP-запросов, вместо этого он идеально подходит для длинных или потенциально длинных, особенно внутри приложений, которые имеют только ограниченное количество доступных потоков. Кроме того, рекомендуется ограничивать параллелизм с помощью ServicePointManager.DefaultConnectionLimit значением, достаточно высоким, чтобы обеспечить хороший уровень параллелизма, но достаточно низким, чтобы предотвратить временное истощение портов. Вы можете найти более подробную информацию о тестах и выводах, представленных по этому вопросу, здесь .
источник
Одна вещь, которую следует учитывать, которая может повлиять на ваши результаты, заключается в том, что с HttpWebRequest вы не получаете ResponseStream и не используете этот поток. С HttpClient по умолчанию он копирует сетевой поток в поток памяти. Чтобы использовать HttpClient так же, как в настоящее время вы используете HttpWebRquest, вам необходимо сделать
Другое дело, что я не совсем уверен, в чем реальная разница с точки зрения потоковой передачи, которую вы на самом деле тестируете. Если вы углубитесь в глубины HttpClientHandler, он просто выполняет Task.Factory.StartNew для выполнения асинхронного запроса. Поведение потоковой передачи делегируется контексту синхронизации точно так же, как и в вашем примере с HttpWebRequest.
Несомненно, HttpClient добавляет некоторые накладные расходы, поскольку по умолчанию он использует HttpWebRequest в качестве своей транспортной библиотеки. Таким образом, вы всегда сможете улучшить производительность с помощью HttpWebRequest напрямую, используя HttpClientHandler. Преимущества, которые дает HttpClient, заключаются в использовании стандартных классов, таких как HttpResponseMessage, HttpRequestMessage, HttpContent и всех строго типизированных заголовков. Само по себе это не оптимизация производительности.
источник
HttpClient
кажется простым в использовании, и я подумал, что асинхронный - это лучший вариант, но, похоже, вокруг этого есть много «но и если». Может,HttpClient
стоит переписать так, чтобы им было удобнее пользоваться? Или что документация действительно подчеркивала важные моменты о том, как ее использовать наиболее эффективно?Хотя это не дает прямого ответа на «асинхронную» часть вопроса OP, это устраняет ошибку в реализации, которую он использует.
Если вы хотите, чтобы ваше приложение масштабировалось, избегайте использования HttpClients на основе экземпляров. Разница ОГРОМНАЯ! В зависимости от нагрузки вы увидите очень разные показатели производительности. HttpClient был разработан для повторного использования в запросах. Это подтвердили парни из команды BCL, написавшие это.
Недавний проект, который у меня был, заключался в том, чтобы помочь очень крупному и хорошо известному онлайн-продавцу компьютеров увеличить объем трафика в Черную пятницу / праздник для некоторых новых систем. Мы столкнулись с некоторыми проблемами производительности при использовании HttpClient. Поскольку он реализуется
IDisposable
, разработчики сделали то, что обычно делали бы вы, создав экземпляр и поместив его вusing()
оператор. Как только мы начали нагрузочное тестирование, приложение поставило сервер на колени - да, сервер, а не только приложение. Причина в том, что каждый экземпляр HttpClient открывает порт завершения ввода-вывода на сервере. Из-за недетерминированного завершения сборки мусора и того факта, что вы работаете с компьютерными ресурсами, которые охватывают несколько уровней OSI. , закрытие сетевых портов может занять некоторое время. Фактически сама ОС Windowsзакрытие порта может занять до 20 секунд (по Microsoft). Мы открывали порты быстрее, чем они могли быть закрыты - исчерпание портов сервера, из-за чего ЦП загружался на 100%. Мое исправление состояло в том, чтобы изменить HttpClient на статический экземпляр, который решил проблему. Да, это одноразовый ресурс, но любые накладные расходы значительно перевешиваются разницей в производительности. Я рекомендую вам провести нагрузочное тестирование, чтобы увидеть, как ведет себя ваше приложение.Также ответил по ссылке ниже:
Каковы затраты на создание нового HttpClient для каждого вызова в клиенте WebAPI?
https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client
источник