Logo Craft Homelab Docs Нейросети Хостинг Обучение Контакты
Автоинструментация в APM: как агент находит, где запрос потерял 4 секунды Трендовые github проекты в нашем телеграм канале. Подпишись →
8 октября 2026 г.

Трассировка без правки кода: что делает агент внутри JVM, CLR, V8 и Go-бинарника

Запрос проходит через API Gateway, три микросервиса, Kafka, PostgreSQL и внешний платёжный API и отвечает за 4 секунды. Профилировщик на одном сервисе тут бесполезен: время могло уйти на сеть, блокировку потока, медленный SQL или ожидание в очереди. Distributed tracing отвечает на вопрос «где именно», а автоинструментация позволяет получить трассы, не расставляя tracing-код по всем сервисам вручную.

Trace, span и parent_span_id

Trace — полный маршрут одного запроса через систему. Он состоит из span: каждый span описывает одну операцию — HTTP-обработчик, SQL-запрос, отправку сообщения. Минимальный набор полей:

trace_id:        4bf92f3577b34da6...
span_id:         00f067aa0ba902b7
parent_span_id:  7a085853722dc6...
operation:       POST /api/orders
duration:        183 ms
status:          OK

Связи через parent_span_id собирают span в дерево выполнения. Сложность в том, что дерево охватывает несколько процессов, и Service B должен как-то узнать, что его текущий запрос — продолжение запроса из Service A.

Перенос контекста: traceparent

Стандартный механизм — W3C Trace Context. Service A создаёт trace ABC123 и span 111. При исходящем HTTP-вызове инструментация добавляет заголовок:

traceparent: 00-ABC123-111-01

В реальности trace-id занимает 32 hex-символа, span-id — 16. Service B извлекает контекст и создаёт дочерний span 222 с родителем 111:

Trace ABC123
└── Service A (span 111)
    └── Service B (span 222, parent 111)

Измерить длительность операции просто. Основная работа инструментации — не уронить контекст при переходе между компонентами.

Где трасса рвётся

  • Очереди (Kafka, RabbitMQ). Продюсер пишет traceparent в заголовки сообщения, консьюмер читает и создаёт связанный span. Если сообщение пролежало в топике несколько минут, связь между отправкой и обработкой всё равно сохраняется.
  • Пулы потоков. Задача создаётся в одном потоке, выполняется в другом. Инструментация оборачивает задачу так, чтобы текущий контекст переехал вместе с ней. Без этого span из worker-потока окажется в отдельной трассе.

Оба случая чаще всего ломаются при ручной разметке, и именно их закрывают готовые агенты.

Ручная разметка и её цена

С OpenTelemetry SDK на Java span создаётся явно:

Span span = tracer.spanBuilder("processOrder").startSpan();
try (Scope scope = span.makeCurrent()) {
    span.setAttribute("order.id", orderId);
    processOrder(orderId);
} finally {
    span.end();
}

Так получается полный контроль и бизнес-атрибуты вроде order.id. Но в крупном приложении таких точек тысячи, а кроме бизнес-логики нужно покрыть HTTP, JDBC, Kafka, Redis, gRPC, асинхронные операции, thread pools и внешние API. Наблюдаемость становится частью application code: её надо писать, тестировать и обновлять с каждым релизом, и что-то обязательно забудут.

OpenTelemetry — фактический стандарт для сбора и передачи телеметрии: единый формат и отсутствие привязки к вендору. Его агенты автоинструментации покрывают популярные библиотеки и фреймворки, бизнес-логику всё равно размечают руками. Анализом OpenTelemetry не занимается: корреляция с инфраструктурой, топология сервисов и поиск первопричины — задача отдельной платформы.

Как автоинструментация работает в разных рантаймах

Идея общая: агент знает, какие библиотеки использует приложение, и перехватывает их операции сам. Механизм перехвата зависит от языка.

Java: javaagent и JVMTI

