«1. Обзор

Netflix Servo — это инструмент метрик для Java-приложений. Servo похож на Dropwizard Metrics, но намного проще. Он использует JMX только для предоставления простого интерфейса для предоставления и публикации метрик приложения.

В этой статье мы расскажем, что предоставляет Servo и как мы можем использовать его для сбора и публикации метрик приложения.

2. Зависимости Maven

Прежде чем мы углубимся в реальную реализацию, давайте добавим зависимость Servo в файл pom.xml:

<dependency>
    <groupId>com.netflix.servo</groupId>
    <artifactId>servo-core</artifactId>
    <version>0.12.16</version>
</dependency>

Кроме того, существует множество доступных расширений, таких как Servo-Apache, Servo -AWS и т. д. Они могут понадобиться нам позже. Последние версии этих расширений также можно найти на Maven Central.

3. Сбор метрик

Сначала давайте посмотрим, как собирать метрики из нашего приложения.

Servo предоставляет четыре основных типа метрик: Counter, Gauge, Timer и Informational.

3.1. Типы метрик — счетчик

Счетчики используются для записи приращения. Обычно используемые реализации: BasicCounter, StepCounter и PeakRateCounter.

BasicCounter делает то, что должен делать счетчик, просто и прямо:

Counter counter = new BasicCounter(MonitorConfig.builder("test").build());
assertEquals("counter should start with 0", 0, counter.getValue().intValue());

counter.increment();
 
assertEquals("counter should have increased by 1", 1, counter.getValue().intValue());

counter.increment(-1);
 
assertEquals("counter should have decreased by 1", 0, counter.getValue().intValue());

PeakRateCounter возвращает максимальное количество за данную секунду в течение интервала опроса:

Counter counter = new PeakRateCounter(MonitorConfig.builder("test").build());
assertEquals(
  "counter should start with 0", 
  0, counter.getValue().intValue());

counter.increment();
SECONDS.sleep(1);

counter.increment();
counter.increment();

assertEquals("peak rate should have be 2", 2, counter.getValue().intValue());

В отличие от других счетчиков, StepCounter записывает скорость в секунду предыдущего интервала опроса:

System.setProperty("servo.pollers", "1000");
Counter counter = new StepCounter(MonitorConfig.builder("test").build());
 
assertEquals("counter should start with rate 0.0", 0.0, counter.getValue());

counter.increment();
SECONDS.sleep(1);

assertEquals(
  "counter rate should have increased to 1.0", 
  1.0, counter.getValue());

Обратите внимание, что мы установили servo.pollers на 1000 в приведенном выше коде. Это должно было установить интервал опроса на 1 секунду вместо интервалов в 60 секунд и 10 секунд по умолчанию. Мы расскажем об этом позже.

3.2. Типы метрик — Gauge

Gauge — это простой монитор, который возвращает текущее значение. Предоставляются BasicGauge, MinGauge, MaxGauge и NumberGauge.

BasicGauge вызывает Callable для получения текущего значения. Мы можем получить размер коллекции, последнее значение BlockingQueue или любое значение, требующее небольших вычислений.

Gauge<Double> gauge = new BasicGauge<>(MonitorConfig.builder("test")
  .build(), () -> 2.32);
 
assertEquals(2.32, gauge.getValue(), 0.01);

MaxGauge и MinGauge используются для отслеживания максимального и минимального значений соответственно:

MaxGauge gauge = new MaxGauge(MonitorConfig.builder("test").build());
assertEquals(0, gauge.getValue().intValue());

gauge.update(4);
assertEquals(4, gauge.getCurrentValue(0));

gauge.update(1);
assertEquals(4, gauge.getCurrentValue(0));

NumberGauge (LongGauge, DoubleGauge) упаковывает предоставленный номер (Long, Double). Чтобы собирать метрики с помощью этих датчиков, мы должны убедиться, что число является потокобезопасным.

3.3. Типы метрик – Таймер

