golang

Сбор и отправка APM-трейсов из разных сервисов: как мы приручили трейсинг в монолитах

  • среда, 12 ноября 2025 г. в 00:00:14
https://habr.com/ru/articles/965144/

Всем привет! Меня зовут Яна Курышева, и я тимлид одной из команд разработки бэкенда в Спортсе’’.

Мы – спортивное медиа. Наш продукт – это сайт и приложения со спортивной статистикой, новостями, редакционным и пользовательским контентом, пушами, рекомендациями и комментариями. 

За 25+ лет развития архитектура Спортса’’ стала достаточно разнообразной под капотом: десятки микросервисов на Go соседствуют с монолитными Perl- и PHP-приложениями, которые мы планомерно переводим на новый стек.

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

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

Коротко об APM-трейсинге

Elastic APM – это инструмент для мониторинга производительности всей системы. Он позволяет получить множество полезных сведений: о времени выполнения и частотности запросов между сервисами, о запросах в БД и внешние API, о статистике ошибок и др. 

Все эти данные агрегируются и предоставляются в удобном интерфейсе, что позволяет находить проблемы с производительностью. А после проведенных оптимизаций собирать пруфы в виде красивых графиков «до» и «после» :)

Стек ELK состоит из нескольких частей:

  1. APM Agent – агент, встроенный в ваше приложение (библиотека для конкретного языка: Java, Python, Node.js, Go, .NET и др.). Он собирает данные по трейсам и отправляет их на APM-сервер.

  2. APM Server – отдельный сервис, который принимает данные от агентов и передает их в Elasticsearch.

  3. Elasticsearch – хранилище данных, где сохраняются все собранные трейсы.

  4. Kibana APM UI – визуальный интерфейс.

Зачем нам понадобилось свое решение?

На официальном сайте Elastic APM приведен перечень языков, для которых существуют готовые APM-агенты: Go, Python, PHP и др. Если заглянуть в документацию и репозитории агентов, то можно заметить, что их реализация и интеграция сильно отличаются. 

Посмотрим чуть детальнее на особенности.

Для языка Go официальный агент – это библиотека (модуль) для приложения, которая импортируется и подключается в коде, например, на уровне middleware. Зачастую требуется использование специальных оберток из модулей для разных инструментов или реализация своих кастомных врапперов.

Ключевая особенность архитектуры агента – механизм накопления и отправки событий, что сильно отличает его от агентов, написанных на интерпретируемых языках вроде PHP или Python.

Механизм APM-агента на языке Go
Механизм APM-агента на языке Go

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

Если говорить про метрики по работе самого агента, то Go-агент не документирует эти метрики как «официальные». Но на самом деле он отслеживает несколько показателей, полезных для понимания эффективности работы агента.

Например:

  • размер очереди буфера событий, ожидающих отправки;

  • количество событий, отброшенных из-за переполнения;

  • количество отправленных спанов, транзакций.

PHP-агент устроен иначе: его модель – «один запрос = одна отправка». Из-за идеологии самого языка он не может поддерживать постоянные очереди, фоновые потоки и делать батчинг. Агент инициализируется и выгружается в рамках одного запроса, поэтому и сбор событий возможен только в этот момент. В итоге PHP-агент генерирует в десятки раз больше сетевых запросов по сравнению с Go-агентом, создавая дополнительную нагрузку на инфраструктуру. APM-серверу приходится принимать тысячи мелких запросов, распаковывать и обрабатывать каждый из них, а также поддерживать множество открытых TCP-соединений – все это снижает его производительность.

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

С Perl-агентом ситуация еще печальнее – его нет, а разработка даже не входит в планы. Для нас трейсинг из монолита на Perl был критически важен, так как на него все еще поступает заметное количество трафика. Однако специфика языка намекает, что даже самописное решение приведет нас к тем же проблемам, что и в PHP.

Что ж делать, что ж делать… Подумали мы и пришли к таким вариантам:

  1. Смириться;

  2. Написать кастомное решение для Perl и принести в PHP существующий агент;

  3. Придумать что-то универсальное.

В наших монолитных приложениях изначально мы решили пойти вторым путем: для приложения на Perl реализовали кастомный агент с полным циклом – создание пейлоадов и отправка напрямую в APM-сервер. Для PHP использовали существующий агент, добавив недостающую логику. Вдохновившись Go-агентом, сделали фоновый сбор и отправку событий на обоих языках – Perl и PHP. 