Самый распространённый способ — bytecode instrumentation. В параметры запуска добавляется -javaagent:agent.jar, агент стартует вместе с приложением и переписывает байт-код загружаемых классов. Так работает OpenTelemetry Java agent. Ограничения у него такие:

  • классы модифицируются, и баг в инструментации может повлиять на само приложение;
  • вставленный код добавляет overhead, поэтому многие решения по умолчанию сэмплируют запросы;
  • агент видит только то, что сообщает вставленный им код: GC, аллокации и блокировки напрямую недоступны;
  • в ряде реализаций новый entry point или новый извлекаемый атрибут применяется только после рестарта.

Второй путь — нативный агент на JVMTI (JVM Tool Interface, есть с Java 5). Он подключается как нативная библиотека через -agentpath и получает callback-уведомления от самой JVM:

  • вход и выход из методов без модификации байт-кода (дорого, поэтому используется точечно);
  • создание, удаление и перемещение объектов сборщиком мусора;
  • начало и конец GC с деталями по поколениям;
  • загрузка классов, создание потоков, блокировки и мониторы;
  • исключения и изменения состояния VM.

Чтобы получить те же события через javaagent, пришлось бы вставлять код в каждый метод. Нативный агент сам решает, где хватит событий JVM, а где нужна вставка кода, и может добавлять точки сбора без рестарта.

.NET: CLR Profiling API

Аналог JVMTI — ICorProfiler. Агент подключается через переменные окружения при старте процесса, получает уведомления от CLR и переписывает IL-код методов во время JIT-компиляции. На этом построена и автоинструментация OpenTelemetry для .NET. Поверх трассировки агенты обычно добавляют always-on CPU-профилирование (горячие методы, узкие места в стеке) и метрики GC по поколениям Gen 0/1/2.

Node.js: AsyncLocalStorage

Главная проблема — асинхронность. Цепочка HTTP request → await DB → await Payment API → HTTP response проходит через event loop, и контекст трассы нужно протащить через промисы и колбэки. Для этого используются async_hooks и AsyncLocalStorage. Агенты дополнительно снимают CPU-сэмплы, задержку и загрузку event loop, heap-метрики и метрики worker threads, а также текст запросов к поддерживаемым БД.

Go: перехват в бинарнике

У Go нет виртуальной машины с интерфейсом для перехвата: код компилируется в машинный. Варианты — перехват вызовов функций в уже собранном бинарнике без перекомпиляции, eBPF-зонды (uprobes) или инструментация на этапе сборки; OpenTelemetry для Go использует два последних. Каждый входящий запрос в net/http порождает горутину, и агент помечает её и порождённые ею горутины как связанные с запросом, а остальные считает фоновыми. Отдельно собираются heap (включая stack и offheap), число объектов, вызовы GC и состояние очередей горутин. Сборщик данных при этом не должен добавлять неявных точек синхронизации между горутинами, иначе он сам станет источником задержек.

Автообнаружение процессов

Коммерческие APM-агенты ставятся один раз на хост и дальше работают по цепочке: обнаружить запущенный процесс → определить технологию → проверить правила исключений → подключить модуль для этого рантайма → перехватывать поддерживаемые операции. Модель «отдельный агент + SDK + правка кода + настройка каждого сервиса» заменяется установкой на хост. Для инфраструктуры из сотен сервисов на разных языках это основной аргумент.

Где автоматика заканчивается

Агент не знает бизнес-смысла. «Рассчитать скоринг клиента» для него — цепочка вызовов методов. Самописные протоколы и нестандартные фреймворки тоже остаются без покрытия. Рабочая схема для таких мест:

  1. Включить автоинструментацию и собрать всё, что она даёт: HTTP, БД, очереди, контекст между сервисами.
  2. Стандартизировать формат через OpenTelemetry, чтобы span от разных источников попадали в одну трассу. Большинство APM-агентов умеют принимать OTel spans и склеивать их со своими данными.
  3. Вручную через SDK разметить только уникальные участки: бизнес-операции, атрибуты вроде order.id, внутренние протоколы.

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