Всплески ЦП/время ожидания для приложения ASP.NET Core

Проблема в том, что процессор регулярно скачет от ~ 10% до более чем 70%:

К сожалению, это, по-видимому, влияет на среднее время отклика, вызывая некоторые всплески.

Это хороший сценарий, когда среднее значение остается ниже 1 с, но иногда оно может работать довольно плохо.

Я попытался исследовать эту проблему на портале Azure и заметил, что некоторые запросы остаются в этом блоке, что заставляет меня думать, что это была проблема с запросом (из того, что я вижу, это не совсем трассировка стека, здесь может быть более одного запроса происходит внутри GetValidFunction() через другую службу, которая здесь не отображается).

Если это так, то у меня нет проблем с переписыванием запросов внутрь, так как они делаются через LINQ с EF, но тут я заметил странность. Обратите внимание, что в этом запросе ожидание выполняется для Framework/Library CEEJitInfo::allocMem

Для другого запроса блок ожидания происходил для запроса REDIS. Но в большинстве случаев кажется, что вызов заблокирован внутри GetResults(), как на третьей картинке. Может ли все это время ожидания быть связано только с запросами к базе данных? (DTU также имеет всплеск, но это еще одна проблема, которую я должен исправить - вероятно, из-за плохого дизайна, большого количества таблиц с GUID как PK / FK - возможно, перестроение индекса? но это будет рассмотрено в другой раз)

Чтобы дать некоторый контекст этому приложению:

  • Веб-API, работающий на .NET 5
  • Позволяет пользователям создавать свои собственные шаблоны бритвы
  • Шаблоны хранятся в базе данных SQL Server.
  • Шаблоны запрашиваются, а затем компилируются и отображаются во время выполнения.

Другая возможная причина, которую я имею в виду, — это большое количество скомпилированных шаблонов бритвы. Таких просмотров могут быть сотни или даже больше тысячи. Я что-то думаю о аннулировании кеша представления, которое фреймворк делает внутри, заставляя представление перекомпилироваться?

Это может быть немного не по теме первоначального вопроса, но кто-нибудь знает, как компиляция среды выполнения бритвы работает именно в ASP.NET Core?

Конкретно:

  • Как долго эти представления хранятся в кеше?
  • Создает ли он DLL для каждого представления, как в .NET Framework, или они хранятся только в памяти?

Я пытался найти ответы на эти два вопроса, но не нашел ни одного.

В общем, я был бы очень признателен, если бы у вас были какие-то рекомендации по проблеме пиков загрузки процессора / времени ожидания. Знаете ли вы какую-либо возможную причину, которая может вызвать время ожидания помимо самого запроса? Может ли это быть связано с перекомпиляцией представления/сборщиком мусора?

Спасибо за ваше время.


Позднее редактирование: Исполняемый код выглядит примерно так

Controller-> GET ExecuteFunction(functionCode) -> ValidateFunction(functionCode) -> GetValidFunction(functionCode)

ValidateFunction также выполняет другие запросы, но уже после GetValidFunction.

private (string, Functions) GetValidFunction(Guid functionCode)
{
    var cacheKey = CacheKeys.FunctionError(functionCode);
    var cacheTimeSpan = new TimeSpan(0, cacheValidationMinutes, 0);
    var validationErrorMessage = cacheProvider.GetWithSlidingExpiration<string>(cacheKey, cacheTimeSpan);
    var function = functionLogic.GetValidFunctionByCode(functionCode);
    if (function == null)
    {
        cacheProvider.AddToCacheInvariantCase(cacheKey, invalidErrorCode, cacheTimeSpan);
        return (invalidErrorCode, null);
    }
    if (string.isNullOrEmpty(validationErrorMessage)) return (validationErrorMessage, function);
    var functionCodeData = functionCodeLogic.GetFunctionCode(functionCode);
    if (functionCodeData == null)
    {
        cacheProvider.AddToCacheInvariantCase(cacheKey, invalidErrorCode, cacheTimeSpan);
        return (invalidErrorCode, null);
    }
    if (function.StatusId == (int)FunctionStatusName.Active || function.StatusId == (int)FunctionStatusName.Draft)
    {
        cacheProvider.AddToCacheInvariantCase(cacheKey, NoErrorFunction, cacheTimeSpan);
    }

    return (null, function);
}

Запросы внутри GetValidFunction будут выполнять эту логику

   public T Get(Expression<Func<T, bool>> where)
    {
        return dbset.Where(where).FirstOrDefault();
    }

Вы смотрели на номер запроса? Вы уже используете async await-паттерн?

Ackdari 14.12.2020 16:09

Количество запросов не увеличивается. Async await используется, но не в этом конкретном блоке кода. Кроме того, запуск запроса, который выполнялся плохо, не воспроизводил эту проблему.