Агенты работали – мы смогли получать трейсы из обоих монолитов. Но у Perl- и PHP-агентов были значительные ограничения:

  1. Отсутствие внутренних метрик мешало качественно анализировать эффективность агентов и взаимодействие с APM-сервером.

  2. Ограниченность удобных нативных инструментов для оптимизаций.

  3. При появлении различных проблем: потеря трейсов, чрезмерная нагрузка на APM-сервер, переполнение агентов памяти и др. – приходилось исследовать каждый агент по отдельности.

  4. Поддержка и развитие агентов требовали больше ресурсов разработки, чем универсальное решение на более современном языке.

Так появилась идея APM-прокси – сервиса, который возьмет на себя всю механику: будет централизовано собирать, обрабатывать и отправлять запросы в  APM-сервер.

Проектируем APM-прокси

Критерии

Мы сформулировали для себя главные критерии, которые должны быть соблюдены при проектировании нового решения:

  1. APM-прокси должен быть универсальным для использования в любых сервисах, независимо от языка.

  2. Клиент (монолит) не должен ждать обработки трейса, отправки на APM-сервер.

  3. Должна быть возможность гибкой настройки прокси, чтобы APM-сервер выдержал поток запросов.

  4. Нужна возможность получать метрики по работе прокси – количество отправок в разрезе по источникам, размер буфера, частотность ошибок от сервера и другие показатели.

Архитектура решения. Сбор и обработка сообщений в фоновом режиме

Мы пришли к выводу, что прокси должен быть отдельным сервисом с HTTP-интерфейсом. Это позволит использовать его со старыми монолитами, где внедрять новые технологии (например, GRPC) сложнее. Для реализации выбрали Go, так как он уже является базовым языком нашей архитектуры и предоставляет много полезных инструментов.

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

Посмотрим на архитектуру такого решения.

На вход ожидаются сообщения в виде NDJSON (Newline Delimited JSON) – формат, где каждый объект JSON находится на отдельной строке. Этот формат принимает сам APM-сервер, поэтому и в APM-прокси было удобно выбрать его.

Ниже пример простого ndjson-сообщения, которое должен отправлять клиент.

// Метадата с информацией о сервисе, окружении, процессе
{
  "metadata": {
    "service": {
      "name": "my-service",
      "environment": "production"
    },
    "process": {
      "pid": 1234
    },
    "system": {
      "hostname": "my-host"
    }
  }
}
// Информация о транзакции - какой был запрос, сколько он обрабатывался
// Какие внутри были действия по работе с базой или внешними API
{
  "transaction": {
    "id": "transaction-id-1",
    "trace_id": "trace-id-1",
    "name": "GET /posts",
    "type": "request",
    "duration": 123.45,
    "timestamp": 1678886400000000,
    "context": {
      "request": {
        "method": "GET",
        "url": {
          "full": "http://example.com/posts"
        }
      }
    },
    "spans": [
      {
        "id": "span-id-1",
        "parent_id": "transaction-id-1",
        "name": "SELECT FROM posts",
        "type": "db",
        "duration": 50,
        "timestamp": 1678886400050000
      }
    ]
  }
}

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

Тело запроса вычитывается прокси, проверяется размер, затем сообщение перекладывается в отдельный канал. Клиент сразу получает ответ 200 – ему не нужно ждать завершения обработки. 

В это время в фоновом режиме работает воркер: он слушает канал, считывает новые сообщения и отправляет их на APM-сервер.

Схема работы агента с фоновой обработкой
Схема работы APM-прокси с фоновой обработкой

Воркер работает в горутине, это менее ресурсозатратно, чем полноценный отдельный процесс. Это дает нам гибкость в масштабировании: при добавлении нового клиента становится больше сообщений, и мы можем добавить и 100, и 200, и 1000 воркеров на обработку сообщений. Здесь нас в основном ограничивают лишь ресурсы, выделенные на сервис.

Наш прокси мы решили назвать просто – apm-sender.

Применяем Circuit Breaker

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