Таймеры помогают измерять продолжительность определенного события. Реализациями по умолчанию являются BasicTimer, StatsTimer и BucketTimer.

BasicTimer записывает общее время, количество и другую простую статистику:

BasicTimer timer = new BasicTimer(MonitorConfig.builder("test").build(), SECONDS);
Stopwatch stopwatch = timer.start();

SECONDS.sleep(1);
timer.record(2, SECONDS);
stopwatch.stop();

assertEquals("timer should count 1 second", 1, timer.getValue().intValue());
assertEquals("timer should count 3 seconds in total", 
  3.0, timer.getTotalTime(), 0.01);
assertEquals("timer should record 2 updates", 2, timer.getCount().intValue());
assertEquals("timer should have max 2", 2, timer.getMax(), 0.01);

StatsTimer предоставляет гораздо более богатую статистику путем выборки между интервалами опроса:

System.setProperty("netflix.servo", "1000");
StatsTimer timer = new StatsTimer(MonitorConfig
  .builder("test")
  .build(), new StatsConfig.Builder()
  .withComputeFrequencyMillis(2000)
  .withPercentiles(new double[] { 99.0, 95.0, 90.0 })
  .withPublishMax(true)
  .withPublishMin(true)
  .withPublishCount(true)
  .withPublishMean(true)
  .withPublishStdDev(true)
  .withPublishVariance(true)
  .build(), SECONDS);
Stopwatch stopwatch = timer.start();

SECONDS.sleep(1);
timer.record(3, SECONDS);
stopwatch.stop();

stopwatch = timer.start();
timer.record(6, SECONDS);
SECONDS.sleep(2);
stopwatch.stop();

assertEquals("timer should count 12 seconds in total", 
  12, timer.getTotalTime());
assertEquals("timer should count 12 seconds in total", 
  12, timer.getTotalMeasurement());
assertEquals("timer should record 4 updates", 4, timer.getCount());
assertEquals("stats timer value time-cost/update should be 2", 
  3, timer.getValue().intValue());

final Map<String, Number> metricMap = timer.getMonitors().stream()
  .collect(toMap(monitor -> getMonitorTagValue(monitor, "statistic"),
    monitor -> (Number) monitor.getValue()));
 
assertThat(metricMap.keySet(), containsInAnyOrder(
  "count", "totalTime", "max", "min", "variance", "stdDev", "avg", 
  "percentile_99", "percentile_95", "percentile_90"));

BucketTimer позволяет получить распределение выборок по значению сегментирования диапазоны:

BucketTimer timer = new BucketTimer(MonitorConfig
  .builder("test")
  .build(), new BucketConfig.Builder()
  .withBuckets(new long[] { 2L, 5L })
  .withTimeUnit(SECONDS)
  .build(), SECONDS);

timer.record(3);
timer.record(6);

assertEquals(
  "timer should count 9 seconds in total",
  9, timer.getTotalTime().intValue());
 
Map<String, Long> metricMap = timer.getMonitors().stream()
  .filter(monitor -> monitor.getConfig().getTags().containsKey("servo.bucket"))
  .collect(toMap(
    m -> getMonitorTagValue(m, "servo.bucket"),
    m -> (Long) m.getValue()));

assertThat(metricMap, allOf(hasEntry("bucket=2s", 0L), hasEntry("bucket=5s", 1L),
  hasEntry("bucket=overflow", 1L)));

Для отслеживания длительных операций, которые могут длиться часами, мы можем использовать составной монитор DurationTimer.

3.4. Типы метрик — информационные

Кроме того, мы можем использовать информационный монитор для записи описательной информации, чтобы помочь в отладке и диагностике. Единственная реализация — BasicInformational, и ее использование не может быть проще:

BasicInformational informational = new BasicInformational(
  MonitorConfig.builder("test").build());
informational.setValue("information collected");

3.5. MonitorRegistry

