Асинк и ждать: они плохие?

Недавно мы разработали сайт, основанный на 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-ответ готов, для завершения обработки используется поток из пула. Это способ, которым вы можете использовать несколько потоков для запроса на тысячу серверов за секунду.

Я бы предположил, что вам следует нарисовать, как работает ваш сайт, что делает каждый поток и как он работает одновременно. Обратите внимание, что вам необходимо решить, важна ли пропускная способность или латентность для вас.