async и ждут: они плохие?
Недавно мы разработали сайт на основе SOA, но у этого сайта возникли ужасные проблемы с нагрузкой и производительностью, когда он находился под нагрузкой. Я разместил вопрос, связанный с этой проблемой здесь:
Сайт ASP.NET перестает отвечать на запросы под нагрузкой
Сайт состоит из сайта API (WEB API), который размещен в кластере с 4 узлами, и веб-сайта, который размещен в другом кластере с 4 узлами и выполняет вызовы API. Оба разработаны с использованием ASP.NET MVC 5, и все действия / методы основаны на методе async-await.
После запуска сайта под некоторыми инструментами мониторинга, такими как NewRelic, исследования нескольких файлов дампа и профилирования рабочего процесса, оказалось, что при очень небольшой нагрузке (например, 16 одновременных пользователей) мы получили около 900 потоков, которые использовали 100% ЦП и заполнил очередь потока IIS!
Несмотря на то, что нам удалось развернуть сайт в производственной среде путем внесения множества изменений в кэшировании и производительности, многие разработчики в нашей команде считают, что нам необходимо удалить все асинхронные методы и преобразовать как API, так и веб-сайт в обычные методы Web API и Action, которые просто верните результат действия.
Лично я не доволен подходом, потому что мое внутреннее чувство состоит в том, что мы не использовали асинхронные методы должным образом, в противном случае это означает, что Microsoft представила функцию, которая в основном довольно разрушительна и непригодна для использования!
Знаете ли вы какие-либо ссылки, которые проясняют это, где и как асинхронные методы должны / могут использоваться? Как мы должны использовать их, чтобы избежать таких драм? Например, основываясь на том, что я прочитал в MSDN, я считаю, что уровень API должен быть асинхронным, но веб-сайт может быть обычным не асинхронным сайтом ASP.NET MVC.
Обновить:
Вот асинхронный метод, который делает все коммуникации с API.
public static async Task<T> GetApiResponse<T>(object parameters, string action, CancellationToken ctk)
{
using (var httpClient = new HttpClient())
{
httpClient.BaseAddress = new Uri(BaseApiAddress);
var formatter = new JsonMediaTypeFormatter();
return
await
httpClient.PostAsJsonAsync(action, parameters, ctk)
.ContinueWith(x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }).Result, ctk);
}
}
Есть ли что-нибудь глупое с этим методом? Обратите внимание, что когда мы преобразовали все методы в неасинхронные методы, мы получили намного лучшую производительность.
Вот пример использования (я вырезал другие биты кода, которые были связаны с валидацией, ведением журналов и т. Д. Этот код является телом метода действия MVC).
В нашей сервисной обертке:
public async static Task<IList<DownloadType>> GetSupportedContentTypes()
{
string userAgent = Request.UserAgent;
var parameters = new { Util.AppKey, Util.StoreId, QueryParameters = new { UserAgent = userAgent } };
var taskResponse = await Util.GetApiResponse<ApiResponse<SearchResponse<ProductItem>>>(
parameters,
"api/Content/ContentTypeSummary",
default(CancellationToken));
return task.Data.Groups.Select(x => x.DownloadType()).ToList();
}
И в действии:
public async Task<ActionResult> DownloadTypes()
{
IList<DownloadType> supportedTypes = await ContentService.GetSupportedContentTypes();
3 ответа
Есть ли что-нибудь глупое с этим методом? Обратите внимание, что когда мы преобразовали все методы в неасинхронные методы, мы получили намного лучшую производительность.
Я вижу, по крайней мере, две вещи идут не так, как надо:
public static async Task<T> GetApiResponse<T>(object parameters, string action, CancellationToken ctk)
{
using (var httpClient = new HttpClient())
{
httpClient.BaseAddress = new Uri(BaseApiAddress);
var formatter = new JsonMediaTypeFormatter();
return
await
httpClient.PostAsJsonAsync(action, parameters, ctk)
.ContinueWith(x => x.Result.Content
.ReadAsAsync<T>(new[] { formatter }).Result, ctk);
}
}
Во-первых, лямбда, к которой вы переходите ContinueWith
блокирует:
x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }).Result
Это эквивалентно:
x => {
var task = x.Result.Content.ReadAsAsync<T>(new[] { formatter });
task.Wait();
return task.Result;
};
Таким образом, вы блокируете поток пула, в котором происходит выполнение лямбды. Это эффективно убивает преимущество естественно асинхронного ReadAsAsync
API и снижает масштабируемость вашего веб-приложения. Остерегайтесь других мест, как это в вашем коде.
Во-вторых, запрос ASP.NET обрабатывается серверным потоком, на котором установлен специальный контекст синхронизации, AspNetSynchronizationContext
, Когда вы используете await
для продолжения обратный вызов продолжения будет опубликован в том же контексте синхронизации, и сгенерированный компилятором код позаботится об этом. Ото, когда вы используете ContinueWith
это не происходит автоматически.
Таким образом, вам нужно явно указать правильный планировщик задач, снять блокировку .Result
(это вернет задачу) и Unwrap
вложенная задача:
return
await
httpClient.PostAsJsonAsync(action, parameters, ctk).ContinueWith(
x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }),
ctk,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
Тем не менее, вам действительно не нужна такая сложность ContinueWith
Вот:
var x = await httpClient.PostAsJsonAsync(action, parameters, ctk);
return await x.Content.ReadAsAsync<T>(new[] { formatter });
Следующая статья Стивена Тауба очень актуальна:
"Производительность асинхронных вычислений: понимание стоимости асинхронных и ожидающих".
Если мне нужно вызвать асинхронный метод в контексте синхронизации, где использование await невозможно, каков наилучший способ сделать это?
Вы почти никогда не должны смешивать await
а также ContinueWith
, ты должен придерживаться await
, В основном, если вы используете async
это должно быть асинхронно "все время".
Для среды выполнения ASP.NET MVC / Web API на стороне сервера это просто означает, что метод контроллера должен быть async
и вернуть Task
или же Task<>
Проверьте это. ASP.NET отслеживает отложенные задачи для данного HTTP-запроса. Запрос не будет завершен, пока не будут выполнены все задачи.
Если вам действительно нужно позвонить async
метод из синхронного метода в ASP.NET, вы можете использовать AsyncManager
как это, чтобы зарегистрировать отложенную задачу. Для классического ASP.NET вы можете использовать PageAsyncTask
,
В худшем случае, вы бы позвонили task.Wait()
и блокировать, потому что в противном случае ваша задача может продолжаться за пределами этого конкретного HTTP-запроса.
Для клиентских приложений пользовательского интерфейса возможны различные сценарии вызова async
метод из синхронного метода. Например, вы можете использовать ContinueWith(action, TaskScheduler.FromCurrentSynchronizationContext())
и запустить событие завершения из action
(вот так).
Есть много вещей для обсуждения.
Прежде всего, async/await может помочь вам естественным образом, когда ваше приложение почти не имеет бизнес-логики. Я имею в виду, что смысл async / await состоит в том, чтобы в спящем режиме не было большого количества потоков, ожидающих чего-либо, в основном некоторого ввода-вывода, например запросов к базе данных (и выборки). Если ваше приложение выполняет огромную бизнес-логику, используя процессор на 100%, async/await вам не поможет.
Проблема 900 потоков в том, что они неэффективны - если они работают одновременно. Дело в том, что лучше иметь такое количество "бизнес" потоков, поскольку у вашего сервера есть ядра / процессоры. Причина в переключении контекста потока, конфликте блокировки и так далее. Существует много систем, таких как шаблон прерывателя LMAX или Redis, которые обрабатывают данные в одном потоке (или одном потоке на ядро). Это просто лучше, так как вам не нужно обрабатывать блокировку.
Как достичь описанного подхода? Посмотрите на прерыватель, поставьте в очередь входящие запросы и обрабатывайте их один за другим, а не параллельно.
Противоположный подход, когда почти нет бизнес-логики, а многие потоки просто ждут ввода-вывода, является хорошим местом, где можно использовать async / await в работе.
Как это в основном работает: есть поток, который читает байты из сети - в основном только один. Когда приходит какой-то запрос, этот поток читает данные. Существует также ограниченный пул потоков рабочих, который обрабатывает запросы. Суть асинхронности заключается в том, что как только один поток обработки ожидает чего-то, в основном io, db, этот поток возвращается в опросе и может использоваться для другого запроса. Как только ответ IO готов, некоторый поток из пула используется для завершения обработки. Это способ, которым вы можете использовать несколько потоков для сервера тысяч запросов в секунду.
Я бы посоветовал вам нарисовать некоторую картину того, как работает ваш сайт, что делает каждый поток и как он работает одновременно. Обратите внимание, что необходимо решить, важна ли для вас пропускная способность или задержка.
async и await не должны создавать большое количество потоков, особенно не только с 16 пользователями. На самом деле, это должно помочь вам лучше использовать потоки. Цель асинхронного и ожидающего в MVC состоит в том, чтобы фактически отказаться от потока пула потоков, когда он занят обработкой связанных с IO задач. Это говорит мне о том, что вы где-то делаете глупости, например, порождаете темы и ждете бесконечно долго.
Тем не менее, 900 потоков это не очень много, и если они используют 100% процессоров, то они не ждут... они что-то жуют. Это то, что вы должны изучить. Вы сказали, что использовали такие инструменты, как NewRelic, на что они указали в качестве источника использования этого процессора? Какие методы?
Если бы я был вами, я бы сначала доказал, что простое использование async и await не является причиной ваших проблем. Просто создайте простой сайт, который имитирует поведение, а затем запустите на нем те же тесты.
Во-вторых, возьмите копию своего приложения и начните извлекать материал, а затем запускать тесты против него. Посмотрите, сможете ли вы отследить, где именно проблема.