Антишаблон загруженности внешнего интерфейса
Выполнение асинхронных операций в большом количестве фоновых потоков может замедлить выполнение других задач переднего плана ресурсов, что приводит к уменьшению времени отклика до недопустимого уровня.
Описание проблемы
Ресурсоемкие задачи могут увеличить время отклика на запросы пользователей и привести к высокой задержке. Один из способов уменьшения времени отклика заключается в разгрузке ресурсоемких задач в отдельный поток. Этот подход позволяет приложению реагировать во время обработки в фоновом режиме. Тем не менее задачи, выполняемые в фоновом потоке, по-прежнему потребляют ресурсы. Большое количество таких задач может замедлить выполнение потоков, обрабатывающих запросы.
Примечание.
Термин ресурс может охватывать множество понятий, таких как использование ЦП, заполнение памяти и операции ввода-вывода сети или диска.
Эта проблема обычно возникает, когда приложение разработано как монолитный фрагмент кода со всеми бизнес-логиками, объединенными на одном уровне с уровнем представления данных.
Ниже приведен псевдокод, демонстрирующий проблему.
public class WorkInFrontEndController : ApiController
{
[HttpPost]
[Route("api/workinfrontend")]
public HttpResponseMessage Post()
{
new Thread(() =>
{
//Simulate processing
Thread.SpinWait(Int32.MaxValue / 100);
}).Start();
return Request.CreateResponse(HttpStatusCode.Accepted);
}
}
public class UserProfileController : ApiController
{
[HttpGet]
[Route("api/userprofile/{id}")]
public UserProfile Get(int id)
{
//Simulate processing
return new UserProfile() { FirstName = "Alton", LastName = "Hudgens" };
}
}
Метод
Post
в контроллереWorkInFrontEnd
реализует операцию HTTP POST. Эта операция моделирует продолжительную задачу с интенсивной нагрузкой ЦП. Задача выполняется в отдельном потоке. Это позволяет выполнить операцию POST быстрее.Метод
Get
в контроллереUserProfile
реализует операцию HTTP GET. Этот метод требует меньше ресурсов ЦП.
Особое внимание следует уделить требованиям к ресурсам метода Post
. Несмотря на то что задачи выполняются в фоновом потоке, они по-прежнему могут потреблять значительный объем ресурсов ЦП. Эти ресурсы используются и другими операциями, выполняемыми другими параллельно работающими пользователями. Если умеренное количество пользователей одновременно отправит этот запрос, это с большей вероятностью отрицательно повлияет на общую производительность, что приведет к снижению скорости выполнения всех операций. У пользователей, например, могут возникнуть значительные задержки в методе Get
.
Как устранить проблему
Переместите процессы, требующие значительные ресурсы, в отдельную серверную часть.
При этом подходе внешний интерфейс помещает ресурсоемкие задачи в очередь сообщений. Серверная часть выбирает эти задачи и асинхронно обрабатывает их. Очередь также позволяет выровнять нагрузку, выполняя буферизацию запросов серверной части. Если очередь становится слишком длинной, вы можете настроить автоматическое масштабирование, чтобы горизонтально увеличить масштаб серверной части.
Ниже приведена исправленная версия предыдущего примера кода. В этой версии метод Post
помещает сообщение в очередь служебной шины.
public class WorkInBackgroundController : ApiController
{
private static readonly QueueClient QueueClient;
private static readonly string QueueName;
private static readonly ServiceBusQueueHandler ServiceBusQueueHandler;
public WorkInBackgroundController()
{
string serviceBusNamespace = ...;
QueueName = ...;
ServiceBusQueueHandler = new ServiceBusQueueHandler(serviceBusNamespace);
QueueClient = ServiceBusQueueHandler.GetQueueClientAsync(QueueName).Result;
}
[HttpPost]
[Route("api/workinbackground")]
public async Task<long> Post()
{
return await ServiceBusQueueHandler.AddWorkLoadToQueueAsync(QueueClient, QueueName, 0);
}
}
Серверная часть извлекает эти сообщения и обрабатывает их.
public async Task RunAsync(CancellationToken cancellationToken)
{
this._queueClient.OnMessageAsync(
// This lambda is invoked for each message received.
async (receivedMessage) =>
{
try
{
// Simulate processing of message
Thread.SpinWait(Int32.MaxValue / 1000);
await receivedMessage.CompleteAsync();
}
catch
{
receivedMessage.Abandon();
}
});
}
Рекомендации
- Этот подход усложняет архитектуру приложения. При обработке помещения сообщений в очередь и удаления их из нее следует соблюдать осторожность, чтобы не потерять запросы в случае сбоя.
- Приложение зависит от дополнительной службы обработки очереди сообщений.
- Среда обработки должна обладать достаточной масштабируемостью, чтобы обработать ожидаемую рабочую нагрузку и обеспечить соответствие целевым показателям пропускной способности.
- Хотя такой подход должен повысить общую скорость реагирования, выполнение задач, перемещаемых в серверную часть, может занять больше времени.
Как определить проблему
Если внешний интерфейс загружен, при выполнении ресурсоемких задач возникают большие задержки. Конечные пользователи, скорее всего, сообщают о расширенных времени отклика или сбоях, вызванных истечением времени ожидания служб. Эти ошибки также могут возвращать ошибки HTTP 500 (внутренний сервер) или ошибки HTTP 503 (служба недоступна). Изучите журналы событий веб-сервера, которые, скорее всего, содержат более подробные сведения о причинах и обстоятельствах возникновения ошибок.
Чтобы определить эту проблему, сделайте следующее:
- Выполните мониторинг рабочей системы, чтобы определить точки замедления времени отклика.
- Проверьте данные телеметрии, собранные в этих точках, чтобы определить, выполняемые операции и используемые ресурсы.
- Определите связь между увеличением времени отклика и количеством выполняемых в это время операций, а также их сочетанием.
- Выполните нагрузочное тестирование каждой подозрительной операции, чтобы определить, какие из них потребляют ресурсы и замедляют выполнение других операций.
- Просмотрите исходный код этих операций, чтобы определить причину избыточного потребления ресурсов.
Пример диагностики
В следующих разделах эти шаги применяются к примеру приложения, описанному ранее.
Определение точек замедления
Выполните инструментирование каждого метода, чтобы отследить продолжительность каждого запроса и потребляемые ресурсы. Затем выполните мониторинг приложения в рабочей среде. Это может обеспечить полное представление того, как запросы конкурируют друг с другом. Во время периодов высокой нагрузки медленно выполняемые, нуждающиеся в ресурсах запросы, скорее всего, повлияют на другие операции. Это поведение, а также соответствующие проблемы с производительностью можно предотвратить, выполняя мониторинг системы.
На снимке экрана ниже показана панель мониторинга. (Мы использовались AppDynamics для наших тестов.) Изначально система имеет светлую нагрузку. Затем пользователи запрашивают метод GET UserProfile
. Производительность оставалась довольно хорошей, пока другие пользователи не начали отправлять запросы к методу POST WorkInFrontEnd
. На этом этапе значительно увеличилось время отклика (первая стрелка). Время отклика улучшилось только после снижения количества запросов к контроллеру WorkInFrontEnd
(вторая стрелка).
Изучение данных телеметрии и поиск связи
На следующем снимке экрана показаны некоторые метрики, собранные для мониторинга использования ресурсов на притяжении того же периода времени. Сначала несколько пользователей вошли в систему. Когда к ней подключается больше пользователей, загрузка ЦП становится очень высокой (100 %). Кроме того, обратите внимание, что с увеличением использования ресурсов ЦП сначала скорость сетевых операций ввода-вывода поднимается. Но когда использование ЦП достигает пиковых значений, скорость сетевых операций ввода-вывода снижается. Это связано с тем, что система может обрабатывать только относительно небольшое число запросов, когда ЦП работает на полную мощность. Когда пользователи отключаются, нагрузка на ЦП уменьшается.
На этом этапе, скорее всего, более тщательно следует проанализировать метод Post
в контроллере WorkInFrontEnd
. Чтобы подтвердить эту версию, в управляемой среде нужно провести дальнейшую работу.
Выполнение нагрузочного тестирования
Следующий шаг — провести тестирования в управляемой среде. Например, можно выполнить ряд нагрузочных тестов, которые сначала включают каждый запрос, а затем исключают их, чтобы оценить влияние.
На графике ниже показаны результаты нагрузочного теста, выполненного в такой же облачной службе, что и в предыдущих тестах. При тестировании применялась постоянная нагрузка с 500 пользователями, выполняющими операцию Get
в контроллере UserProfile
, а также пошаговая нагрузка с пользователями, выполняющими операцию Post
в контроллере WorkInFrontEnd
.
Изначально пошаговая нагрузка равна нулю, поэтому только активные пользователи выполняют запросы UserProfile
. Система может отвечать примерно на 500 запросов в секунду. Через 60 секунд нагрузку увеличили на 100 дополнительных пользователей, которые начинают отправлять запросы POST к контроллеру WorkInFrontEnd
. Почти мгновенно рабочая нагрузка, отправленная к контроллеру UserProfile
, снижается до около 150 запросов в секунду. Это связано с функционированием средства выполнения нагрузочных тестов. Он ожидает ответ перед отправкой следующего запроса, поэтому чем больше время ответа, тем ниже частота запросов.
При увеличении количества пользователей, отправляющих запросы POST к контроллеру WorkInFrontEnd
, скорость ответа контроллера UserProfile
продолжает снижаться. Однако обратите внимание, что количество запросов, обрабатываемых контроллером WorkInFrontEnd
, остается относительно постоянным. Насыщенность системы становится очевидной, так как общее число обоих запросов становится постоянным, но имеет минимальный уровень.
Просмотр исходного кода
Последний шаг — просмотреть исходный код. Группе разработчиков стало известно, что выполнение метода Post
может занять значительное время, поэтому при исходной реализации используется отдельный поток. Это позволило решить насущную проблему, так как метод Post
не блокирует выполнение длительных задач.
Однако операция, выполняемая этим методом, по-прежнему зависит от ЦП, памяти и других ресурсов. Если выполнить этот процесс асинхронно, это может фактически снизить производительность, так как пользователи смогут одновременно активировать большое количество этих операций без какого-либо контроля. Число потоков, которое можно запустить на сервере, ограничено. При превышении этого предела, когда приложение попытается создать поток, скорее всего, возникнет исключение.
Примечание.
Это не значит, что следует избегать асинхронных операций. В сетевых вызовах рекомендуется применять асинхронные методы с использованием оператора await (См. раздел Синхронный антипаттерн ввода-вывода .) Проблема заключается в том, что работа с большим объемом ЦП была вызвана другим потоком.
Реализация решения и проверка результатов
На снимке экрана ниже приведены показатели производительности после реализации решения. Нагрузка совпадает с приведенной выше, но теперь время ответа контроллера UserProfile
значительно выше. За аналогичный период количество запросов выросло с 2759 до 23 565.
Обратите внимание, что контроллер WorkInBackground
также обработал гораздо большее количество запросов. Однако прямое сравнение в этом случае невозможно сделать, так как операции, выполняемые в этом контроллере, сильно отличаются от исходного кода. Новая версия просто добавляет запрос в очередь, а не выполняет продолжительное вычисление. Самое главное, что этот метод больше не влияет на производительность всей системы под нагрузкой.
Показатели использования ресурсов ЦП и сети также улучшились. Показатель использования ЦП никогда не достигал 100 %, а количество обработанных сетевых запросов значительно выросло и не снижалось до падения рабочей нагрузки.
На графике ниже показаны результаты нагрузочного теста. Общее количество обработанных запросов значительно увеличилось по сравнению с предыдущими тестами.