Все типы метрик относятся к типу Monitor, который является самой основой Servo. Теперь мы знаем, какие инструменты собирают необработанные метрики, но чтобы сообщать данные, нам нужно зарегистрировать эти мониторы.

Обратите внимание, что каждый отдельный настроенный монитор должен быть зарегистрирован один раз и только один раз, чтобы обеспечить правильность метрик. Таким образом, мы можем зарегистрировать мониторы, используя шаблон Singleton.

Большую часть времени мы можем использовать DefaultMonitorRegistry для регистрации мониторов:

Gauge<Double> gauge = new BasicGauge<>(MonitorConfig.builder("test")
  .build(), () -> 2.32);
DefaultMonitorRegistry.getInstance().register(gauge);

Если мы хотим динамически зарегистрировать монитор, можно использовать DynamicTimer и DynamicCounter:

DynamicCounter.increment("monitor-name", "tag-key", "tag-value");

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

Servo также предоставляет несколько вспомогательных методов для регистрации мониторов, объявленных в объектах:

Monitors.registerObject("testObject", this);
assertTrue(Monitors.isObjectRegistered("testObject", this));

Метод registerObject будет использовать отражение для добавления всех экземпляров мониторов, объявленных аннотацией @Monitor, и добавления тегов, объявленных @MonitorTags:

@Monitor(
  name = "integerCounter",
  type = DataSourceType.COUNTER,
  description = "Total number of update operations.")
private AtomicInteger updateCount = new AtomicInteger(0);

@MonitorTags
private TagList tags = new BasicTagList(
  newArrayList(new BasicTag("tag-key", "tag-value")));

@Test
public void givenAnnotatedMonitor_whenUpdated_thenDataCollected() throws Exception {
    System.setProperty("servo.pollers", "1000");
    Monitors.registerObject("testObject", this);
    assertTrue(Monitors.isObjectRegistered("testObject", this));

    updateCount.incrementAndGet();
    updateCount.incrementAndGet();
    SECONDS.sleep(1);

    List<List<Metric>> metrics = observer.getObservations();
 
    assertThat(metrics, hasSize(greaterThanOrEqualTo(1)));
 
    Iterator<List<Metric>> metricIterator = metrics.iterator();
    metricIterator.next(); //skip first empty observation
 
    while (metricIterator.hasNext()) {
        assertThat(metricIterator.next(), hasItem(
          hasProperty("config", 
          hasProperty("name", is("integerCounter")))));
    }
}

~ ~~ 4. Публикация метрик

«Собранные метрики мы можем публиковать в любом формате, например, отображать графики временных рядов на различных платформах визуализации данных. Чтобы опубликовать метрики, нам нужно периодически опрашивать данные из наблюдений монитора.

4.1. MetricPoller

MetricPoller используется как сборщик метрик. Мы можем получить метрики MonitorRegistries, JVM, JMX. С помощью расширений мы можем опрашивать такие показатели, как состояние сервера Apache и показатели Tomcat.

MemoryMetricObserver observer = new MemoryMetricObserver();
PollRunnable pollRunnable = new PollRunnable(new JvmMetricPoller(),
  new BasicMetricFilter(true), observer);
PollScheduler.getInstance().start();
PollScheduler.getInstance().addPoller(pollRunnable, 1, SECONDS);

SECONDS.sleep(1);
PollScheduler.getInstance().stop();
List<List<Metric>> metrics = observer.getObservations();

assertThat(metrics, hasSize(greaterThanOrEqualTo(1)));
List<String> keys = extractKeys(metrics);
 
assertThat(keys, hasItems("loadedClassCount", "initUsage", "maxUsage", "threadCount"));

Здесь мы создали JvmMetricPoller для опроса метрик JVM. При добавлении опросчика в планировщик мы позволяем задаче опроса выполняться каждую секунду. Конфигурации системных опросников по умолчанию определяются в Pollers, но мы можем указать опросники для использования с системным свойством servo.pollers.

4.2. MetricObserver

