OTel Collector как посредник: декаплируем приложения от Jaeger и Prometheus
Подключили OpenTelemetry Collector между сервисами и бэкендами наблюдаемости - смена Jaeger на Zipkin превратилась из деплоя в правку конфига.
OpenTelemetry Collector - единый агент для сбора трейсов, метрик и логов, первые версии под эгидой CNCF
После того как OpenTelemetry объявил о слиянии OpenTracing и OpenCensus, проект не стал ждать у моря погоды - довольно быстро появился OpenTelemetry Collector. Отдельный агент-посредник, который принимает телеметрию от приложений и раздаёт её дальше - туда, куда скажут. Мы взяли его на пилот в один из managed-проектов и поняли, что идея стоящая, хотя и с шероховатостями.
Откуда росла проблема
Приложения в том проекте были инструментированы напрямую: Java-сервисы слали трейсы в Jaeger по UDP через jaeger-client, Go-шные метрики уходили в Prometheus через экспортер. Схема рабочая, но жёсткая.
Когда заказчик сказал «нам хочется попробовать Zipkin для части сервисов», команда немного поморщилась. Потому что переехать с Jaeger на Zipkin - это не просто поднять новый контейнер. Это поменять клиент в приложении, пересобрать образы, проверить формат трейсов, накатить в prod. Немало возни за то, что по сути является сменой адреса для отправки данных.
Вот тут и пришло время попробовать Collector.
Что такое OTel Collector
Грубо говоря - это прокси для телеметрии. У него три части: receivers (куда приходят данные от приложений), processors (что с ними сделать по дороге) и exporters (куда отправить дальше). Конфиг выглядит примерно так:
receivers:
jaeger:
protocols:
thrift_udp:
endpoint: "0.0.0.0:6831"
thrift_http:
endpoint: "0.0.0.0:14268"
processors:
batch:
exporters:
jaeger:
endpoint: "jaeger-collector:14250"
insecure: true
zipkin:
endpoint: "http://zipkin:9411/api/v2/spans"
service:
pipelines:
traces:
receivers: [jaeger]
processors: [batch]
exporters: [jaeger]
Приложения продолжают слать трейсы по jaeger-протоколу, но теперь - не в Jaeger напрямую, а в Collector. А Collector решает куда это идёт. Хотим Zipkin вместо Jaeger - меняем exporter в конфиге. Хотим в оба сразу - добавляем оба в список exporters. Ни одной строчки кода в приложении.
graph LR
A[Java-сервис] -->|jaeger thrift| C[OTel Collector]
B[Go-сервис] -->|jaeger thrift| C
C -->|jaeger gRPC| J[Jaeger]
C -->|zipkin HTTP| Z[Zipkin]
P[Prometheus] -->|scrape| C
Как разворачивали
Collector поставили как DaemonSet - по одному pod-у на ноду. Сервисы настроили слать на localhost:6831, то есть через loopback, что убирает network-hop и упрощает конфиг security-групп. Jaeger-коллектор вынесли за пределы прямого доступа приложений - только Collector знает куда именно ходить.
На старте всё было хорошо, пока не начали смотреть на метрики самого Collector-а. Он экспортирует собственную статистику в Prometheus-формате, и там сразу видно что за последние 10 минут receiver потерял несколько сотен spans. Начали копать.
Первая проблема - UDP и буферы. Jaeger-клиент по умолчанию шлёт spans по UDP. При нагрузке UDP-пакеты дропались - размер датаграммы превышал MTU, и часть spans тихо терялась ещё до Collector-а. Переключились на jaeger thrift_http (TCP), потери пропали. Это скорее проблема jaeger-клиента, но Collector хотя бы помог её заметить через свои метрики.
Вторая проблема - batch processor. Без батчинга Collector честно форвардит каждый span отдельным запросом в Jaeger, что при большом количестве spans создаёт лишнюю нагрузку. Добавили batch processor с настройками по умолчанию - ситуация улучшилась. Документация на этот момент была скудной, разбирались по исходникам.
Версия продукта сырая. Это нельзя замалчивать. OTel Collector на момент пилота - это early release. Конфигурационный формат менялся между минорными версиями, несколько раз пришлось обновлять конфиг при обновлении образа. Пару раз Collector падал без очевидной причины, journald помог найти panic в Go-стеке, но суть была в некорректном конфиге, который предыдущие версии прощали. Для production это потребует аккуратного подхода к обновлениям.
Что получили
Смена Jaeger на Zipkin для части сервисов заняла 20 минут - ровно столько, сколько нужно чтобы добавить Zipkin exporter в конфиг Collector-а и дождаться пересоздания pod-а. Никаких изменений в приложениях, никаких новых сборок образов.
Это и есть главное, что даёт Collector: приложение больше не знает о топологии наблюдаемости. Оно знает только адрес Collector-а рядом. Всё остальное - детали конфига агента.
Дополнительный плюс, который обнаружился по ходу: processors дают возможность обрабатывать данные по дороге. Можно добавлять атрибуты ко всем spans от конкретного receiver-а, фильтровать spans по имени операции, сэмплировать. Мы пока использовали только batch, но понятно что тут есть что поисследовать.
Где сейчас
Пилот работает на одном проекте, Collector обрабатывает трейсы от пяти сервисов. Jaeger и Zipkin получают данные параллельно - заказчик сравнивает UI и решает что оставить. Мы в это не вмешиваемся: наша задача выполнена на уровне инфраструктуры, дальше - их выбор.
Metrics pipeline пока не подключали - метрики по-прежнему идут напрямую в Prometheus через экспортеры. Collector умеет принимать метрики через prometheus receiver (scrape), но это отдельная задача, которая требует обдумать зачем нам ещё один hop в pipeline метрик если Prometheus и так умеет скрейпить напрямую.
Инструмент перспективный. Но внедрять его стоит осознанно - лишний компонент в pipeline наблюдаемости это и лишняя точка отказа.