openjdk.ruOpenJDK на русском

JEP draft: provide stable USDT probe points on JVM compiled methods

стабильные точки зондирования USDT в скомпилированных методах JVM

ОтветственныйJohn Rose
ТипFeature
ОбластьJDK
СтатусDraft
Компонентhotspot / compiler
Создан2017/05/05 01:06
Обновлён2021/11/22 09:05
Задача8179657

FIXME: ДОБАВИТЬ СТРУКТУРУ JEP

Точки зондирования в пользовательском коде, допускающие патчинг, становятся всё более важной «точкой подключения» (hook) для онлайн-отладки и телеметрии в больших масштабах в облачных средах. Стандартные механизмы, такие как USDT, позволяют компиляторам вставлять точки зондирования на входе в метод и на выходе из него, а онлайн-инструменты (dtrace, BPF) могут их обнаруживать и изменять. Согласование между компиляторами и инструментами часто строится на специальных секциях или других метаданных в ELF-файлах, которые являются статическими артефактами (отсюда буква S в USDT).

Сейчас JVM не взаимодействует с этими технологиями, но может и должна.

Проблема с JVM в основном в том, что она динамически генерирует горячий скомпилированный код, а значит, у инструментов нет статически сгенерированного ELF-файла, к которому можно обратиться. Кроме того, JVM часто и без ограничений выполняет собственный патчинг генерируемого ею кода, поэтому, даже если внешний инструмент патчинга сможет найти адрес скомпилированного метода JVM, установка внешнего патча на любую инструкцию гарантированно приведёт к серьёзному сбою. Чтобы JVM могла взаимодействовать с инструментами патчинга инструкций, она должна управлять всем патчингом скомпилированных методов. Чтобы внешние инструменты могли выполнять свою работу, JVM должна уметь создавать (по запросу) точки патчинга, которые внешние инструменты могут безопасно патчить.

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

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

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

Минимальный набор точек подключения, требуемый от JVM, выглядит примерно так:

  • Понятие «точки патчинга» — набора инструкций, предоставленного инструментом (часто это одна инструкция „nop“), через который проходит управление, когда происходит определённое событие.
  • API для создания (на лету) точек патчинга для соответствующих событий: загрузка скомпилированного метода, вход в метод, выход из фрейма, выброс исключения из фрейма, деоптимизация фрейма, OSR фрейма (по одной для каждого отдельного скомпилированного метода)
  • API обнаружения для поиска потенциальных и существующих точек патчинга.
  • API точек патчинга для безопасного подключения, отключения, включения, выключения и удаления точек патчинга или групп точек патчинга.
  • Вспомогательные функции (доступные для кода патчей) для разбора соответствующих структур JVM (фреймов, объектов, блоков кода и т. д.)

Два типичных события — «был вызван метод Foo::bar» или «был вызван метод Foo::baz». Обратите внимание, что это независимые события с отдельными точками патчинга. Точки патчинга создаются не заранее, а только по запросу внешнего инструмента. Типы событий будут включать все виды входов в блоки скомпилированного кода JVM и выходов из них. События в интерпретаторе покрываются уже существующими API, а не точками патчинга скомпилированных методов.

Этот JEP не пытается работать с методами Java как таковыми, а только с крупными составными «блоками» кода, называемыми скомпилированными методами (class CompiledMethod), которые находятся в кэше кода и отвечают за быстрое выполнение в больших масштабах. Уже существуют средства трассировки более высокого уровня, которые позволяют пользователям устанавливать точки останова и точки трассировки на конкретные методы и строки исходного кода. Эти приёмы не всегда применимы к оптимизированным скомпилированным методам, потому что первым делом они отбрасывают методы и перекомпилируют их с отключёнными оптимизациями и вставленными специальными вызовами. Для отладки отдельного процесса это приемлемо, но для трассировки больших облачных нагрузок слишком разрушительно.

Накладные расходы, связанные с USDT, сопоставимы с вызовами подпрограмм и быстрыми прерываниями (traps), и, как правило, расходуются небольшими порциями и только тогда, когда трассировка действительно используется. В JVM пока нет механизма трассировки скомпилированных методов, который соответствовал бы такому профилю.

Реализовать это относительно просто при определённых усилиях и тесном сотрудничестве нужных сторон. Форму скомпилированного кода почти или совсем не нужно менять, поскольку он уже допускает патчинг. (Например, деоптимизация метода требует патча точки входа.) API USDT придётся немного «прогнуться», чтобы учесть необычные условия внутри JVM. JVM придётся выделить части кода под точки патчинга и настроить временные дополнительные передачи управления между точками входа в методы и точками патчинга.

Существует вариант USDT под названием «dynamic UDST», который используется для инструментирования методов в Node.js. Вероятно, многие из рассмотренных здесь особых проблем там уже решены.