Русский
Русский
English
Статистика
Реклама

OpenTelemetry на практике

Совсем недавно два стандарта OpenTracing и OpenCensus окончательно объединились в один. Появился новый стандарт распределенного трейсинга и мониторинга OpenTelemetry. Но несмотря на то, что разработка библиотек идет полным ходом, реального опыта его использования пока не слишком много.

Илья Казначеев, который занимается разработкой восемь лет и работает backend-разработчиком в МТС, готов поделиться тем, как применять OpenTelemetry в Golang-проектах. На конференции Golang Live 2020 он рассказал о том, как настроить использование нового стандарта для трейсинга и мониторинга и подружить его с уже существующей в проекте инфраструктурой.



OpenTelemetry стандарт, который появился относительно недавно: в конце прошлого года. При этом он получил широкое распространение и поддержку множества вендоров ПО для трейсинга и мониторинга.

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



Для трейсинга и мониторинга существует множество вендорских решений. До недавнего времени было два открытых стандарта: OpenTracing от CNCF, который появился в 2016, и Open Census от Google, появившийся в 2018.

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



Этот стандарт включает в себя распределенный трейсинг и мониторинг. Он совместим с первыми двумя. Более того, OpenTracing и Open Census прекращают поддержку в течение двух лет, что неотвратимо приближает нас к переходу на OpenTelemetry.

Сценарии использования

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

Для распределенного трейсинга можно напрямую настроить подключение к Jaeger или к тому сервису, который вы используете.



Если трейсинг транслируется напрямую, можно использовать config и просто заменить библиотеку.

В случае, если ваше приложение уже использует OpenTracing, можно воспользоваться OpenTracing Bridge оберткой, которая будет конвертировать запросы к OpenTracing API в OpenTelemetry API на верхнем уровне.



Для сбора метрик также можно настроить прямой доступ в Prometheus к порту для сбора метрик вашего приложения.



Это пригодится, если у вас простая инфраструктура, и вы собираете метрики напрямую. Но стандарт предоставляет и более гибкие возможности.

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

Для этого достаточно настроить в приложении экспортер в формате OTLP. Это grpc-схема для передачи данных в формате OpenTracing. Со стороны коллектора можно настроить формат и параметры экспорта метрик и трейсов конечным потребителям, либо в другие форматы. Например, в OpenCensus.



Коллектор позволяет подключать большое количество видов источников данных и множество приемников данных на выходе.



Таким образом, стандарт OpenTelemetry обеспечивает совместимость с многими открытыми и вендорскими стандартами.

Стандартный коллектор расширяем. Поэтому у большинства вендоров уже готовы экспортеры в их собственные решения, если они есть. Вы можете использовать OpenTelemetry, даже если применяете сбор метрик и трейсов от какого-то закрытого вендора. Таким образом решается проблема с vendor lock-in. Даже если что-то еще не появилось непосредственно для OpenTelemetry, это можно пробросить через OpenCensus.

Сам коллектор очень просто конфигурируется через банальный YAML конфиг:



Здесь указываются ресиверы. В вашем приложении может быть какой-то другой источник (Kafka и т.д.):



Экспортеры получатели данных.
Процессоры методы обработки данных внутри коллектора:



И pipelines, которые непосредственно определяют, как будет обрабатываться каждый поток данных, который протекает внутри коллектора:



Давайте рассмотрим один показательный пример.



Допустим, у вас есть некий микросервис, к которому вы уже прикрутили OpenTelemetry и настроили его. И еще один сервис с аналогичной фрагментацией.

Пока все легко. Но появляются:

  • legacy-сервисы, которые работают через OpenCensus;
  • база данных, которая отдает данные в своем формате (например, напрямую в Prometheus, как это делает PostgreSQL);
  • еще какой-то сервис, работающий в контейнере и отдающий метрики в своем формате. Вам не хочется перебилдить этот контейнер и прикручивать sidecars, чтобы они переформатировали метрики. Вы хотите просто брать и отправлять их.
  • железо, с которого вы тоже собираете метрики и хотите как-то использовать их.


Все эти метрики можно объединить одним коллектором.



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

Теперь мы хотим использовать эту информацию. В качестве экспортера трейсов можно указать Jaeger, а метрики отправлять в Prometheus, или что-то совместимое. Допустим, всеми любимую VictoriaMetrics.

Но что, если мы вдруг решили переехать в AWS и использовать местный трейсер XRay? Не проблема. Это можно пробросить через OpenCensus, в котором есть экспортер для XRay.

Таким образом, из этих кусков можно собрать всю вашу инфраструктуру для метрик и трейсов.

С теорией покончено. Поговорим о том, как использовать трейсинг на практике.

Инструментация Golang приложения: трейсинг

Сначала нужно создать корневой span, от которого и будет расти дерево вызовов.

ctx := context.Background()
tr := global.Tracer(github.com/me/otel-demo)
ctx, span := tr.Start(ctx, root)
span.AddEvent(ctx, I am a root span!)
doSomeAction(ctx, 12345)
span.End()

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

