Асинк и ждать: они плохие?
Недавно мы разработали сайт, основанный на SOA, но этот сайт столкнулся с ужасными проблемами загрузки и производительности, когда он перешел под нагрузку. Я разместил вопрос, связанный с этой проблемой здесь:
Веб-сайт ASP.NET становится неактуальным при загрузке
Сайт создан на веб-сайте API (WEB API), который размещен в кластере 4- node и веб-сайте, который размещен в другом кластере 4 node и вызывает вызовы API. Оба они разработаны с использованием ASP.NET MVC 5, и все действия/методы основаны на методе асинхронного ожидания.
После запуска сайта под некоторыми инструментами мониторинга, такими как NewRelic, при исследовании нескольких файлов дампов и профилирования рабочего процесса выяснилось, что при очень небольшой нагрузке (например, 16 одновременных пользователей) у нас было около 900 потоков, которые использовали 100 % от процессора и заполнил очередь потоков IIS!
Несмотря на то, что нам удалось развернуть сайт в производственной среде, представив кучи кеширования и поправки к производительности, многие разработчики в нашей команде считают, что нам нужно удалить все асинхронные методы и скрывать как API, так и веб-сайт для обычного веб-API и Методы действий, которые просто возвращают результат действия.
Мне лично не нравится подход, потому что мое чувство кишки состоит в том, что мы не использовали методы асинхронизации должным образом, иначе это означает, что Microsoft внедрила функцию, которая в основном довольно разрушительна и непригодна для использования!
Знаете ли вы какую-либо ссылку, которая очищает ее, где и как можно использовать/асинхронные методы? Как мы должны использовать их, чтобы избежать таких драм? например Основываясь на том, что я читал в MSDN, я считаю, что уровень API должен быть асинхронным, но веб-сайт может быть обычным сайтом без ASP.NET MVC.
Update:
Вот асинхронный метод, который делает все связи с 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();
Ответы
Ответ 1
Есть ли что-то глупо с этим методом? Обратите внимание, что когда мы весь метод для не-асинхронных методов, мы получили более высокую производительность.
Я вижу, по крайней мере, две вещи идут не так:
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;
};
Таким образом, вы блокируете поток пула, на котором выполняется lambda. Это эффективно уничтожает преимущество асинхронного API ReadAsAsync
и снижает масштабируемость вашего веб-приложения, Остерегайтесь других мест, подобных этому в вашем коде.
Во-вторых, запрос ASP.NET обрабатывается потоком сервера со специальным контекстом синхронизации, установленным на нем, AspNetSynchronizationContext
. Когда вы используете await
для продолжения, обратный вызов продолжения будет отправлен в тот же контекст синхронизации, код, сгенерированный компилятором, позаботится об этом. OTOH, когда вы используете 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 });
Следующая статья Стивена Туба очень важна:
"Async Performance: Понимание затрат Async и Await" .
Если мне нужно вызвать метод async в контексте синхронизации, где используется ожидание невозможно, что это лучший способ сделать это?
Вам почти никогда не нужно смешивать await
и ContinueWith
, вы должны придерживаться await
. В принципе, если вы используете async
, это должно быть async "all the way" .
Для среды выполнения ASP.NET MVC/Web API на стороне сервера это просто означает, что метод контроллера должен быть async
и возвращать Task
или Task<>
, проверить this. ASP.NET отслеживает ожидающие задачи для заданного HTTP-запроса. Запрос не будет завершен, пока все задачи не будут завершены.
Если вам действительно нужно вызвать метод async
из синхронного метода в ASP.NET, вы можете использовать AsyncManager
как this для регистрации ожидающей задачи. Для классического ASP.NET вы можете использовать PageAsyncTask
.
В худшем случае вы вызываете task.Wait()
и блокируете, потому что иначе ваша задача может продолжаться за пределами этого конкретного HTTP-запроса.
Для приложений пользовательского интерфейса на стороне клиента возможны разные сценарии для вызова метода async
из синхронного метода. Например, вы можете использовать ContinueWith(action, TaskScheduler.FromCurrentSynchronizationContext())
и запустить событие завершения из action
(например this).
Ответ 2
async и ожидание не должны создавать большое количество потоков, особенно не только с 16 пользователями. Фактически, это должно помочь вам лучше использовать потоки. Цель async и ждать в MVC - фактически отказаться от потока пула потоков, когда он занят обработкой IO связанных задач. Это говорит мне, что вы делаете что-то глупое, например, нерестилища, а затем ожидаете бесконечно.
Тем не менее, 900 потоков не очень много, и если они используют 100% -ный процессор, то они не ждут.. они что-то пережевывают. Это то, что вы должны изучать. Вы сказали, что используете такие инструменты, как NewRelic, и что они указывают на источник использования этого процессора? Какие методы?
Если бы я был вами, я бы сначала доказал, что просто использование async и ожидания не являются причиной ваших проблем. Просто создайте простой сайт, который имитирует поведение и затем запускает те же тесты на нем.
Во-вторых, возьмите копию своего приложения и начните извлечения материала, а затем выполните тесты против него. Посмотрите, можете ли вы отследить, где именно проблема.
Ответ 3
Существует много материала для обсуждения.
Прежде всего, async/await может помочь вам, когда у вашего приложения практически нет бизнес-логики. Я имею в виду, что точка async/await состоит в том, чтобы не иметь много потоков в спящем режиме, ожидая чего-то, в основном, некоторых IO, например. запросы базы данных (и выборки). Если ваше приложение использует огромную бизнес-логику с использованием процессора на 100%, async/wait не поможет вам.
Проблема 900 потоков заключается в том, что они неэффективны - если они работают одновременно. Дело в том, что лучше иметь такое количество "деловых" потоков, поскольку у вас на сервере есть ядра/процессоры. Причина - переключение контекста потока, блокировка и т.д. Существует множество систем, таких как шаблон LMAX distribuptor или Redis, которые обрабатывают данные в одном потоке (или одном потоке на ядро). Это просто лучше, поскольку вам не нужно обрабатывать блокировку.
Как достичь описанного подхода? Посмотрите на прерыватель, загрузите входящие запросы и обработайте их один за другим, а не параллельно.
Противоположный подход, когда практически нет бизнес-логики, и многие потоки просто ждут ввода-вывода, это хорошее место, где нужно поставить async/await в работу.
Как он работает в основном: есть поток, который считывает байты из сети - в основном только один. После поступления некоторого запроса этот поток считывает данные. Существует также ограниченный пул потоков работников, который обрабатывает запросы. Точка async заключается в том, что как только один поток обработки ожидает чего-то, в основном io, db, поток возвращается в опросе и может использоваться для другого запроса. Когда IO-ответ готов, для завершения обработки используется поток из пула. Это способ, которым вы можете использовать несколько потоков для запроса на тысячу серверов за секунду.
Я бы предположил, что вам следует нарисовать, как работает ваш сайт, что делает каждый поток и как он работает одновременно. Обратите внимание, что вам необходимо решить, важна ли пропускная способность или латентность для вас.