Remus 14.12.2020 16:14

Создается впечатление, что задействовано много движущихся частей. Можете ли вы упростить или исключить определенные аспекты из решения, чтобы увидеть, возникнет ли у вас та же проблема?

Bernard Vander Beken 14.12.2020 16:19

@BernardVanderBeken действительно много движущихся частей. К сожалению, я не могу исключить ни одного аспекта из решения. Запрос в причине в основном является ядром этого приложения. Почти все запросы будут отображать представление (существует 10 типов представлений, которые можно определить), и перед переходом к конкретной реализации типа представления вызывается метод/блок кода, описанный выше.

Remus 14.12.2020 16:33

В краткосрочной перспективе я могу переписать эти запросы, и, надеюсь, это уменьшит время ожидания. В долгосрочной перспективе будет избавление от бритвенных представлений в пользу механизма шаблонов на стороне клиента, такого как Handlebars, и избавление от GUID как PK.

Remus 14.12.2020 16:36

Мне кажется, что где-то вы выполняете синхронизацию (блокировку) вызовов ввода-вывода, что вызывает конфликты потоков. Кто-то не может устранить неполадки, если вы не поделитесь соответствующим фрагментом кода! В частности, мне было бы интересно увидеть вызов ввода-вывода (БД), а также вызывающего абонента до самого верха.

krishg 15.12.2020 08:14

@krishg Под БД ввода-вывода вы имеете в виду это? imgur.com/5oboKgo Так же в среднем 6-7 запросов в секунду, но как видите не постоянно. Внутри метода запросы структуры сущностей выполняются в одноэлементном dbcontext (ни один из которых не является асинхронным).

Remus 15.12.2020 16:01

learn.microsoft.com/azure/architecture/antipatterns/…

krishg 15.12.2020 18:40

@Remus - я думаю, что запрашивался код в функции IsValid, а также то, как он вызывается с вашего контроллера. Трудно отлаживать проблемы без кода.

Paddy 16.12.2020 16:50

Почему вы используете GetResults()? Можете ли вы полностью выполнять асинхронные вызовы вместо использования GetResults?

Mauricio Atanache 16.12.2020 16:56

1. Вы не должны использовать одноэлементный контекст базы данных. Сделайте его ограниченным. 2. Дважды проверьте свои фактические SQL-запросы, чтобы убедиться, что вы не оцениваете большие части вашего запроса в памяти.

Alex AIT 17.12.2020 07:09

@Paddy добавил немного кода в начальный пост

Remus 17.12.2020 08:35

@MauricioAtanache GetResults(), похоже, выполняется EntityFramework. Я перепишу эти запросы в dapper, используя асинхронные методы.

Remus 17.12.2020 08:38

@AlexAIT Плохо. Он имеет фабрику с ограниченной областью действия, но создает один экземпляр контекста в этой области.

Remus 17.12.2020 08:38
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
8
14
1 377
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Хотя вы не предоставили общий доступ к соответствующему фрагменту кода, но, судя по описанию и симптомам, это результат синхронного (блокирующего) ввода-вывода, выполненного где-то в вашем коде, что вызывает конфликты потоков.

ОБНОВЛЯТЬ: В вашем общем коде я вижу вызов синхронизации ввода-вывода, например, в методах GetValidFunction и Get. Должно быть, как показано ниже, и вызывающий абонент должен ждать. Помните, полностью асинхронно.

public Task<T> GetAsync(Expression<Func<T, bool>> where)
    {
        return dbset.Where(where).FirstOrDefaultAsync();
    }

Ниже будет очень общий ответ на эту проблему, в основном полученный из антипаттерна синхронного ввода-вывода . Некоторые ссылки на старое приложение asp.net и старую облачную службу ниже могут быть устаревшими сегодня, но концепция по-прежнему актуальна.

Антипаттерн синхронного ввода/вывода

Блокировка вызывающего потока во время завершения ввода-вывода может снизить производительность и повлиять на вертикальную масштабируемость.

Описание проблемы

Синхронная операция ввода-вывода блокирует вызывающий поток, пока ввод-вывод завершается. Вызывающий поток переходит в состояние ожидания и не может выполнять полезную работу в течение этого интервала, тратя впустую ресурсы обработки.

Общие примеры ввода/вывода включают:

  • Получение или сохранение данных в базе данных или постоянном хранилище любого типа.
  • Отправка запроса веб-сервису.
  • Публикация сообщения или извлечение сообщения из очереди.
  • Запись или чтение из локального файла.

