Как использовать OpenTelemetry для определения зависимостей базы данных
13 мая 2022 г.Микросервисы могут помочь любой организации достичь своей цели повышения гибкости за счет решения критических факторов, таких как повышение автономии команды, сокращение времени выхода на рынок, экономически эффективное масштабирование нагрузки и предотвращение полного простоя приложений.
По мере того как организации разбивают свои монолитные приложения на микросервисы, одной из основных проблем, с которыми они сталкиваются, является определение зависимостей базы данных.
Совместное использование базы данных может быть сложной и трудоемкой задачей. Базы данных не позволяют вам определить, что является общим, а что нет. При изменении схемы для лучшего обслуживания одной микрослужбы вы можете непреднамеренно нарушить способ использования той же базы данных другой микрослужбой.
Кроме того, часто бывает сложно определить владельца данных и найти бизнес-логику, управляющую данными.
В этой статье мы рассмотрим, как мы можем использовать OpenTelemetry для идентификации компонентов, которые совместно используют одну и ту же базу данных и объекты базы данных, такие как таблицы.
Наблюдаемость и OpenTelemetry: основы
Прежде чем мы создадим наше демонстрационное приложение, давайте заложим основу, обсудив наблюдаемость и OpenTelemetry.
Что делает приложение заметным?
Говорят, что система хорошо наблюдаема, если о ее внутреннем состоянии можно сделать вывод, изучая ее выходные данные в любой момент времени.
Например, наблюдаемое мобильное приложение, взаимодействующее с несколькими службами, может реконструировать транзакцию, выдающую ответ об ошибке, чтобы разработчики могли определить основную причину сбоя.
Наблюдаемое приложение собирает три типа информации для каждой транзакции:
- Журналы: запись отдельных событий, составляющих транзакцию.
- Метрики: запись совокупностей событий, составляющих транзакцию.
- Трассы: запись о задержке операций для выявления узких мест в транзакции.
Что такое OpenTelemetry?
OpenTelemetry — это система, которая интегрированным образом генерирует журналы, метрики и трассировки. OpenTelemetry определяет стандарт для сбора данных наблюдаемости. Модель данных OpenTelemetry состоит из нескольких ключевых компонентов.
Атрибуты
Каждая структура данных в OpenTelemetry состоит из атрибутов, представляющих собой пары ключ-значение. Стандарт OpenTelemetry определяет, какие атрибуты может указывать любой компонент (например, клиент SQL или HTTP-запрос).
События
События — это просто отметка времени и набор атрибутов. Вы можете записывать такие сведения, как сообщения и сведения об исключениях для события.
Контекст
Контекст включает в себя атрибуты, общие для набора событий. Это два типа контекстов. Статический контекст (или ресурс) определяет местонахождение событий. Их значение не меняется после запуска исполняемого файла приложения. Примеры включают имя или версию службы или имя библиотеки.
Динамический контекст (или промежуток) определяет активную операцию, содержащую событие. Значение атрибутов span изменяется при выполнении операции. Некоторые общие примеры атрибутов диапазона включают время начала запроса, код состояния ответа HTTP или путь запроса HTTP.
В распределенной транзакции контекст необходимо передать всем связанным службам. В таких случаях служба получателя использует контекст для создания новых интервалов. Трассировка, пересекающая границу службы, становится распределенной трассировкой , а процесс передачи контекста другим службам называется распространением контекста.
Журналы
Журналы — это события, которые только сопровождают ресурсы. Одним из примеров этого является событие, генерируемое при запуске программы.
Следы
События могут быть организованы в граф операций, связанных с ресурсами. Трассировка — это график, отображающий события, связанные с транзакцией.
Показатели
Событие может произойти несколько раз в любом приложении или его значение может измениться. Метрика — это событие, значением которого может быть количество связанных событий или некоторое вычисление значения события. Примером метрики является событие системной памяти, его атрибутами являются использование и использование.
Подробные сведения о принципах OpenTelemetry см. в документации.
Использование OpenTelemetry для определения зависимостей базы данных
Ранее мы обсуждали, что OpenTelemetry предписывает атрибуты, которые должны захватывать различные компоненты приложения. Спецификация атрибутов, которые должны быть частью диапазона, охватывающего вызов клиента базы данных, задокументирована на GitHub. Многие популярные языки предоставляют готовые инструментальные библиотеки, которые собирают данные телеметрии для операций с базами данных.
В этой демонстрации мы будем использовать инструментарий .NET SQLClient для OpenTelemetry в координации с Lightstep для хранения и анализа телеметрии.
Давайте обсудим архитектуру демонстрационного приложения, чтобы понять путь, по которому телеметрия идет к Lightstep. Мы сосредоточим наше обсуждение только на трассировках, поскольку их достаточно для определения зависимости между базами данных и компонентами монолита.
Однако любое корпоративное приложение должно генерировать соответствующие журналы и метрики в дополнение к трассировкам для полной видимости.
Во-первых, мы снабдим наше монолитное приложение SDK OpenTelemetry для генерации сигналов наблюдаемости. В то время как инструментирование приложения — это ручной процесс для приложений .NET, автоматическое инструментирование доступно для приложений, созданных с помощью таких языков, как Golang или Java.
Мы используем экспортер OpenTelemetry Protocol (OTLP), который включен в SDK. Экспортер позволяет нам отправлять данные непосредственно в службу приема телеметрии. Платформы OpenTelemetry, такие как Jaeger и Lightstep, объединяют трассировки, чтобы помочь вам получить представление.
После интеграции с пакетом SDK различные части вашего приложения, такие как обработчик запросов ASP.NET Core и клиент SQL, автоматически начинают создавать трассировки с соответствующей информацией. Ваш код может генерировать дополнительные трассировки для обогащения доступной информации.
В случае .NET реализация OpenTelemetry основана на существующих типах в пространстве имен System.Diagnostics.* следующим образом:
System.Diagnostics.ActivitySource
представляет трассировщик OpenTelemetry, отвечающий за созданиеSpan
.
System.Diagnostics.Activity
представляетSpan
.
- Вы можете добавить атрибуты в диапазон, используя функцию
AddTag
. Кроме того, вы можете добавить багаж с помощью функцииAddBaggage
. Багаж переносится в дочерние действия, которые могут быть доступны в других службах, использующих заголовок W3C.
После инструментирования вашего приложения вы можете запускать автоматические тесты или разрешать пользователям использовать ваше приложение для охвата всех путей взаимодействия между приложением и базой данных.
Демонстрация
Давайте создадим простую монолитную службу управления персоналом (EMS), смоделированную как минимальный API ASP.NET Core. Наш API будет иметь следующие конечные точки:
POST /ems/billing
: записывает количество часов, отработанных сотрудником над проектом.
GET /ems/billing/{employeeId}
: получает количество часов, затраченных сотрудником на разные проекты.
POST /ems/payroll/add
: добавляет сотрудника в платежную ведомость.
GET /ems/payroll/{employeeId}
: извлекает данные платежной ведомости для сотрудника.
Вы заметите, что монолит обслуживает два отдельных домена: выставление счетов и начисление заработной платы. Такие зависимости могут быть не очень очевидны в сложных монолитах, и их разделение может потребовать значительного рефакторинга кода.
Однако, изучив зависимости, вы сможете без особых усилий их расцепить.
Полный исходный код приложения EMS доступен в этом репозитории GitHub.
Раскрутка базы данных
Сначала мы запускаем экземпляр SQL-сервера в докере:
``` ударить
докер запустить \
-e "ПРИНЯТЬ_EULA=Y" \
-e "SA_PASSWORD=Str0ngPa$$w0rd" \
-p 1433:1433 \
--name монолит-БД \
--имя хоста sql1 \
-d mcr.microsoft.com/mssql/server:2019-последняя
Мы используем следующий сценарий SQL для создания базы данных EMS и таблиц, используемых нашим приложением:
```sql
ЕСЛИ НЕ СУЩЕСТВУЕТ (ВЫБЕРИТЕ * ИЗ sys.databases, ГДЕ имя = 'EMSDb')
НАЧИНАТЬ
СОЗДАТЬ БАЗУ ДАННЫХ EMSDb
КОНЕЦ
ИДТИ
ИСПОЛЬЗОВАТЬ EMSDb
ЕСЛИ OBJECT_ID('[dbo].[Хранение времени]', 'U') IS NULL
НАЧИНАТЬ
СОЗДАТЬ ТАБЛИЦУ [Хранение времени] (
[EmployeeId] INT NOT NULL,
[ProjectId] INT NOT NULL,
[WeekClosingDate] DATETIME NOT NULL,
[Часы работы] INT NOT NULL,
ОГРАНИЧЕНИЕ [PK_Timekeeping] ПЕРВИЧНЫЙ КЛЮЧ CLUSTERED ([EmployeeId] ASC, [ProjectId] ASC, [WeekClosingDate] ASC)
КОНЕЦ
ИДТИ
ЕСЛИ OBJECT_ID('[dbo].[Зарплатная ведомость]', 'U') IS NULL
НАЧИНАТЬ
СОЗДАТЬ ТАБЛИЦУ [Зарплата] (
[EmployeeId] INT NOT NULL,
[PayRateInUSD] ДЕНЬГИ ПО УМОЛЧАНИЮ 0 НЕ NULL,
ОГРАНИЧЕНИЕ [PK_Payroll] ПЕРВИЧНЫЙ КЛЮЧ CLUSTERED ([EmployeeId] ASC)
КОНЕЦ
ИДТИ
Реализация службы API
Далее мы пишем код для конечных точек API. Мы заменяем шаблонный код в классе Program следующим кодом:
```csharp
var builder = WebApplication.CreateBuilder(аргументы);
builder.Services.AddScoped(_ =>
новое SqlConnection(builder.Configuration.GetConnectionString("EmployeeDbConnectionString")));
var app = builder.Build();
приложение.UseSwagger();
приложение.UseSwaggerUI();
app.MapPost("/ems/billing", async (Timekeeping timekeepingRecord, SqlConnection db) =>
ожидание db.ExecuteAsync(
«ВСТАВИТЬ В Значения хронометража (@EmployeeId, @ProjectId, @WeekClosingDate, @HoursWorked)»,
Табель учета рабочего времени);
return Results.Created($"/ems/billing/{timekeepingRecord.EmployeeId}", timekeepingRecord);
.WithName("RecordProjectWork")
.Produces(StatusCodes.Status201Created);
app.MapGet("/ems/billing/{empId}/", async (int empId, SqlConnection db) =>
var result = await db.QueryAsync<хронометраж>("SELECT * FROM Timekeeping WHERE EmployeeId=@empId", empId);
вернуть результат.Любой() ? Results.Ok(результат): Results.NotFound();
.WithName("ПолучитьПодробностиБиллинга")
.Produces<IEnumerable<Хранение времени>>()
.Produces(StatusCodes.Status404NotFound);
app.MapPost("/ems/payroll/add/", async (Payroll payrollRecord, SqlConnection db) =>
ожидание db.ExecuteAsync(
«ВСТАВЬТЕ В ЗНАЧЕНИЯ ЗАРПЛАТЫ(@EmployeeId, @PayRateInUSD)», payrollRecord);
return Results.Created($"/ems/payroll/{payrollRecord.EmployeeId}", payrollRecord);
.WithName("AddEmployeeToPayroll")
.Produces(StatusCodes.Status201Created);
app.MapGet("/ems/payroll/{empId}", async (int empId, SqlConnection db) =>
var result = await db.QueryAsync
вернуть результат.Любой() ? Results.Ok(результат): Results.NotFound();
.WithName("GetEmployeePayroll")
.Produces
.Produces(StatusCodes.Status404NotFound);
приложение.Выполнить();
публичный класс Хронометраж
публичный идентификатор сотрудника {получить; набор; }
общественный интервал ProjectId { получить; набор; }
общественное DateTimeWeekClosingDate { получить; набор; }
общественность int HoursWorked { получить; набор; }
публичный класс
публичный идентификатор сотрудника {получить; набор; }
общественная десятичная PayRateInUSD { получить; набор; }
На этом этапе мы можем запустить приложение, протестировать различные конечные точки и просмотреть записи, сохраненные в базе данных. Хотя зависимости различных конечных точек и путей запросов от базы данных в этом демонстрационном примере очевидны, в больших приложениях это не так.
Далее давайте автоматизируем процесс обнаружения зависимостей базы данных.
Добавление инструментария
Мы оснастили приложение [OpenTelemetry SDK] (https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.SqlClient) и библиотекой инструментов SqlClient для. NET. Во-первых, мы добавляем следующие ссылки на пакеты NuGet в файл проекта API:
SDK предоставляет нам несколько методов расширения, которые мы можем использовать для быстрого подключения OpenTelemetry к конвейеру обработки запросов.
Следующий фрагмент кода использует OpenTelemetry в нашем API. Он также настроит SqlClient для отправки подробных данных телеметрии. Телеметрия от SqlClient
является ключом к подробному определению зависимостей базы данных.
```csharp
// Настроить трассировку
builder.Services.AddOpenTelemetryTracing (строитель => строитель
// Настраиваем трассировки, собранные обработчиком HTTP-запросов
.AddAspNetCoreInstrumentation(параметры =>
// Захватите только промежутки, сгенерированные из конечных точек ems/*
options.Filter = context => context.Request.Path.Value?.Contains("ems") ?? ЛОЖЬ;
options.RecordException = истина;
// Добавьте метаданные для запроса, такие как метод HTTP и длина ответа
options.Enrich = (активность, eventName, rawObject) =>
переключатель (имя события)
случай «OnStartActivity»:
если (rawObject не является HttpRequest httpRequest)
возврат;
Activity.SetTag("Протокол запроса", httpRequest.Protocol);
Activity.SetTag («Метод запроса», httpRequest.Method);
перемена;
случай "OnStopActivity":
если (rawObject является HttpResponse httpResponse)
Activity.SetTag("responseLength", httpResponse.ContentLength);
перемена;
// Настраиваем телеметрию, сгенерированную SqlClient
.AddSqlClientInstrumentation(параметры =>
options.EnableConnectionLevelAttributes = true;
options.SetDbStatementForStoredProcedure = true;
options.SetDbStatementForText = true;
options.RecordException = истина;
options.Enrich = (активность, x, y) => activity.SetTag("db.type", "sql");
.AddSource("my-corp.ems.ems-api")
// Создайте ресурсы (пары "ключ-значение"), описывающие вашу службу, например имя службы и ее версию.
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("ems-api")
.AddAttributes(new[] { new KeyValuePair
// Гарантирует, что все действия записываются и отправляются экспортеру
.SetSampler(новый AlwaysOnSampler())
// Экспорт отрезков в Lightstep
.AddOtlpExporter(otlpOptions =>
otlpOptions.Endpoint = новый Uri("https://ingest.lightstep.com:443/traces/otlp/v0.9");
otlpOptions.Headers = $"lightstep-access-token={lsToken}";
otlpOptions.Protocol = OtlpExportProtocol.HttpProtobuf;
Хотя в текущем состоянии нам достаточно инструментария, давайте еще больше обогатим данные, добавив соответствующие трассировки.
Во-первых, мы определяем трассировщик, из которого будут исходить диапазоны нашего приложения.
```csharp
var activitySource = новый ActivitySource("my-corp.ems.ems-api");
Затем мы создаем диапазон и добавляем соответствующие детали — атрибуты и события:
```csharp
app.MapPost("/ems/billing", async (Timekeeping timekeepingRecord, SqlConnection db) =>
используя var activity = activitySource.StartActivity("Запись работы над проектом", ActivityKind.Server);
Activity?.AddEvent(new ActivityEvent("Проект выставлен счет"));
Activity?.SetTag(nameof(Timekeeping.EmployeeId), timekeepingRecord.EmployeeId);
Activity?.SetTag(nameof(Timekeeping.ProjectId), timekeepingRecord.ProjectId);
Activity?.SetTag(nameof(Timekeeping.WeekClosingDate), timekeepingRecord.WeekClosingDate);
ожидание db.ExecuteAsync(
«ВСТАВИТЬ В Значения хронометража (@EmployeeId, @ProjectId, @WeekClosingDate, @HoursWorked)»,
Табель учета рабочего времени);
return Results.Created($"/ems/billing/{timekeepingRecord.EmployeeId}", timekeepingRecord);
.WithName("RecordProjectWork")
.Produces(StatusCodes.Status201Created);
Мы следуем той же процедуре, чтобы настроить оставшиеся конечные точки.
Подключение к Lightstep
Наконец, нам нужен ключ API для отправки трассировки в Lightstep. Начнем с создания учетной записи. На странице настроек проекта нашей учетной записи мы находим токен, который будет служить нашим ключом API.
Мы копируем токен и вставляем его в файл appsettings.
```json
"Логирование": {
«Уровень журнала»: {
"По умолчанию": "Информация",
"Microsoft.AspNetCore": "Предупреждение"
"Разрешенные хосты": "*",
«Строки подключения»: {
"EmployeeDbConnectionString": "Сервер=localhost;База данных=EMSDb;Идентификатор пользователя=sa;Пароль=Str0ngPa$$w0rd;"
"LsToken": "<токен Lightstep>"
Отправка запросов
Наше приложение готово. Мы запускаем приложение и отправляем несколько запросов на каждую конечную точку. Вот пример запроса, который я отправил на конечную точку /ems/billing
. Этот запрос должен создать запись в таблице хронометража базы данных».
Вот еще один запрос, который я отправил в конечную точку /emp/payroll/add
, чтобы добавить запись в таблицу Payroll:
Когда мы переходим на [портал наблюдения Lightstep] (https://app.lightstep.com/), мы можем щелкнуть вкладку «Операции», чтобы увидеть все промежутки, полученные Lightstep из приложения.
Когда мы нажимаем на операцию /ems/payroll/add
, мы можем просмотреть сквозную трассировку. Просмотрев промежутки, мы можем установить последовательность операций для любого запроса. Нажатие на промежутки вызывает его события и атрибуты, из которых мы можем получить более глубокое представление об операции.
Последний диапазон, видимый в трассировке, — EMSDb, созданный нашим инструментальным SQL-клиентом. Мы нажимаем на диапазон, чтобы просмотреть его атрибуты и события следующим образом:
Мы можем извлечь некоторые ключевые идеи из атрибутов:
- Название базы данных
- Оператор SQL, используемый в операции с базой данных
- Тип оператора SQL (текст или хранимая процедура)
- Имя хоста службы, сделавшей запрос
Аналогичный набор сведений мы находим в дочернем диапазоне операции /ems/billing.
Прочесывая информацию из следов, мы можем сделать следующие выводы:
- Входные операции (операции, которые получают внешний запрос)
- Последовательность действий по выполнению запроса, включая вызовы внешних служб и операции с базой данных.
- Операции с базой данных, задействованные в каждой операции.
В совокупности этой информации нам достаточно, чтобы спланировать разделение сервисов и баз данных и установить контакты для связи между микросервисами.
Вывод
В этой статье обсуждалась одна из распространенных проблем, с которыми сталкиваются разработчики при переводе своих монолитных приложений на микросервисы. Из всех проблем разделение базы данных является сложной задачей, поскольку любая служба, имеющая доступ к базе данных, может манипулировать ею.
Используя OpenTelemetry, мы можем определить зависимости между различными компонентами и между компонентами и базой данных. Зная и понимая наши зависимости, мы можем разработать план рефакторинга для наших компонентов, спланировав, как они должны развиваться как независимые микросервисы.
Оригинал