Дальше создается корневой span с указанием названия:


ctx, span := tr.Start(ctx, root)


Выбирайте название, которое будет внятно описывать уровень трейса. Например, это может быть либо название метода (или класса и метода), либо слой архитектуры. Например, инфраструктурный уровень, уровень логики, уровень баз данных и т.д.

Данные о span также кладутся в контекст:


ctx, span := tr.Start(ctx, root)
span.AddEvent(ctx, I am a root span!)
doSomeAction(ctx, 12345)


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

ctx, span := tr.Start(ctx, root)
span.AddEvent(ctx, I am a root span!)
doSomeAction(ctx, 12345)
span.End()

Так наш span выглядит в Jaeger:



Его можно развернуть и посмотреть логи и атрибуты.

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

func doSomeAction(ctx context.Context, requestID string) {
span := trace.SpanFromContext(ctx)
span.AddEvent(ctx, I am the same span!)

}
Обратите внимание, что закрывать здесь его не нужно, потому что он закроется в том же методе, где был создан. Мы просто берем его из контекста.

Записываем сообщение в корневой span:



Иногда нужно создать новый дочерний span, чтобы он существовал отдельно.

func doSomeAction(ctx context.Context, requestID string) {
ctx, span := global.Tracer(github.com/me/otel-demo).
Start(ctx, child)
defer span.End()
span.AddEvent(ctx, I am a child span!)

}

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

Дальше создается дочерний span из контекста, и ему присваивается имя по аналогии с тем, как мы делали это в начале:


Start(ctx, child)


Не забудьте, что span нужно закрыть в конце метода, в котором он был создан.


ctx, span := global.Tracer(github.com/me/otel-demo).
Start(ctx, child)
defer span.End()


Пишем в него сообщения, которые попадают в дочерний span.



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

В нем показываются атрибуты, которые можно писать в span:

func doSomeAction(ctx context.Context, requestID string) {

span.SetAttributes(label.String(request.id, requestID))
span.AddEvent(ctx, request validation ok)
span.AddEvent(ctx, entities loaded, label.Int64(count, 123))
span.SetStatus(codes.Error, insertion error)
}

Например, сюда попал наш request. id:



Можно добавлять events:


span.AddEvent(ctx, request validation ok)


Кроме того, сюда можно добавлять label. Это работает примерно также, как структурный лог в виде logrus:


span.AddEvent(ctx, entities loaded, label.Int64(count, 123))


Здесь мы видим свое сообщение в логе span. Можно развернуть его и посмотреть labels. В нашем случае сюда добавился label count:



Потом это будет удобно использовать при фильтрации в поиске.

Если возникла ошибка, в span можно дописать статус. В этом случае он будет помечен как ошибочный.


span.SetStatus(codes.Error, insertion error)


Раньше стандарт использовал коды ошибок из OpenCensus, и они были из grpc. Сейчас оставили только OK, ERROR и UNSET. OK ставится по умолчанию, ERROR в случае ошибки добавляете вы.

Здесь видно, что трейс ошибки помечен красным значком. Есть код ошибки и сообщение о ней:



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

Трейсинг микросервисов

В OpenTelemetry уже есть множество set party реализаций interceptors и middleware для различных фреймворков и библиотек. Их можно найти в репозитории: github.com/open-telemetry/opentelemetry-go-contrib

Список фреймворков, для которых есть interceptors и middleware:

  • beego
  • go-restful
  • gin
  • gocql
  • mux
  • echo
  • http
  • grpc
  • sarama
  • memcache
  • mongo
  • macaron


Как это использовать, посмотрим на примере стандартного http клиента и сервера.

middleware client

В клиенте просто добавляем interceptor в качестве транспорта, после чего наши запросы обогащаются на trace.id и необходимую для продолжения трейса информацию.

client := http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, _ := http.NewRequestWithContext(ctx, GET, url, nil)
resp, err := client.Do(req)

middleware server

На сервере добавляется небольшой middleware с названием библиотеки:

http.Handle("/", otelhttp.NewHandler(
http.HandlerFunc(get), root))
err := http.ListenAndServe(addr, nil)

Дальше как обычно: получаете span из контекста, работаете с ним, пишите в него что-то, создаете дочерние spans, закрываете их и т.д.

Так выглядит простой запрос, проходящий через три сервиса:



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

А так выглядит ошибка:



Легко отследить, где она произошла, когда и сколько времени прошло.
В span можно посмотреть подробную информацию о контексте, в котором произошла ошибка:



Более того, поля, которые относятся ко всему span (различные id запроса, ключевые поля в таблице в запросе, еще какие-то мета данные, которые вы хотите положить) можно вкладывать в span при его создании. Грубо говоря, не нужно копипастить все эти поля в каждое место, где вы обрабатываете ошибку. Можно записать данные о ней в span.

middleware func

Вот небольшой бонус: как сделать middleware, чтобы можно было использовать его в качестве глобального middleware для таких вещей как Gorilla и Gin:

middleware := func(h http.Handler) http.Handler {
return otelhttp.NewHandler(h, root)
}

