Метрики асинхронных вызовов в Spring WebFlux (Java Reactor)

Существует множество фреймворков для работы с метриками Java-приложений (например Micrometer, который, к тому же, еще и включён в новые версии Spring Boot). Но когда дело доходит до мониторинга времени выполнения асинхронных запросов у некоторых (у меня!) сразу возникает множество проблем.

Передо мной стояла задача настроить JMX-мониторинг для отслеживания асинхронных вызовов в Mono фреймворка Java Reactor. Меня интересовали всего два параметра – общее количество вызовов и среднее время их выполнения.

Spring Boot позволяет создать и зарегистрировать JMX-бин, указав всего пару аннотаций. Вот так в итоге выглядел мой класс:

@ManagedResource(description = "JMX Metrics MBean")
@Component
public class MyMBean {
    private volatile long avgQueryTime = 1;
    private volatile long queryQty = 0;

    @ManagedAttribute(description = "Average time of async call")
    public long getAvgQueryTime() {
        return avgQueryTime;
    }

    public synchronized void addQueryTime(long newTime) {
        this.avgQueryTime = (avgQueryTime + newTime) / 2;
    }

    @ManagedAttribute(description = "Quantity of queries being called")
    public long getQueryQty() {
        return queryQty;
    }

    public synchronized void incQty() {
        queryQty++;
    }
}

Аннотация @ManagedResource устанавливается на уровне класса и регистрирует наш компонент в качестве JMX-бина, а при помощи @ManagedAttribute обозначаются поля, которые должны быть доступны для мониторинга. Я добавил аннотацию @Component для регистрации класса в качестве компонента Spring, для удобства.

Прошу не судить меня строго за реализацию методов подсчёта среднего времени и количества вызовов, это пример.

По окончании обработки каждого Mono нам необходимо обновлять атрибуты MyMBean, чтобы клиенты могли увидеть свежую статистику. Как получить время выполнения асинхронного метода?

Непосредственно перед возвратом Mono сделаем временную “засечку”:

final long start = System.nanoTime()

А в монаду Mono добавим метод doFinally(), который выполняется по событию завершения Mono (это может быть отмена, завершение или ошибка):

.doFinally(end -> {         
    long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    myMBean.incQty();
    myMBean.addQueryTime(elapsed);
  })

Примечание: myMBean – @Autowired экземпляр класса MyMBean.

Выглядит просто, реализуется быстро, даёт хоть какой-то результат. Посмотреть статистику JMX-бина можно в JConsole (есть в поставке JDK).

Реализация с применением metrics()

После прочёсывания мануалов оказалось, что класс Mono содержит метод metrics(), который публикует обширную JMX-статистику по использованию асинхронных вызовов. Достаточно добавить вызов этого метода в монаду Mono (примечание: статистика публикуется только если Mono был задействован хотя бы один раз).

Запускаем приложение, активируем интересующий нас Mono хотя бы один раз, подключаемся к процессу через JConsole и ищем ветку metrics -> ReactorFlowDuration.exception.flow.reactor.status.completed.type.Mono. Здесь находится довольно обширная статистика асинхронных вызовов:

Метрики Mono