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

Jaeger в микросервисах: нашли узкое место за час, искали бы неделю

Подняли Jaeger-трейсинг в микросервисном приложении и за первые сутки обнаружили: 70% латентности - один синхронный вызов во внешний API.

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

Jaeger вступает в CNCF как graduated-проект в 2019, становясь частью стека observability рядом с Prometheus и Grafana

У нас на managed-проектах к этому лету уже нормально работал мониторинг: Prometheus собирает метрики, Grafana рисует дашборды, Alertmanager будит дежурного когда нужно. SLO по латентности поставлены, error budget считается. Казалось бы - observability закрыта. Оказалось, нет.

Когда в одном из микросервисных приложений начала периодически вырастать p95-латентность, метрики давали только ответ «что»: вот, latency выросла. Почему - нет. Сервисов около дюжины, между ними синхронные вызовы, есть внешние зависимости. Смотреть логи каждого сервиса по очереди - то ещё удовольствие.

Именно тут и нужен distributed tracing.

Почему Jaeger, а не Zipkin

Выбирали между Jaeger и Zipkin - оба реализуют OpenTracing, оба работают с инструментированием на Go, Python, Java. Zipkin старше, Jaeger появился внутри Uber в 2016-м и в 2017-м ушёл в CNCF. В этом году Jaeger должен получить статус graduated - CNCF рассматривает его как достаточно зрелый для продакшна.

Для нас решили два момента:

  • Нативная интеграция с Kubernetes. Jaeger Operator плюс всё стандартно деплоится через Helm, с CRD и ServiceAccount как положено.
  • Казуальный анализ через UI. Jaeger умеет показывать сравнение трейсов между собой - удобно, когда нужно понять «а что изменилось между медленным и быстрым запросом».

Zipkin тоже вполне рабочий инструмент, просто Jaeger на Kubernetes ощущается чуть органичнее.

Как поднимали

Деплоили в режиме all-in-one для начала - один под, всё в памяти, для продакшна не подходит, но для первичной отладки сойдёт. Полноценный стек с Elasticsearch как бэкендом для хранения трейсов - следующий шаг, когда убедились что всё работает.

Инструментирование приложений - самая трудоёмкая часть. Jaeger-клиент нужно добавить в каждый сервис, инициализировать tracer, проставлять span-ы вокруг HTTP-вызовов и обращений к БД. Для Go-сервисов использовали opentracing-go плюс jaeger-client-go. HTTP middleware для проброса контекста трейса через заголовки (uber-trace-id) - несколько строк кода, но без этого трейс будет обрываться на границе сервисов.

Контекст трейса нужно тащить через весь стек явно - в Go это context.Context в каждом вызове. Несколько сервисов написаны без нормального прокидывания контекста, там пришлось рефакторить. Не критично, но работа есть.

На Istio-кластере часть заголовков прокидывается автоматически через sidecar, но только на уровне HTTP - внутренние span-ы внутри сервиса всё равно нужно расставлять вручную.

Что показал первый день

Вот тут выяснилось то, ради чего всё затевалось.

Включили трейсинг, подождали пока накопятся данные по медленным запросам, открыли Jaeger UI и посмотрели на waterfall одного из тяжёлых трейсов. Картина была неожиданно прозрачная: основной запрос обрабатывается, из него уходит вызов во внешний API партнёра - и вот на этом вызове висит примерно 70% от всего времени обработки.

Сам по себе вызов был известен. Но никто не понимал, что именно он съедает такую долю латентности - на фоне всего остального он выглядел как «один из». Метрики Prometheus показывали средний response time сервиса целиком. Куда внутри уходит время - без трейсинга не видно.

Дальше оказалось ещё интереснее: внешний API отвечал быстро в 80% случаев, но в оставшихся 20% вдруг затормаживал до нескольких секунд. Это была проблема на стороне партнёра, а не у нас - но мы её принимали на себя как свою латентность, потому что вызов синхронный и блокирующий.

Решение: вынести вызов в асинхронный воркер с очередью, отдавать клиенту быстрый ответ, а результат от партнёра догружать отдельно. Без трейсинга мы бы продолжали смотреть в логи и перекладывать ответственность между командами.

Что дальше с инструментированием

Несколько вещей которые стали понятны после первого боевого применения:

  • Sampling надо настраивать сразу. По умолчанию Jaeger пишет все трейсы - при реальной нагрузке это быстро забьёт Elasticsearch. Начинали с 10% probabilistic sampling, для медленных запросов - adaptive.
  • Span-теги важны. Чем больше контекста в тегах (user_id, request_type, partner_id), тем быстрее находишь нужное в UI. Потом жалеть что не добавил сразу.
  • Ошибки в span-ах. Если внутри span упало исключение - нужно явно выставлять span.SetTag("error", true). Иначе в UI трейс выглядит зелёным, а внутри сломано.
  • Elasticsearch в продакшне. all-in-one режим - только для разработки. В продакшне нужен отдельный Elasticsearch-кластер под трейсы, иначе при рестарте Jaeger-пода все данные теряются.

Prometheus и Grafana дают ответ на «что происходит с системой в целом», Jaeger - на «где именно внутри одного запроса теряется время». Это разные вопросы, и оба нужны. То, что SLO по латентности мы уже считали, помогло быстро зафиксировать проблему. Но найти где копать - это уже трейсинг.

В следующих проектах инструментирование под tracing будем закладывать сразу при проектировании сервиса, а не добавлять потом.

Контакт

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

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