В этой статье описывается, как команда разработчиков использовала метрики для поиска узких мест и повышения производительности распределенной системы. Статья основана на фактическом нагрузочном тестировании, которое мы провели для примера приложения.
Этот материал входит в цикл статей. Прочитайте первую часть здесь.
Сценарий. Обработка потока событий с помощью Функции Azure.
В этом сценарии парк беспилотных летательных аппаратов отправляет данные о положении в режиме реального времени в Центр Интернета вещей Azure. Приложение Функций получает события, преобразует данные в формат GeoJSON и записывает преобразованные данные в Azure Cosmos DB. Azure Cosmos DB имеет встроенную поддержку геопространственных данных, а коллекции Azure Cosmos DB можно индексировать для эффективных пространственных запросов. Например, клиентское приложение может запрашивать все беспилотные летательные аппараты в пределах 1 км от заданного расположения или находить все беспилотные летательные аппараты в пределах определенной области.
Эти требования к обработке достаточно просты и не требуют полноценного обработчика потоковой обработки. В частности, обработка не объединяет потоки, агрегированные данные и не обрабатывается в течение временных периодов. Исходя из этих требований, Функции Azure хорошо подходит для обработки сообщений. Azure Cosmos DB также можно масштабировать для поддержки очень высокой пропускной способности записи.
Мониторинг пропускной способности
Этот сценарий представляет собой интересную проблему производительности. Скорость передачи данных на устройство известна, но количество устройств может меняться. В этом бизнес-сценарии требования к задержке не являются особенно строгими. Сообщаемое положение дрона должно быть точным только в течение минуты. При этом приложение-функция должно соответствовать средней скорости приема с течением времени.
Центр Интернета вещей сохраняет сообщения в потоке журналов. Входящие сообщения добавляются к заключительному фрагменту потока. Читатель потока ( в данном случае приложение-функция) управляет собственной скоростью обхода потока. Это разделение путей чтения и записи делает Центр Интернета вещей очень эффективным, но также означает, что медленное средство чтения может отстать. Чтобы обнаружить это условие, группа разработчиков добавила пользовательскую метрику для измерения задержки сообщений. Эта метрика записывает разницу между поступлением сообщения в Центр Интернета вещей и получением функцией сообщения для обработки.
var ticksUTCNow = DateTimeOffset.UtcNow;
// Track whether messages are arriving at the function late.
DateTime? firstMsgEnqueuedTicksUtc = messages[0]?.EnqueuedTimeUtc;
if (firstMsgEnqueuedTicksUtc.HasValue)
{
CustomTelemetry.TrackMetric(
context,
"IoTHubMessagesReceivedFreshnessMsec",
(ticksUTCNow - firstMsgEnqueuedTicksUtc.Value).TotalMilliseconds);
}
Метод TrackMetric
записывает пользовательскую метрику в Application Insights. Сведения об использовании TrackMetric
в функции Azure см. в статье Пользовательские данные телеметрии в функции C#.
Если функция соответствует объему сообщений, эта метрика должна оставаться в низком стабильном состоянии. Некоторая задержка неизбежна, поэтому значение никогда не будет равным нулю. Но если функция отстает, разница между временем постановки в очередь и временем обработки начнет идти вверх.
Тест 1. Базовые показатели
Первый нагрузочный тест показал немедленную проблему: приложение-функция постоянно получало ошибки HTTP 429 из Azure Cosmos DB, указывающие на то, что Azure Cosmos DB регулирует запросы на запись.
В ответ команда масштабирует Azure Cosmos DB, увеличив количество ЕЗ, выделенных для коллекции, но ошибки продолжаются. Это казалось странным, поскольку их вычисление обратной оболочки показало, что Azure Cosmos DB не должно иметь проблем с объемом запросов на запись.
Позже в тот же день один из разработчиков отправил команде следующее сообщение электронной почты:
Я посмотрел на Azure Cosmos DB, чтобы найти теплый путь. Есть одна вещь, которую я не понимаю. Ключ секции — deliveryId, однако мы не отправляем deliveryId в Azure Cosmos DB. Мне чего-то не хватает?
Это была подсказка. Просмотрев тепловую карту секций, оказалось, что все документы находились на одной секции.
То, что вы хотите увидеть на тепловой карте, — это равномерное распределение по всем секциям. В этом случае, поскольку каждый документ записывался в одну и ту же секцию, добавление ЕЗ не помогло. Проблема оказалась ошибкой в коде. Хотя в коллекции Azure Cosmos DB был ключ секции, функция Azure фактически не включала ключ секции в документ. Дополнительные сведения о тепловой карте секций см. в разделе Определение распределения пропускной способности между секциями.
Тест 2. Исправление проблемы с секционированием
Когда команда развернула исправление кода и повторно запустила тест, Azure Cosmos DB прекратила регулирование. На некоторое время все выглядело хорошо. Но при определенной нагрузке данные телеметрии показали, что функция писала меньше документов, чем она должна. На следующем графике показаны сообщения, получаемые из Центр Интернета вещей и документы, записанные в Azure Cosmos DB. Желтая строка — количество сообщений, полученных на пакет, а зеленая — количество документов, написанных на пакет. Они должны быть пропорциональными. Вместо этого количество операций записи базы данных на пакет значительно снижается примерно с 07:30.
На следующем графике показана задержка между поступлением сообщения на Центр Интернета вещей с устройства и обработкой этого сообщения приложением-функцией. Вы можете видеть, что в тот же момент времени, задержка резко пики, выровняться, и снижение.
Причина, по которой значение достигает максимума в 5 минут, а затем падает до нуля, заключается в том, что приложение-функция удаляет сообщения с задержкой более чем на 5 минут:
foreach (var message in messages)
{
// Drop stale messages,
if (message.EnqueuedTimeUtc < cutoffTime)
{
log.Info($"Dropping late message batch. Enqueued time = {message.EnqueuedTimeUtc}, Cutoff = {cutoffTime}");
droppedMessages++;
continue;
}
}
Это можно увидеть на графике, когда метрика задержки падает до нуля. В то же время данные были потеряны, так как функция выбрасывала сообщения.
Что происходит? Для этого конкретного нагрузочного теста в коллекции Azure Cosmos DB было необходимо сэкономить ЕЗ, поэтому узкое место не было в базе данных. Скорее, проблема заключается в цикле обработки сообщений. Проще говоря, функция не писала документы достаточно быстро, чтобы не отставать от объема входящих сообщений. Со временем, он падал все дальше и дальше позади.
Тест 3. Параллельная запись
Если время обработки сообщения является узким местом, одно из решений заключается в параллельной обработке большего число сообщений. В этом сценарии:
- Увеличьте число секций Центр Интернета вещей. Каждой секции Центр Интернета вещей назначается по одному экземпляру функции за раз, поэтому мы ожидаем линейного масштабирования пропускной способности с количеством секций.
- Параллелизуйте записи в документе в функции .
Чтобы изучить второй вариант, команда изменила функцию для поддержки параллельных операций записи. Исходная версия функции использовала выходную привязку Azure Cosmos DB. Оптимизированная версия вызывает клиент Azure Cosmos DB напрямую и выполняет записи параллельно с помощью Task.WhenAll:
private async Task<(long documentsUpserted,
long droppedMessages,
long cosmosDbTotalMilliseconds)>
ProcessMessagesFromEventHub(
int taskCount,
int numberOfDocumentsToUpsertPerTask,
EventData[] messages,
TraceWriter log)
{
DateTimeOffset cutoffTime = DateTimeOffset.UtcNow.AddMinutes(-5);
var tasks = new List<Task>();
for (var i = 0; i < taskCount; i++)
{
var docsToUpsert = messages
.Skip(i * numberOfDocumentsToUpsertPerTask)
.Take(numberOfDocumentsToUpsertPerTask);
// client will attempt to create connections to the data
// nodes on Azure Cosmos DB clusters on a range of port numbers
tasks.Add(UpsertDocuments(i, docsToUpsert, cutoffTime, log));
}
await Task.WhenAll(tasks);
return (this.UpsertedDocuments,
this.DroppedMessages,
this.CosmosDbTotalMilliseconds);
}
Обратите внимание, что условия гонки возможны при подходе. Предположим, что два сообщения от одного дрона поступают в один и тот же пакет сообщений. Записав их параллельно, более раннее сообщение может перезаписать последующее сообщение. В этом конкретном сценарии приложение может допускать потерю случайных сообщений. Дроны отправляют новые данные о положении каждые 5 секунд, поэтому данные в Azure Cosmos DB постоянно обновляются. Однако в других сценариях может быть важно обрабатывать сообщения строго по порядку.
После развертывания этого изменения кода приложение могло принимать более 2500 запросов в секунду, используя Центр Интернета вещей с 32 секциями.
Рекомендации на стороне клиента
Общее взаимодействие с клиентом может быть уменьшено из-за агрессивной параллелизации на стороне сервера. Рассмотрите возможность использования библиотеки исполнителя массовых операций Azure Cosmos DB (не показано в этой реализации), которая значительно сокращает объем вычислительных ресурсов на стороне клиента, необходимых для насыщения пропускной способности, выделенной контейнеру Azure Cosmos DB. Однопоточное приложение, записывающее данные с помощью API массового импорта, обеспечивает почти в десять раз большую пропускную способность записи по сравнению с многопоточным приложением, которое параллельно записывает данные, одновременно перенасыщая ЦП клиентского компьютера.
Сводка
В этом сценарии были выявлены следующие узкие места:
- Раздел горячей записи из-за отсутствия значения ключа секции в записываемом документе.
- Запись документов в последовательном формате для каждой секции Центр Интернета вещей.
Для диагностики этих проблем команда разработчиков использовала следующие метрики:
- Регулируемые запросы в Azure Cosmos DB.
- Тепловая карта секций — максимальное потребление ЕЗ на секцию.
- Полученные сообщения и созданные документы.
- Задержка сообщения.
Дальнейшие действия
Проверка антишаблонов производительности