При опросе метрик будут обновляться наблюдения зарегистрированных MetricObservers.

По умолчанию предоставляются MetricObserver: MemoryMetricObserver, FileMetricObserver и AsyncMetricObserver. Мы уже показали, как использовать MemoryMetricObserver в предыдущем примере кода.

В настоящее время доступно несколько полезных расширений:

    AtlasMetricObserver: публикация метрик в Netflix Atlas для создания в памяти данных временных рядов для аналитики CloudWatchMetricObserver: отправка метрик в Amazon CloudWatch для мониторинга и отслеживания метрик GraphiteObserver: публикация метрик в Graphite для сохранения и график

Мы можем реализовать настроенный MetricObserver для публикации метрик приложения там, где мы считаем нужным. Единственное, о чем нужно позаботиться, это обработать обновленные показатели:

public class CustomObserver extends BaseMetricObserver {

    //...

    @Override
    public void updateImpl(List<Metric> metrics) {
        //TODO
    }
}

4.3. Публикация в Netflix Atlas

Atlas — еще один инструмент Netflix, связанный с метриками. Это инструмент для управления многомерными данными временных рядов, который является идеальным местом для публикации собранных нами показателей.

Теперь мы покажем, как публиковать наши показатели в Netflix Atlas.

Во-первых, давайте добавим зависимость servo-atlas к pom.xml:

<dependency>
      <groupId>com.netflix.servo</groupId>
      <artifactId>servo-atlas</artifactId>
      <version>${netflix.servo.ver}</version>
</dependency>

<properties>
    <netflix.servo.ver>0.12.17</netflix.servo.ver>
</properties>

Эта зависимость включает AtlasMetricObserver, чтобы помочь нам публиковать метрики в Atlas.

Затем мы настроим сервер Atlas:

$ curl -LO 'https://github.com/Netflix/atlas/releases/download/v1.4.4/atlas-1.4.4-standalone.jar'
$ curl -LO 'https://raw.githubusercontent.com/Netflix/atlas/v1.4.x/conf/memory.conf'
$ java -jar atlas-1.4.4-standalone.jar memory.conf

Чтобы сэкономить время на тест, давайте установим размер шага в 1 секунду в memory.conf, чтобы мы могли сгенерировать график временных рядов с достаточно деталей показателей.

AtlasMetricObserver требует простой конфигурации и списка тегов. Метрики заданных тегов будут отправлены в Atlas:

System.setProperty("servo.pollers", "1000");
System.setProperty("servo.atlas.batchSize", "1");
System.setProperty("servo.atlas.uri", "http://localhost:7101/api/v1/publish");
AtlasMetricObserver observer = new AtlasMetricObserver(
  new BasicAtlasConfig(), BasicTagList.of("servo", "counter"));

PollRunnable task = new PollRunnable(
  new MonitorRegistryMetricPoller(), new BasicMetricFilter(true), observer);

После запуска PollScheduler с задачей PollRunnable мы можем автоматически опубликовать метрики в Atlas:

Counter counter = new BasicCounter(MonitorConfig
  .builder("test")
  .withTag("servo", "counter")
  .build());
DefaultMonitorRegistry
  .getInstance()
  .register(counter);
assertThat(atlasValuesOfTag("servo"), not(containsString("counter")));

for (int i = 0; i < 3; i++) {
    counter.increment(RandomUtils.nextInt(10));
    SECONDS.sleep(1);
    counter.increment(-1 * RandomUtils.nextInt(10));
    SECONDS.sleep(1);
}

assertThat(atlasValuesOfTag("servo"), containsString("counter"));

На основе метрик мы можем сгенерировать линейный график с использованием графического API Atlas:

5. Резюме

В этой статье мы рассказали, как использовать Netflix Servo для сбора и публикации метрик приложения.

Если вы не читали наше введение в Dropwizard Metrics, посмотрите его здесь для быстрого сравнения с Servo.

Как всегда, полный код реализации этой статьи можно найти на Github.