Проблема в том, что процессор регулярно скачет от ~ 10% до более чем 70%:
К сожалению, это, по-видимому, влияет на среднее время отклика, вызывая некоторые всплески.
Это хороший сценарий, когда среднее значение остается ниже 1 с, но иногда оно может работать довольно плохо.
Я попытался исследовать эту проблему на портале Azure и заметил, что некоторые запросы остаются в этом блоке, что заставляет меня думать, что это была проблема с запросом (из того, что я вижу, это не совсем трассировка стека, здесь может быть более одного запроса происходит внутри GetValidFunction()
через другую службу, которая здесь не отображается).
Если это так, то у меня нет проблем с переписыванием запросов внутрь, так как они делаются через LINQ с EF, но тут я заметил странность. Обратите внимание, что в этом запросе ожидание выполняется для Framework/Library CEEJitInfo::allocMem
Для другого запроса блок ожидания происходил для запроса REDIS
. Но в большинстве случаев кажется, что вызов заблокирован внутри GetResults()
, как на третьей картинке. Может ли все это время ожидания быть связано только с запросами к базе данных? (DTU также имеет всплеск, но это еще одна проблема, которую я должен исправить - вероятно, из-за плохого дизайна, большого количества таблиц с GUID как PK / FK - возможно, перестроение индекса? но это будет рассмотрено в другой раз)
Чтобы дать некоторый контекст этому приложению:
Другая возможная причина, которую я имею в виду, — это большое количество скомпилированных шаблонов бритвы. Таких просмотров могут быть сотни или даже больше тысячи. Я что-то думаю о аннулировании кеша представления, которое фреймворк делает внутри, заставляя представление перекомпилироваться?
Это может быть немного не по теме первоначального вопроса, но кто-нибудь знает, как компиляция среды выполнения бритвы работает именно в ASP.NET Core?
Конкретно:
Я пытался найти ответы на эти два вопроса, но не нашел ни одного.
В общем, я был бы очень признателен, если бы у вас были какие-то рекомендации по проблеме пиков загрузки процессора / времени ожидания. Знаете ли вы какую-либо возможную причину, которая может вызвать время ожидания помимо самого запроса? Может ли это быть связано с перекомпиляцией представления/сборщиком мусора?
Спасибо за ваше время.
Позднее редактирование: Исполняемый код выглядит примерно так
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
используется, но не в этом конкретном блоке кода. Кроме того, запуск запроса, который выполнялся плохо, не воспроизводил эту проблему.
Создается впечатление, что задействовано много движущихся частей. Можете ли вы упростить или исключить определенные аспекты из решения, чтобы увидеть, возникнет ли у вас та же проблема?
@BernardVanderBeken действительно много движущихся частей. К сожалению, я не могу исключить ни одного аспекта из решения. Запрос в причине в основном является ядром этого приложения. Почти все запросы будут отображать представление (существует 10 типов представлений, которые можно определить), и перед переходом к конкретной реализации типа представления вызывается метод/блок кода, описанный выше.
В краткосрочной перспективе я могу переписать эти запросы, и, надеюсь, это уменьшит время ожидания. В долгосрочной перспективе будет избавление от бритвенных представлений в пользу механизма шаблонов на стороне клиента, такого как Handlebars, и избавление от GUID как PK.
Мне кажется, что где-то вы выполняете синхронизацию (блокировку) вызовов ввода-вывода, что вызывает конфликты потоков. Кто-то не может устранить неполадки, если вы не поделитесь соответствующим фрагментом кода! В частности, мне было бы интересно увидеть вызов ввода-вывода (БД), а также вызывающего абонента до самого верха.
@krishg Под БД ввода-вывода вы имеете в виду это? imgur.com/5oboKgo Так же в среднем 6-7 запросов в секунду, но как видите не постоянно. Внутри метода запросы структуры сущностей выполняются в одноэлементном dbcontext (ни один из которых не является асинхронным).
learn.microsoft.com/azure/architecture/antipatterns/…
@Remus - я думаю, что запрашивался код в функции IsValid, а также то, как он вызывается с вашего контроллера. Трудно отлаживать проблемы без кода.
Почему вы используете GetResults()? Можете ли вы полностью выполнять асинхронные вызовы вместо использования GetResults?
1. Вы не должны использовать одноэлементный контекст базы данных. Сделайте его ограниченным. 2. Дважды проверьте свои фактические SQL-запросы, чтобы убедиться, что вы не оцениваете большие части вашего запроса в памяти.
@Paddy добавил немного кода в начальный пост
@MauricioAtanache GetResults(), похоже, выполняется EntityFramework. Я перепишу эти запросы в dapper, используя асинхронные методы.
@AlexAIT Плохо. Он имеет фабрику с ограниченной областью действия, но создает один экземпляр контекста в этой области.
Хотя вы не предоставили общий доступ к соответствующему фрагменту кода, но, судя по описанию и симптомам, это результат синхронного (блокирующего) ввода-вывода, выполненного где-то в вашем коде, что вызывает конфликты потоков.
ОБНОВЛЯТЬ:
В вашем общем коде я вижу вызов синхронизации ввода-вывода, например, в методах 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 (служба недоступна).
Вы можете выполнить следующие шаги, чтобы определить проблему:
Отслеживайте производственную систему и определяйте, ограничивают ли пропускную способность заблокированные рабочие потоки.
Если запросы блокируются из-за отсутствия потоков, проверьте приложение, чтобы определить, какие операции могут выполнять ввод-вывод синхронно.
Выполните контролируемое нагрузочное тестирование каждой операции, выполняющей синхронный ввод-вывод, чтобы выяснить, влияют ли эти операции на производительность системы.
В следующих разделах эти шаги применяются к примеру приложения, описанному ранее.
Для веб-приложений и веб-ролей Azure стоит отслеживать производительность веб-сервера IIS. В частности, обратите внимание на длину очереди запросов, чтобы определить, блокируются ли запросы в ожидании доступных потоков в периоды высокой активности. Вы можете собрать эту информацию, включив диагностику Azure. Для получения дополнительной информации см.:
Инструментируйте приложение, чтобы увидеть, как обрабатываются запросы после их принятия. Отслеживание потока запроса может помочь определить, выполняет ли он медленные вызовы и блокирует ли текущий поток. Профилирование потоков также может выделять заблокированные запросы.
На следующем графике показана производительность синхронного метода GetUserProfile
, показанного ранее, при различных нагрузках до 4000 одновременных пользователей. Приложение представляет собой приложение ASP.NET, работающее в веб-роли облачной службы Azure.
Синхронная операция жестко запрограммирована на 2 секунды бездействия, чтобы имитировать синхронный ввод-вывод, поэтому минимальное время отклика составляет чуть более 2 секунд. Когда нагрузка достигает примерно 2500 одновременных пользователей, среднее время отклика выходит на плато, хотя объем запросов в секунду продолжает увеличиваться. Обратите внимание, что шкала для этих двух показателей является логарифмической. Между этой точкой и окончанием теста количество запросов в секунду удваивается.
В отдельности из этого теста не обязательно ясно, является ли синхронный ввод-вывод проблемой. При более высокой нагрузке приложение может достичь критической точки, когда веб-сервер больше не может своевременно обрабатывать запросы, что приводит к тому, что клиентские приложения получают исключения из-за тайм-аута.
Входящие запросы ставятся в очередь веб-сервером IIS и передаются потоку, работающему в пуле потоков ASP.NET. Поскольку каждая операция выполняет ввод-вывод синхронно, поток блокируется до завершения операции. По мере увеличения рабочей нагрузки в конечном итоге все потоки ASP.NET в пуле потоков выделяются и блокируются. В этот момент любые дальнейшие входящие запросы должны ждать в очереди доступного потока. По мере увеличения длины очереди запросы начинают истекать по тайм-ауту.
На следующем графике показаны результаты нагрузочного тестирования асинхронной версии кода.
Пропускная способность гораздо выше. За ту же продолжительность, что и в предыдущем тесте, система успешно справляется с почти десятикратным увеличением пропускной способности, измеряемой в запросах в секунду. Более того, среднее время отклика остается относительно постоянным и примерно в 25 раз меньше, чем в предыдущем тесте.
Вы смотрели на номер запроса? Вы уже используете
async await
-паттерн?