Здесь на помощь приходит паттерн Circuit Breaker. Принцип работы прост: если сервис выдает ошибки, запросы к нему временно блокируются, чтобы сохранить работоспособность системы. Через заданный интервал «прощупываем» сервис, и если он стабилен, запросы в него возобновляются. Подробнее о паттерне можно прочитать по ссылке.

Основное преимущество для нас в применении этого паттерна – APM-сервер не перегружается лишними запросами, если ему и так плохо. Кроме того, мы хотели избежать потери трейсов во время проблем на стороне APM-сервера – ведь клиенты продолжают отправлять данные, даже если сервер временно не принимает запросы.

Перейдем к реализации:

  • Добавляется второй уровень воркеров с собственным каналом – channel 2. Он станет «накопителем» сообщений, а воркеры возьмут на себя отправку сообщений в APM.

  • Первые воркеры становятся транспортом между каналами, и первый канал channel 1 становится доступен для записи при обработке сообщений от клиентов всегда.

Схема работы агента с двумя уровнями воркеров
Схема работы APM-прокси – с двумя уровнями воркеров

С помощью второго уровня воркеров реализуем паттерн Circuit Breaker.

  • При получении ошибки (timeout, unavailable и т.д.) от APM-сервера ставим таймер на несколько секунд и блокируем второй уровень воркеров-отправщиков, давая возможность APM-серверу восстановить работу. 

  • В это время в channel 2 начнут копиться сообщения, пока работает блокировка. Время подбирается в зависимости от того, сколько ресурсов у вас выделено на накопление и как быстро в среднем оживает APM-сервер.

  • Необходим контроль ресурсов с помощью размера буфера: при добавлении сообщений в channel 2 проверяем, заполнился ли буфер. Если да – значит мы накапливаем слишком долго и сознательно новые события в канал не добавляем, избегаем переполнения по памяти.

  • После истечения таймера сигнализируем воркерам, что можно пробовать снова, и если APM-сервер отвечает ошибкой – опять ставим таймер, блокируемся и повторяем цикл.

Блокировка отправки с таймером
Блокировка отправки с таймером

Мы добавили метрики на разных этапах обработки трейсов: объем входящих и исходящих пейлоадов по клиентам, время блокировки, размер буфера (полезно при накоплении) и другие показатели. Тут у нас свобода творчества.

Переключение клиентов

Разработанный прокси принимает на вход уже готовый пейлоад с информацией о трейсе, поэтому создание транзакций при обработке запросов и формирование ndjson-пейлоада остается на стороне клиентов.

Так как на стороне клиентов уже была реализована отправка на APM-сервер, мы просто повторили его HTTP-эндпоинт в нашем APM-прокси – для переключения нужно было лишь изменить адрес отправки.

Переключение клиентов на новый APM-агент
Переключение клиентов на APM-прокси

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

Метрики и результаты

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

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

  • На размер пейлоада влияет количество спанов в транзакции = глубина трейса. Важно контролировать объем создаваемых спанов и не перебарщивать, иначе рискуете создавать слишком большие пейлоады, которые будут обрабатываться APM-сервером медленнее. У нас, например, нашлись клиенты, которые отправляли гигантские пейлоады, так как туда помещалась избыточная информация. Большой поток таких запросов сильно нагружал APM-сервер, о чем мы не догадывались ранее.

  • На основе этих данных удалось подобрать оптимальные значения для количества воркеров и объема буфера канала, при которых мы утилизируем приемлемое количество ресурсов и можем хранить сообщения в буфере.

  • С помощью прокси мы смогли увидеть реальную нагрузку на APM-сервер по каждому из клиентов, что позволило нам наилучшим образом сконфигурировать сам APM-сервер.

  • Стоит упомянуть про реализованное накопление сообщений: если накапливать слишком много, то после возобновления работы APM-сервера в него полетит сильно больше трафика, чем обычно, к чему он может быть не готов. Поэтому настройка APM-прокси тесно связана с настройкой APM-сервера.

Прямо сейчас

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

Мы имеем возможность конфигурировать наш APM-прокси – настраивать количество воркеров, увеличивать или уменьшать накопление сообщений, идентифицировать источник слишком большого количества данных.

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

Если вы тоже живете с монолитами – возможно, наш опыт вдохновит вас не бояться внедрять туда новые инструменты 😉