Трендовые github проекты в нашем телеграм канале. Подпишись → Трассировка без правки кода: что делает агент внутри 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 + правка кода + настройка каждого сервиса» заменяется установкой на хост. Для инфраструктуры из сотен сервисов на разных языках это основной аргумент.
Где автоматика заканчивается
Агент не знает бизнес-смысла. «Рассчитать скоринг клиента» для него — цепочка вызовов методов. Самописные протоколы и нестандартные фреймворки тоже остаются без покрытия. Рабочая схема для таких мест:
- Включить автоинструментацию и собрать всё, что она даёт: HTTP, БД, очереди, контекст между сервисами.
- Стандартизировать формат через OpenTelemetry, чтобы span от разных источников попадали в одну трассу. Большинство APM-агентов умеют принимать OTel spans и склеивать их со своими данными.
- Вручную через SDK разметить только уникальные участки: бизнес-операции, атрибуты вроде
order.id, внутренние протоколы.
При выборе агента проверьте три вещи: как он переносит контекст через ваши очереди и пулы потоков, какой overhead и сэмплирование включены по умолчанию, и нужен ли рестарт для добавления новой точки сбора.