JEP 349: JFR Event Streaming
Потоковая передача событий JFR
| Authors | Erik Gahlin, Markus Grönlund |
| Ответственный | Erik Gahlin |
| Тип | Feature |
| Область | JDK |
| Статус | Closed / Delivered |
| Выпуск | 14 |
| Компонент | hotspot / jfr |
| Обсуждение | hotspot dash jfr dash dev at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | S |
| Рецензенты | Karen Kinnear, Mikael Vidstedt |
| Одобрен | Mikael Vidstedt |
| Создан | 2017/07/11 19:20 |
| Обновлён | 2026/06/29 14:05 |
| Задача | 8184193 |
Аннотация
Сделать данные JDK Flight Recorder доступными для непрерывного мониторинга.
Цели
- Предоставить API для непрерывного получения данных JFR с диска как для приложений внутри того же процесса, так и для приложений вне его.
- Записывать тот же набор событий, что и без потоковой передачи, с накладными расходами менее 1 %, если это возможно.
- Потоковая передача событий должна работать одновременно с записями без потоковой передачи, как на диске, так и в памяти.
Что не является целью
- Предоставить потребителям синхронные обратные вызовы.
- Разрешить получение данных из записей в памяти.
Мотивация
HotSpot VM выдаёт через JFR более 500 точек данных, и большинство из них нельзя получить другими способами, кроме разбора файлов журналов.
Сейчас, чтобы получить эти данные, пользователь должен запустить запись, остановить её, сбросить содержимое на диск и затем разобрать файл записи. Это хорошо подходит для профилирования приложений, где обычно за один раз записывается не меньше минуты данных, но не подходит для мониторинга. Пример использования для мониторинга — панель, на которой данные обновляются в реальном времени.
Создание записи связано с накладными расходами, например:
- выдача событий, которые должны происходить при создании новой записи,
- запись метаданных событий, например структуры полей,
- запись данных контрольных точек, например трассировок стека, и
- копирование данных из дискового репозитория в отдельный файл записи.
Если бы можно было читать записываемые данные из дискового репозитория, не создавая новый файл записи, большей части этих накладных расходов удалось бы избежать.
Описание
Пакет jdk.jfr.consumer в модуле jdk.jfr дополнен возможностью асинхронно подписываться на события. Пользователи могут читать данные записи напрямую, то есть в виде потока, из дискового репозитория, не сбрасывая файл записи. Для работы с потоком регистрируется обработчик, например лямбда-функция, который вызывается при поступлении события.
Следующий пример выводит общую загрузку процессора и блокировки, за которые шла конкуренция дольше 10 мс.
try (var rs = new RecordingStream()) {
rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));
rs.onEvent("jdk.CPULoad", event -> {
System.out.println(event.getFloat("machineTotal"));
});
rs.onEvent("jdk.JavaMonitorEnter", event -> {
System.out.println(event.getClass("monitorClass"));
});
rs.start();
}
Класс RecordingStream реализует интерфейс jdk.jfr.consumer.EventStream, который даёт единый способ фильтровать и получать события независимо от того, является ли источником живой поток или файл на диске.
public interface EventStream extends AutoCloseable {
public static EventStream openRepository();
public static EventStream openRepository(Path directory);
public static EventStream openFile(Path file);
void setStartTime(Instant startTime);
void setEndTime(Instant endTime);
void setOrdered(boolean ordered);
void setReuse(boolean reuse);
void onEvent(Consumer<RecordedEvent> handler);
void onEvent(String eventName, Consumer<RecordedEvent handler);
void onFlush(Runnable handler);
void onClose(Runnable handler);
void onError(Runnable handler);
void remove(Object handler);
void start();
void startAsync();
void awaitTermination();
void awaitTermination(Duration duration);
void close();
}
Для создания потока есть три фабричных метода. EventStream::openRepository(Path) создаёт поток из дискового репозитория. Так можно отслеживать другие процессы, работая напрямую с файловой системой. Расположение дискового репозитория хранится в системном свойстве "jdk.jfr.repository", которое можно прочитать с помощью attach API. Мониторинг внутри процесса также можно выполнять с помощью метода EventStream::openRepository(). В отличие от RecordingStream, он не запускает запись. Вместо этого поток получает события только тогда, когда записи запускаются извне, например через JCMD или JMX. Метод EventStream::openFile(Path) создаёт поток из файла записи. Он дополняет уже существующий класс RecordingFile.
С помощью интерфейса также можно задать объём буферизуемых данных и указать, нужно ли упорядочивать события по времени. Чтобы снизить нагрузку от выделения памяти, есть также параметр, который определяет, создавать ли новый объект события для каждого события или можно повторно использовать предыдущий объект. Поток можно запустить в текущем потоке выполнения или асинхронно.
События, хранящиеся в буферах, локальных для потоков, раз в секунду сбрасываются в дисковый репозиторий виртуальной машиной Java (JVM). Отдельный поток разбирает самый новый файл до места, до которого записаны данные, и передаёт события подписчикам. Чтобы накладные расходы оставались низкими, из файла читаются только события, на которые есть активная подписка. Чтобы получать уведомление о завершении сброса, можно зарегистрировать обработчик методом EventStream::onFlush(Runnable). В этот момент можно агрегировать данные или передавать их во внешние системы, пока JVM готовит следующий набор событий.
Альтернативы
Уведомления JMX позволяют JDK и сторонним приложениям предоставлять информацию для непрерывного мониторинга. Однако у JMX есть недостатки, из-за которых он не подходит для целей этого JEP.
- Точки данных в JVM часто собираются там, где вызвать код на Java невозможно, например во время safepoint, вызванной GC.
- На сбор данных с помощью JFR уже потрачено время разработчиков. Переписывание всех этих точек сбора под JMX потребовало бы очень больших усилий.
- JMX не даёт механизма для отсеивания событий до их отправки, поэтому систему легко перегрузить.
- Сложные структуры данных со ссылками, например трассировки стека, невозможно эффективно представить с помощью типов Open MBean.
Тестирование
- Проверить, что в этой функциональности нет утечек памяти.
- Проверить, что производительность этой функциональности стабильна со временем (соответствующее нагрузочное тестирование).
- Написать модульные тесты для всех экспортируемых методов.
- Проверить, что подписки на события работают при одновременном выполнении других записей.
- Проверить, что API хорошо работает сразу, без дополнительной настройки.
- Проверить, что API подходит для пересылки данных событий в другие фреймворки для обработки.
- Проверить, что API подходит для сред, где важна низкая задержка (минимальные паузы GC).
- Проверить, что API подходит для производителей инструментов, то есть данные поступают с частотой, подходящей для построения графиков.
- Проверить, что API безопасен: не должно быть возможности получить обратный вызов в контексте привилегированного потока.
- Проверить, что накладные расходы приемлемы.
- Проверить, что в подписчиках невозможно вызвать бесконечную рекурсию.
Риски и допущения
- Операции в обратных вызовах API могут порождать события JFR, что может привести к бесконечной рекурсии. Этот риск можно снизить, если в такой ситуации не записывать события.