Этот антипаттерн обычно возникает потому, что:

  • Это кажется наиболее интуитивным способом выполнения операции.
  • Приложение требует ответа на запрос.
  • Приложение использует библиотеку, которая предоставляет только синхронные методы ввода-вывода.
  • Внешняя библиотека выполняет внутренние синхронные операции ввода-вывода. Один синхронный вызов ввода-вывода может заблокировать всю цепочку вызовов.

Следующий код отправляет файл в хранилище BLOB-объектов Azure. Есть два места, где кодовые блоки ожидают синхронного ввода-вывода, метод CreateIfNotExists и метод UploadFromStream.

var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");

container.CreateIfNotExists();
var blockBlob = container.GetBlockBlobReference("myblob");

// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
    blockBlob.UploadFromStream(fileStream);
}

Вот пример ожидания ответа от внешней службы. Метод GetUserProfile вызывает удаленную службу, которая возвращает UserProfile.

public interface IUserProfileService
{
    UserProfile GetUserProfile();
}

public class SyncController : ApiController
{
    private readonly IUserProfileService _userProfileService;

    public SyncController()
    {
        _userProfileService = new FakeUserProfileService();
    }

    // This is a synchronous method that calls the synchronous GetUserProfile method.
    public UserProfile GetUserProfile()
    {
        return _userProfileService.GetUserProfile();
    }
}

Вы можете найти полный код для обоих этих примеров здесь.

Как решить проблему

Замените синхронные операции ввода-вывода асинхронными операциями. Это освобождает текущий поток для продолжения выполнения значимой работы, а не блокировки, и помогает улучшить использование вычислительных ресурсов. Асинхронный ввод-вывод особенно эффективен для обработки неожиданного всплеска запросов от клиентских приложений.

Многие библиотеки предоставляют как синхронные, так и асинхронные версии методов. По возможности используйте асинхронные версии. Вот асинхронная версия предыдущего примера, которая отправляет файл в хранилище BLOB-объектов Azure.

var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");

await container.CreateIfNotExistsAsync();

var blockBlob = container.GetBlockBlobReference("myblob");

// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
    await blockBlob.UploadFromStreamAsync(fileStream);
}

Оператор await возвращает управление вызывающей среде во время выполнения асинхронной операции. Код после этого оператора действует как продолжение, которое запускается после завершения асинхронной операции.

Хорошо спроектированная служба должна также обеспечивать асинхронные операции. Вот асинхронная версия веб-службы, которая возвращает профили пользователей. Метод GetUserProfileAsync зависит от наличия асинхронной версии службы профилей пользователей.

public interface IUserProfileService
{
    Task<UserProfile> GetUserProfileAsync();
}

public class AsyncController : ApiController
{
    private readonly IUserProfileService _userProfileService;

    public AsyncController()
    {
        _userProfileService = new FakeUserProfileService();
    }

    // This is an synchronous method that calls the Task based GetUserProfileAsync method.
    public Task<UserProfile> GetUserProfileAsync()
    {
        return _userProfileService.GetUserProfileAsync();
    }
}

Для библиотек, которые не предоставляют асинхронные версии операций, возможно создание асинхронных оболочек вокруг выбранных синхронных методов. Следуйте этому подходу с осторожностью. Хотя это может улучшить реакцию потока, который вызывает асинхронную оболочку, на самом деле он потребляет больше ресурсов. Может быть создан дополнительный поток, и накладные расходы связаны с синхронизацией работы, выполняемой этим потоком. Некоторые компромиссы обсуждаются в этой записи блога: Должен ли я предоставлять асинхронные оболочки для синхронных методов?

Вот пример асинхронной оболочки вокруг синхронного метода.

// Asynchronous wrapper around synchronous library method
private async Task<int> LibraryIOOperationAsync()
{
    return await Task.Run(() => LibraryIOOperation());
}

Теперь вызывающий код может ожидать в оболочке:

// Invoke the asynchronous wrapper using a task
await LibraryIOOperationAsync();

Соображения

  • Операции ввода-вывода, которые, как ожидается, будут очень короткими и вряд ли вызовут конкуренцию, могут быть более производительными, чем синхронные операции. Примером может быть чтение небольших файлов на SSD-накопителе. Накладные расходы на отправку задачи в другой поток и синхронизацию с этим потоком после завершения задачи могут перевесить преимущества асинхронного ввода-вывода. Однако такие случаи относительно редки, и большинство операций ввода-вывода следует выполнять асинхронно.

  • Повышение производительности ввода-вывода может привести к тому, что другие части системы станут узкими местами. Например, разблокировка потоков может привести к увеличению объема одновременных запросов к общим ресурсам, что, в свою очередь, приведет к нехватке ресурсов или регулированию. Если это становится проблемой, вам может потребоваться увеличить количество веб-серверов или разделов хранилищ данных, чтобы уменьшить конкуренцию.

Как обнаружить проблему

