Скачки процессора / Время ожидания ASP.NET Основное приложение

#c# #.net #azure #asp.net-core #azure-web-app-service

#c# #.net #azure #asp.net-ядро #azure-веб-приложение-сервис

Вопрос:

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

Процент процессора приложения

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

Среднее время отклика

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

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

блок ожидания 1

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

блок ожидания 2

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

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

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

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

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

В частности:

  • Как долго эти представления хранятся в кэше?
  • Создает ли он 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();
    }
 

Комментарии:

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

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

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

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

5. Почему вы используете GetResults() ?, можете ли вы выполнять асинхронные вызовы до конца вместо использования GetResults?

Ответ №1:

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

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

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

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

Антишаблон синхронного ввода-вывода

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

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

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

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

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

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

  • По-видимому, это наиболее интуитивно понятный способ выполнения операции.
  • Приложение требует ответа на запрос.
  • Приложение использует библиотеку, которая предоставляет только синхронные методы для ввода-вывода.
  • An external library performs synchronous I/O operations internally. A single synchronous I/O call can block an entire call chain.

The following code uploads a file to Azure blob storage. There are two places where the code blocks waiting for synchronous I/O, the CreateIfNotExists method and the UploadFromStream method.

 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);
}
 

Here’s an example of waiting for a response from an external service. The GetUserProfile method calls a remote service that returns a 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();
    }
}
 

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

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

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

Многие библиотеки предоставляют как синхронные, так и асинхронные версии методов. По возможности используйте асинхронные версии. Вот асинхронная версия предыдущего примера, которая загружает файл в хранилище больших двоичных объектов 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();
 

Соображения

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

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

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

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

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

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

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

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

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

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

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

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

Диаграмма производительности для примера приложения, выполняющего синхронные операции ввода-вывода

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

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

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

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

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

Диаграмма производительности для примера приложения, выполняющего асинхронные операции ввода-вывода

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