Инструментация Golang приложения: мониторинг

Пришло время поговорить о мониторинге.

Подключение к системе мониторинга настраивается аналогично тому, что делается для трейсинга.

Измерения делятся на два типа:

1. Синхронные, когда пользователь явно передает значения в момент вызова:
  • Counter
  • UpDownCounter
  • ValueRecorder


int64, float64

2. Асинхронные, которые SDK считывает в момент коллекта данных из приложения:
  • SumObserver
  • UpDownSumObserver
  • ValueObserver


int64, float64

Сами метрики бывают:

Аддитивные и монотонные (Counter, SumObserver), которые суммируют положительные числа, и они не уменьшаются.

Аддитивные, но не монотонные (UpDownCounter, UpDownSumObserver), которые могут суммировать положительные и отрицательные числа.

Неаддитивные (ValueRecorder, ValueObserver), которые просто записывают последовательность значений. Например, какое-то распределение.

В начале программы создается глобальный измеритель, которому указывается название библиотеки или сервиса.

meter := global.Meter(github.com/ilyakaznacheev/otel-demo)
floatCounter := metric.Must(meter).NewFloat64Counter(
float_counter,
metric.WithDescription(Cumulative float counter),
).Bind(label.String(label_a, some label))
defer floatCounter.Unbind()

Дальше создается метрика:


floatCounter := metric.Must(meter).NewFloat64Counter(
float_counter,
metric.WithDescription(Cumulative float counter),
).Bind(label.String(label_a, some label))


Ей указывается название:


float_counter,


Описание:


metric.WithDescription(Cumulative float counter),


Набор лейблов, по которым вы потом можете фильтровать запросы. Например, при построении дашбордов в Grafana:


).Bind(label.String(label_a, some label))


В конце программы также нужно вызвать Unbind для каждой метрики, что освободит ресурсы и правильно ее закроет:


defer floatCounter.Unbind()


Записывать изменения просто:

var (
counter metric.BoundFloat64Counter
udCounter metric.BoundFloat64UpDownCounter
valueRecorder metric.BoundFloat64ValueRecorder
)

counter.Add(ctx, 1.5)
udCounter.Add(ctx, -2.5)
valueRecorder.Record(ctx, 3.5)

Это положительные числа для Counter, любые числа для UpDownCounter, которые он будет суммировать, и также любые числа для ValueRecorder. Для всех видов инструментов в Go поддерживаются int64 и float64.

Вот что мы получаем на выходе:

# HELP float_counter Cumulative float counter
# TYPE float_counter counter
float_counter{label_a=some label} 20

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

Инструментация Golang приложения: библиотеки

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

Раньше, когда использовались OpenCensus и OpenTracing, вы не могли инструментировать ваши отдельные библиотеки, особенно опенсорсные. Потому что в этом случае у вас получался vendor lock-in. Тот, кто плотно работал с трейсингом, наверняка обращал внимание на то, что большие клиентские библиотеки, или крупные API к облачным сервисам, время от времени падают с трудно объяснимыми ошибками.

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

OpenTelemetry решает эту проблему.



Так как в стандарте SDK и API разделены, API трейсинга метрик можно использовать независимо от SDK и конкретных настроек экспорта данных. Более того, вы можете сначала инструментировать свои методы, а только потом настроить экспорт этих данных вовне.
Таким образом можно инструментировать импортируемую библиотеку, не заботясь о том, как и куда будут экспортированы эти данные. Это подойдет и для внутренних, и для открытых опенсорсных библиотек.

Не нужно заботиться о vendor lock-in, не нужно переживать по поводу того, как будет использована эта информация и будет ли использована вообще. Библиотеки и приложения инструментируются заранее, а конфигурация экспорта данных указывается при инициализации приложения.

Таким образом видно, что настройки конфигурации задаются в SDK приложении. Дальше нужно заняться экспортерами трейсинга и метрик. Это может быть один экспортер через OTLP, если вы экспортируете в OpenTelemetry коллектор. Потом все необходимые трейсы и метрики попадают в контекст, а он пропагируется по дереву вызова другим методом вниз.

Приложение наследует от корневого span остальные spans, просто используя OpenTelemetry API и данные, которые лежат в контексте. При этом импортируемые библиотеки получают на вход методы контекст, пытаются считать из этого метода информацию о корневом span. Если его нет, создают свой, и дальше инструментируют логику. Таким образом, вы можете сначала инструментировать свою библиотеку.

Более того, вы можете инструментировать все, но не настраивать экспортеры данных, и просто задеплоить это.

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

Таким образом, если у вас какая-то опенсорсная библиотека, вы можете инструментировать ее при помощи OpenTelemetry. Потом люди, которые ее используют, настроят у себя OpenTelemetry и будут использовать эти данные.

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

У нас в компании активно применяется стандарт OpenCensus для трейсинга и мониторинга микросервисного ландшафта компании. Планируется внедрение OpenTelemetry после его релиза.
Источник: habr.com
К списку статей
Опубликовано: 18.01.2021 16:06:50
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании мтс

Мтс

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru