ADG Оставить заявку
Блог DevOps 5 мин чтения

OpenTelemetry и Jaeger: ищем узкое место там, где метрики молчат

Внедряем распределённый трейсинг с OpenTelemetry 1.12 в микросервисном приложении клиента. Как один trace нашёл то, что Prometheus не показывал месяцами.

Контекст момента

OpenTelemetry 1.8 - stable Tracing API для Go, Java, Python и .NET; готовая интеграция с Jaeger и Zipkin без вендорной привязки

Несколько месяцев у клиента висела жалоба на периодические «тормоза» в веб-приложении. Не всегда, не у всех, не воспроизводимо по требованию. Метрики в Prometheus показывали норму: CPU ровный, память в порядке, latency на уровне p99 - в рамках. Запросы в PostgreSQL - быстрые. Очередь в RabbitMQ - пустая. Всё хорошо, только у пользователей что-то лагает.

Это классический сценарий, при котором метрики бесполезны: они агрегируют, усредняют и прячут именно то, что нужно найти. Мы решили поставить распределённый трейсинг - и OpenTelemetry 1.8 вышел как раз вовремя.

Почему OpenTelemetry, а не что-то проще

Приложение - микросервисная архитектура: Go-сервис на входе, два Java-бэкенда, Python-воркер для фоновых задач. Предыдущие попытки инструментировать это добро заканчивались на вопросе «какой SDK брать». Jaeger-клиент для Go, OpenTracing для Java, что-то своё для Python - это три разных API, три разных способа передавать контекст через HTTP-заголовки, и гарантированный ад при первом же рефакторинге.

OpenTelemetry решает это на уровне спецификации: один стандарт propagation, vendor-agnostic экспортёры, и в версии 1.x Tracing API уже стабилен для всех четырёх языков, которые нам нужны. Можно инструментировать один раз и не думать, куда потом отправлять данные - хоть в Jaeger, хоть в Zipkin, хоть в облачный collector.

Как разворачивали

Схема стандартная: OpenTelemetry Collector в роли агрегатора, Jaeger как backend для хранения и UI. Jaeger поднял в Docker Compose рядом с остальной наблюдаемостью клиента - там уже жили Prometheus и Grafana.

[Go service] --OTLP--> [OTel Collector] ---> [Jaeger]
[Java backend x2]   /
[Python worker]   /

Collector нужен не строго обязательно - можно слать прямо в Jaeger через Jaeger exporter - но с Collector появляется возможность фильтровать, семплировать и перенаправлять без изменений кода. Выбрали head-based sampling с частотой 10% для обычного трафика и 100% для запросов с ошибками. Для поиска периодических тормозов это не идеально - нужная транзакция может попасть в 90% выброшенных - но на первое время сойдёт, потом перешли на tail-based sampling через Collector.

Инструментирование Go - через go.opentelemetry.io/otel и авто-инструментирование HTTP-middleware. Java - через Java Agent, который подключается без изменений кода вообще: -javaagent:opentelemetry-javaagent.jar в JVM-аргументы и несколько переменных окружения. Python - через opentelemetry-distro, там тоже есть команда opentelemetry-instrument для авто-инструментирования Flask и Celery.

На весь стек ушло около двух дней - большая часть времени на выяснение, какой именно header использует каждый сервис для передачи trace context, и почему Python-воркер упорно не подхватывал его из очереди RabbitMQ. Оказалось - нужна явная инъекция контекста в properties сообщения при публикации, авто-инструментирование Celery это не делает автоматически. Добавили вручную десяток строк.

Что нашли

Через три дня после включения трейсинга на продакшне картинка начала складываться.

Нашли классику, которую метрики не видят: один из Java-бэкендов при определённом сценарии делал N+1 запросов к базе. Не всегда - только когда в ответе приходил список пользователей с вложенными настройками. Если пользователей было пятеро - запросов 6. Если двадцать - 21 запрос. При стандартной нагрузке это размазывалось по времени и среднее latency не выбивалось из нормы. Но когда несколько таких запросов прилетало одновременно - всё вставало в очередь к пулу соединений PostgreSQL.

Как это выглядело в Jaeger. Один trace с двумя десятками span-ов, где первый span - входящий HTTP-запрос на Go-сервисе, а дальше видно, как один вызов Java-бэкенда порождает цепочку из 21 последовательного SQL-запроса каждый в 3-8ms. Итого 60-170ms только на базу там, где должен быть один запрос на 5-10ms. В метриках это теряется - средний latency SQL всё равно в норме, просто их много.

Без трейсинга мы бы смотрели на этот Prometheus ещё месяц.

Что поправили

Fix банальный: загрузка вложенных настроек через JOIN вместо отдельных запросов в цикле. Три часа работы Java-разработчика, одна строчка в HQL. После деплоя та же транзакция в трейсах стала выглядеть опрятно - один SQL-запрос вместо двадцати одного, latency упал в несколько раз на этом сценарии.

Где трейсинг не помог

Честно: не всё нашли трейсингом. Ещё одна периодическая проблема - всплески latency раз в несколько часов без видимой причины - трейсы показывают замедление, но не объясняют источник. Подозрение на GC-паузы в JVM, но это уже нужен другой инструментарий - JVM metrics и, возможно, async-profiler. Трейсинг показывает что медленно, но не всегда почему.

Общее ощущение

OpenTelemetry 1.8 - это версия, где перестаёшь чувствовать себя первопроходцем. Stable API, нормальная документация, авто-инструментирование для основных фреймворков работает. Jaeger как backend для небольшого стека - разумный выбор: быстро поднимается, UI понятен, запросы к трейсам не требуют изучения PromQL.

Инструмент оправдал ожидания - не как «серебряная пуля» для всех проблем, а как дополнение к метрикам там, где метрики слепы. Узкое место, которое мы не могли найти несколько месяцев, нашлось за три дня после включения трейсинга.

Мы ведём эту инфраструктуру в рамках managed-контракта - добавили OpenTelemetry Collector и Jaeger в стандартный стек наблюдаемости для всех микросервисных проектов.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.