Для пользователей приложение может периодически переставать отвечать. Приложение может завершиться ошибкой из-за тайм-аута. Эти сбои также могут возвращать ошибки HTTP 500 (внутренний сервер). На сервере входящие клиентские запросы могут блокироваться до тех пор, пока поток не станет доступным, что приводит к чрезмерной длине очереди запросов, что проявляется в виде ошибок HTTP 503 (служба недоступна).

Вы можете выполнить следующие шаги, чтобы определить проблему:

  1. Отслеживайте производственную систему и определяйте, ограничивают ли пропускную способность заблокированные рабочие потоки.

  2. Если запросы блокируются из-за отсутствия потоков, проверьте приложение, чтобы определить, какие операции могут выполнять ввод-вывод синхронно.

  3. Выполните контролируемое нагрузочное тестирование каждой операции, выполняющей синхронный ввод-вывод, чтобы выяснить, влияют ли эти операции на производительность системы.

Пример диагностики

В следующих разделах эти шаги применяются к примеру приложения, описанному ранее.

Отслеживание производительности веб-сервера

Для веб-приложений и веб-ролей Azure стоит отслеживать производительность веб-сервера IIS. В частности, обратите внимание на длину очереди запросов, чтобы определить, блокируются ли запросы в ожидании доступных потоков в периоды высокой активности. Вы можете собрать эту информацию, включив диагностику Azure. Для получения дополнительной информации см.:

Инструментируйте приложение, чтобы увидеть, как обрабатываются запросы после их принятия. Отслеживание потока запроса может помочь определить, выполняет ли он медленные вызовы и блокирует ли текущий поток. Профилирование потоков также может выделять заблокированные запросы.

Нагрузочное тестирование приложения

На следующем графике показана производительность синхронного метода GetUserProfile, показанного ранее, при различных нагрузках до 4000 одновременных пользователей. Приложение представляет собой приложение ASP.NET, работающее в веб-роли облачной службы Azure.

Performance chart for the sample application performing synchronous I/O operations

Синхронная операция жестко запрограммирована на 2 секунды бездействия, чтобы имитировать синхронный ввод-вывод, поэтому минимальное время отклика составляет чуть более 2 секунд. Когда нагрузка достигает примерно 2500 одновременных пользователей, среднее время отклика выходит на плато, хотя объем запросов в секунду продолжает увеличиваться. Обратите внимание, что шкала для этих двух показателей является логарифмической. Между этой точкой и окончанием теста количество запросов в секунду удваивается.

В отдельности из этого теста не обязательно ясно, является ли синхронный ввод-вывод проблемой. При более высокой нагрузке приложение может достичь критической точки, когда веб-сервер больше не может своевременно обрабатывать запросы, что приводит к тому, что клиентские приложения получают исключения из-за тайм-аута.

Входящие запросы ставятся в очередь веб-сервером IIS и передаются потоку, работающему в пуле потоков ASP.NET. Поскольку каждая операция выполняет ввод-вывод синхронно, поток блокируется до завершения операции. По мере увеличения рабочей нагрузки в конечном итоге все потоки ASP.NET в пуле потоков выделяются и блокируются. В этот момент любые дальнейшие входящие запросы должны ждать в очереди доступного потока. По мере увеличения длины очереди запросы начинают истекать по тайм-ауту.

Реализовать решение и проверить результат

На следующем графике показаны результаты нагрузочного тестирования асинхронной версии кода.

Performance chart for the sample application performing asynchronous I/O operations

Пропускная способность гораздо выше. За ту же продолжительность, что и в предыдущем тесте, система успешно справляется с почти десятикратным увеличением пропускной способности, измеряемой в запросах в секунду. Более того, среднее время отклика остается относительно постоянным и примерно в 25 раз меньше, чем в предыдущем тесте.

Другие вопросы по теме

Похожие вопросы

Не удается обновить собственную службу Windows, поскольку она используется другим процессом
Как установить MATLAB MCR и среду выполнения dotnet в контейнере Docker?
Как я могу объяснить отсутствие точности даты и времени в миллисекундах при выполнении сравнений даты и времени?
Можно ли перенести мое приложение .NET Core 3.1 на .NET 5?
Как привязать сгенерированную кнопку С# к XAML MVVM
При индексировании (ElasticClient.IndexMany()) генерируется исключение StackOverflowException
LINQ — сумма на основе перекрывающихся дат
Клиент на мобильном телефоне, который будет управлять/отправлять команды клиенту моего ПК-сервера
Исключение для Xamarin: System.ObjectDisposedException: невозможно получить доступ к удаленному объекту «Xamarin.Forms.Platform.Android.FastRenderers.LabelRenderer»
Как очистить историю переходов во фрейме при использовании окна Текущая страница в качестве модели представления Свойство для модели представления окна