Это (длинная) история о том, как мы внедряли OpenTelemetry в Норвежском управлении труда и социального обеспечения (NAV). Мы рассмотрим путь от первых коммитов до реального применения в production. А ещё расскажем о некоторых трудностях, с которыми столкнулись, и о том, как их преодолели.

В NAV используется микросервисная архитектура с тысячами сервисов, работающих в кластерах Kubernetes. С самого начала мы убеждали команды перейти на Prometheus и Grafana. Увы, но они до сих пор в значительной степени полагаются на логи приложений, изучая их в Kibana.

Без надлежащего трейсинга трудно понять, как запросы проходят через систему. Это затрудняет устранение ошибок в длинных и зачастую сложных цепочках обработки данных или оптимизацию медленных запросов. Для наших команд, которые внедрили event-driven архитектуру на базе Kafka, этот вызов стал особо острым. Это всё равно что пытаться ориентироваться в городе без карты.

За последние годы мы предприняли несколько попыток реализовать ту или иную форму отслеживания запросов с помощью HTTP-заголовков. Вот только некоторые из них: Nav-Callid, nav-call-id, callId, X-Correlation-ID, x_correlationId, correlationId, x_correlation-id и даже x_korrelasjonsId (норвежский вариант correlationId). Список наверняка неполный: у меня слишком мало времени, чтобы найти все.

Создаётся впечатление, что мы застряли в бесконечном цикле попыток договориться о стандарте, а затем попытаться заставить всех правильно его применять... Вот тут-то на помощь и приходит OpenTelemetry! Она обеспечивает стандартный способ извлечения данных телеметрии из приложений и предоставляет библиотеки и SDK для всех распространённых языков программирования (есть даже инициативы по поддержке мэйнфреймов), тем самым сильно упрощая внедрение. Так что не всё потеряно!

Первые шаги

Хотя OpenTelemetry автоматизирует основную часть работы, это всё равно сложная система со множеством компонентов. Знаете ли вы, что OpenTelemetry — самый быстрорастущий проект в Cloud Native Computing Foundation (CNCF)? Интерес к нему даже выше, чем у Kubernetes в его первые годы.

Чтобы начать работу с OpenTelemetry, нужны две вещи:

  1. Место для хранения и визуализации данных телеметрии.

  2. Убедить разработчиков в том, что адаптация приложений стоит затраченного времени.

Звучит не слишком сложно, правда? Начнём с бэкенда.

OpenTelemetry — вендор-нейтральный проект, поэтому для хранения данных можно выбрать любой бэкенд. Самые популярные — Jaeger, Zipkin и Tempo. Мы остановились на Grafana Tempo, потому что это масштабируемое, экономически эффективное решение с открытым исходным кодом, которое легко интегрируется с Grafana (а её мы уже используем для метрик и дашбордов).

Мы подробно рассказали о том, как начать работу с Grafana Tempo, в документации NAIS, а также в справочном руководстве по TraceQL — языку запросов, который используется в Tempo.

Данные OpenTelemetry можно отправлять непосредственно в Tempo, но рекомендуется использовать OpenTelemetry Collector. Collector может получать данные из различных источников, обрабатывать их и отправлять по разным адресатам. То есть можно добавлять новые источники или адресаты, не меняя конфигурацию приложения.

Установка подобных вещей в кластерах Kubernetes — это то, чем команда NAIS занимается добрую часть последней декады, поэтому мы без проблем настроили Collector и подключили его к Tempo в кластерах. Для каждого окружения (dev и prod) запускается по одному инстансу Tempo; все они подключены к одной Grafana.

Перейдём ко второму пункту. Самое сложное — это заставить разработчиков адаптировать свои приложения.

Инструментирование приложений

С самого начала мы знали, что для успеха всего предприятия необходимо донести до разработчиков ценность и преимущества хорошей наблюдаемости, а также сделать процесс адаптации приложений под неё максимально простым. Учитывая, что в production у нас более 1 600 приложений, тратить недели и месяцы на каждое из них — не вариант. Решение, требующее ручной настройки приложений, было неприемлемо.

Поскольку большинство наших внутренних сервисов написаны на Kotlin и Java, мы начали с тестирования Java-агента OpenTelemetry. Он запускается рядом с приложением и меняет байткод по мере его загрузки в JVM. То есть можно автоматически инструментировать приложение, не внося изменений в исходный код.

К нашему приятному удивлению, агент работал без проблем с большинством приложений. Он правильно соотносил входящие и исходящие запросы, понимал различные фреймворки и даже перехватывал запросы к базам данных и асинхронные вызовы очередей сообщений типа Kafka, хотя скептики утверждали, что это всё реклама и не будет работать так хорошо, как ожидается. Фактически, Java-агент OpenTelemetry поддерживает более 100 различных библиотек и фреймворков из коробки!

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

Именно здесь на помощь приходит OpenTelemetry Operator. Этот оператор Kubernetes позволяет автоматически внедрять Java-агент OpenTelemetry (а также агенты для других языков программирования) непосредственно в под. Также он может настраивать агента на отправку данных в нужный коллектор, задавать правильное имя сервиса и переменные окружения для каждого приложения, поскольку у него есть доступ к Kubernetes API.

Собираем всё вместе

У NAV есть платформа приложений с открытым исходным кодом под названием NAIS, которая предоставляет всё, что нужно нашим командам для разработки, запуска и работы приложений. Её основной компонент — naiserator (оператор Kubernetes) и файл nais.yaml, определяющий, как приложение будет запускаться в кластере Kubernetes.

Базовый манифест приложения выглядит примерно так:

apiVersion: "nais.io/v1alpha1"
kind: "Application"
metadata:
  name: "my-application"
  namespace: "my-team"
spec:
  image: "navikt/my-application:abc123"
  replicas: 2
  ...

Это очень мощная абстракция, которая позволила нам добавлять новые функции в платформу с минимальными усилиями со стороны разработчиков. В nais.yaml было добавлено новое поле под названием observability, которое позволяет разработчикам включить трейсинг приложений с помощью всего четырёх строк в YAML-конфигурации:

...
  observability:
    autoInstrument:
      enabled: true
      runtime: "java"

Когда naiserator видит это поле, он устанавливает необходимые аннотации OpenTelemetry Operator, чтобы получить правильную конфигурацию OpenTelemetry и агента в соответствии с используемым рантаймом. В настоящее время у нас поддерживается автоинструментирование Java, Node.js и Python. Разработчикам не нужно думать о том, как настроить трейсинг в приложениях — достаточно включить его в манифесте. Для нас это огромная победа!

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

Мы также включили трейсинг в ингресс-контроллере, чтобы отслеживать полный путь запроса от клиента до внутреннего сервиса. Поскольку используется ингресс-контроллер Nginx, достаточно было включить конфигурацию OpenTelemetry в Helm-чарте, чтобы получить трейсинг всех входящих запросов.

Внедряем или нет?

После запуска функции автоинструментирования мы получили кучу положительных отзывов от разработчиков. Они были рады увидеть трейсинг в Grafana и Tempo, и наконец-то смогли получить хорошее представление о том, через какие сервисы проходит запрос. Мы даже стали свидетелями того, как несколько команд использовали трейсинг для устранения ошибок и оптимизации медленных запросов.

Невероятно круто видеть полный путь запроса от клиента до внутреннего сервиса в Grafana Tempo с помощью Grafana Faro Web SDK. Такого откровения у нас ещё не было! Оно меняет правила игры для наших разработчиков — особенно для тех, кто работает над фронтендом.

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

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

Путь к успеху вымощен трудностями

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

Шумные спаны трейсинга

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

Решение было очевидным — отфильтровать шум. Мы добавили фильтр в OpenTelemetry Collector, который отбрасывает трейсы для определённых путей или кодов состояния. Это значительно сократило шум и облегчило поиск нужных трейсов.

Это распространённая проблема, о чём свидетельствуют комментарии в opentelemetry-java-instrumentation#1060. Там пользователи просили реализовать возможность фильтровать определённые спаны.

Хотя существуют и более продвинутые способы убрать шум, например, с помощью tail sampling (это когда решение о выборке трейса принимается после завершения спанов в запросе — прим. ред.), мы пришли к выводу, что для наших нужд достаточно простого фильтра. Единственным недостатком является вероятность получить потерянные спаны, которые не являются частью трейса.

Фильтр задаётся в общем коллекторе OpenTelemetry Collector. Да, мы знаем, что в долгосрочной перспективе это не самое масштабируемое решение. В будущем мы планируем сделать так, чтобы разработчикам было легче определять свои собственные фильтры.

filter/drop_noisy_trace_urls:
  error_mode: ignore
  traces:
    span:
      - |
        (attributes["http.method"] == "GET" or attributes["http.request.method"] == "GET") and (
          attributes["http.route"] == "/favicon.ico" or attributes["http.target"] == "/favicon.ico" or attributes["url.path"] == "/favicon.ico"
          or IsMatch(attributes["http.route"], ".*[iI]s_?[rR]eady")    or IsMatch(attributes["http.target"], ".*[iI]s_?[rR]eady")       or IsMatch(attributes["url.path"], ".*[iI]s[rR]eady")
          or IsMatch(attributes["http.route"], ".*[iI]s_?[aA]live")    or IsMatch(attributes["http.target"], ".*[iI]s_?[aA]live")       or IsMatch(attributes["url.path"], ".*[iI]s[aA]live")
          or IsMatch(attributes["http.route"], ".*prometheus")         or IsMatch(attributes["http.target"], ".*prometheus")          or IsMatch(attributes["url.path"], ".*prometheus")
          or IsMatch(attributes["http.route"], ".*metrics")            or IsMatch(attributes["http.target"], ".*metrics")             or IsMatch(attributes["url.path"], ".*metrics")
          or IsMatch(attributes["http.route"], ".*actuator.*")         or IsMatch(attributes["http.target"], ".*actuator.*")          or IsMatch(attributes["url.path"], ".*actuator.*")
          or IsMatch(attributes["http.route"], ".*internal/health.*")  or IsMatch(attributes["http.target"], ".*internal/health.*")   or IsMatch(attributes["url.path"], ".*internal/health.*")
          or IsMatch(attributes["http.route"], ".*internal/status.*")  or IsMatch(attributes["http.target"], ".*internal/status.*")   or IsMatch(attributes["url.path"], ".*internal/status.*")
        )    

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

Rapids и Rivers — как не погибнуть под лавиной спанов

Мы перешли на событийно-ориентированную архитектуру на базе Kafka, которая является основой для многих наших сервисов. Некоторые даже взяли на вооружение схему Rapids, Rivers and Ponds Фреда Джорджа, в которой все сервисы подписываются на все события и отфильтровывают те, которые им действительно нужны. Это затрудняет отслеживание запроса в системе, поскольку он может проходить через, казалось бы, бесконечное количество сервисов.

Основная проблема, с которой мы столкнулись, заключается в том, что по умолчанию в Grafana Tempo установлено ограничение на размер трейса. Нам пришлось увеличить его до 40 МБ, чтобы видеть полный трейс для некоторых запросов (и даже этого иногда не хватало). Это непростая проблема, над решением которой мы всё ещё работаем.

Из общения с сообществом OpenTelmetry следует, что одно из возможных решений — использовать span-ссылки. Увы, мы не уверены, насколько хорошо это будет работать на практике, и реально ли их полноценно визуализировать в Grafana Tempo (см. grafana/tempo#63531).

Приложения на Node.js — боремся с ошибками

Хотя в NAV работают преимущественно на Java и Kotlin, у нас есть несколько Node.js-приложений в production, в основном Next.js и Express.

Во-первых, Next.js обещает поддержку OpenTelemetry из коробки, однако мы обнаружили, что она не работает в автономном режиме (см. vercel/next.js#49897).

Поэтому и здесь нам пришлось прибегнуть к помощи автоинструментирования. Но из-за того, как изолируются контейнеры в Kubernetes, возникло несколько странных ошибок при инструментировании приложения: принадлежность (ownership) файлов в контейнере не сохранялась. Эту проблему необходимо было исправить, прежде чем двигаться дальше (см. opentelemetry-operator#2655).

В итоге нам удалось запустить приложения Node.js с OpenTelemetry. Однако оказалось, что некоторые исходящие запросы от приложений Node.js не трассируются, поскольку не сохраняется заголовок traceparent. Пришлось вручную добавлять заголовок traceparent в исходящие запросы следующим образом:

const response = await fetch(someUrl, {
  headers: {
    get traceparent() {
      return getTraceparentHeader();
    },
    'Content-Type': 'application/json'
  },
  ...
});

Поддержка Node.js Fetch (API для выполнения HTTP-запросов; был добавлен в версию 18 Node.js) появилась в auto-instrumentations-node-v0.45.0 после добавления пакета @opentelemetry/instrumentation-undici.

Лишние логи — проблема с конфиденциальной информацией

Одно из главных преимуществ агента — корреляция логов и трейсов. Агент поддерживает различные библиотеки логирования (log4j, logback и slf4j) и может автоматически добавлять информацию о трейсе и отправлять её в Grafana Loki.

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

Теперь перехват логов необходимо явно включить для каждого приложения, добавив следующую переменную окружения в соответствующее развёртывание:

spec:
  env:
    - name: OTEL_LOGS_EXPORTER
      value: otlp

Мы связались с сообществом OpenTelemetry, чтобы узнать, есть ли способ перехватывать только те логи, которые предназначены для stdout/stderr, но на данный момент это единственный способ отключить функцию перехвата логов.

Будущее OpenTelemetry в NAV

За короткое время мы проделали большой путь, но нам ещё многое предстоит сделать. OpenTelemetry — мощный инструмент, но не панацея. Он требует больших усилий, и потребуется время, чтобы научить разработчиков эффективно им пользоваться.

Дашборды и панели по умолчанию

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

Пока это самое сложное в работе с Grafana. С ней можно делать что угодно, но требуется приложить соответствующие усилия. Может быть, Library Panels в Grafana помогут нам в этом?

Метрики на основе спанов

Спан-метрики (Span Metrics) — функция Grafana Tempo, которая позволяет генерировать метрики из полученных данных трейсинга, включая метрики запросов, ошибок и длительности (RED). Мы считаем, что спан-метрики снижают сложность использования exemplar'ов (exemplar — это метаданные, добавляемые к метрике, которые позволяют, в том числе, связать метрики, логи и трейсы. Например, для метрики http_request_duration_seconds exemplar может представлять конкретный HTTP-запрос. Пример — на сайте наблюдается всплеск трафика. У основной массы пользователей сайт грузится менее 2 секунд, но есть и те, кому приходится ждать. Чтобы определить факторы, влияющие на задержку, необходимо сравнить трейсинг быстрого отклика с трейсингом медленного отклика. Учитывая огромный объем данных в типичной production-среде, это сделать непросто. Exemplar'ы помогают изолировать проблемы в распределении данных, указывая на трайсы запросов с высокими задержками. — прим. ред., см. документацию Grafana). Exemplar — это конкретный представитель трейса для измерения, проведённого в заданном временном промежутке.

Перед включением span-метрик нужно убедиться, что инстанс Prometheus способен выдержать дополнительную нагрузку. Может быть, Grafana Mimir поможет нам в этом?

Корреляция с логами и метриками

Есть потенциал у коррелирования логов и трейсов, но пока этот подход не получил широкого распространения. Логи собираются со stdout/stderr, а метрики собираются Prometheus, что затрудняет корреляцию логов и метрик с трейсами, поскольку они обрабатываются независимо друг от друга.

Вместо этого можно отправлять трейсы, логи и метрики через OpenTelemetry SDK и OpenTelemetry Collector, что позволит автоматически коррелировать их, получив все преимущества OpenTelemetry.

Тренинги и семинары-практикумы

Мы начали планировать тренинги и семинары для разработчиков, чтобы научить их эффективно использовать OpenTelemetry. Еще мы создали сайт с руководствами, туториалами и примерами, которые помогут разработчикам познакомиться с OpenTelemetry.

Позже в этом году мы проведём первый Public Sector Observability Day, на который пригласим разработчиков из норвежского госсектора. Планируем рассказать им о наблюдаемости и о том, как эффективно использовать OpenTelemetry.

Мы с большим воодушевлением смотрим на будущее OpenTelemetry в NAV, и нам не терпится увидеть, какие прикладные задачи будут решать разработчики с помощью этой технологии. Как видно из приведённого ниже графика, количество трейсов неуклонно растёт, и мы убеждены: это только начало!

P. S. 

Читайте также в нашем блоге:

Комментарии (4)


  1. csl
    30.09.2024 08:05
    +3

    Ссылка для удобства на комикс номер 927 из начала поста


  1. ExAnimo
    30.09.2024 08:05

    Спасибо за статью!

    Подскажите, а почему вы выбрали otel-collector, а не Grafana Alloy? Последний словно бы напрашивается в окружении Tempo, Grafana, Faro.


    1. vemcaster
      30.09.2024 08:05

      Возможная причина: Grafana Alloy как продукт ещё не был production ready на момент начала внедрения Open Telemetry в NAV.

      1. Внедрение в NAV начали раньше 28 января 2024-ого года, если ориентироваться на даты в Pull Request с внедрением OpenTelemetry одного из их проектов.

      2. Grafana Alloy была анонсирована 9-го апреля 2024-ого года на GrafanaCON 2024.


      1. ExAnimo
        30.09.2024 08:05

        Хм, на даты как-то даже не обратил внимание... Спасибо большое!