Introduzione a Netflix Servo

1. Panoramica

Netflix Servo è uno strumento di misurazione per le applicazioni Java. Servo è simile a Dropwizard Metrics, ma molto più semplice. Sfrutta JMX solo per fornire una semplice interfaccia per l'esposizione e la pubblicazione delle metriche dell'applicazione.

In questo articolo, introdurremo cosa fornisce Servo e come possiamo usarlo per raccogliere e pubblicare le metriche dell'applicazione.

2. Dipendenze di Maven

Prima di immergerci nell'effettiva implementazione, aggiungiamo la dipendenza Servo al file pom.xml :

 com.netflix.servo servo-core 0.12.16 

Inoltre, sono disponibili molte estensioni, come Servo-Apache, Servo-AWS, ecc. Potremmo averne bisogno in seguito. Le ultime versioni di queste estensioni possono essere trovate anche su Maven Central.

3. Raccogli le metriche

Per prima cosa, vediamo come raccogliere le metriche dalla nostra applicazione.

Servo offre quattro tipi di metriche principali: Contatore , Gauge , Timer, e informativo .

3.1. Tipi di metriche - Contatore

I contatori vengono utilizzati per registrare l'incremento. Le implementazioni comunemente utilizzate sono BasicCounter , StepCounter e PeakRateCounter .

BasicCounter fa quello che dovrebbe fare un contatore, semplice e diretto:

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 restituisce il conteggio massimo per un dato secondo durante l'intervallo di polling:

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());

A differenza di altri contatori, StepCounter registra la frequenza al secondo dell'intervallo di polling precedente:

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());

Si noti che abbiamo impostato servo.pollers su 1000 nel codice sopra. Questo consisteva nell'impostare l'intervallo di polling a 1 secondo invece di intervalli di 60 secondi e 10 secondi per impostazione predefinita. Ne parleremo più avanti in seguito.

3.2. Tipi di metriche - Indicatore

Gauge è un semplice monitor che restituisce il valore corrente. Vengono forniti BasicGauge , MinGauge , MaxGauge e NumberGauges .

BasicGauge richiama un Callable per ottenere il valore corrente. Possiamo ottenere la dimensione di una raccolta, l'ultimo valore di BlockingQueue o qualsiasi valore che richieda piccoli calcoli.

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

MaxGauge e MinGauge vengono utilizzati per tenere traccia rispettivamente dei valori massimo e minimo:

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 ) avvolge un numero fornito ( Long , Double ). Per raccogliere le metriche utilizzando questi indicatori, dobbiamo assicurarci che il numero sia thread-safe.

3.3. Tipi di metriche - Timer

I timer aiutano a misurare la durata di un particolare evento. Le implementazioni predefinite sono BasicTimer , StatsTimer e BucketTimer .

BasicTimer registra il tempo totale, il conteggio e altre semplici statistiche:

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 fornisce statistiche molto più ricche campionando tra gli intervalli di polling:

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 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 fornisce un modo per ottenere la distribuzione dei campioni tramite il bucket di intervalli di valori:

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 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)));

Per tenere traccia delle operazioni a lungo termine che potrebbero durare per ore, possiamo utilizzare il monitor composito DurationTimer .

3.4. Tipi di metriche - Informativi

Inoltre, è possibile utilizzare il monitor informativo per registrare informazioni descrittive per facilitare il debug e la diagnostica. L'unica implementazione è BasicInformational e il suo utilizzo non può essere più semplice:

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

3.5. MonitorRegistry

I tipi di metrica sono tutti di tipo Monitor , che è la base di Servo . Ora sappiamo che i tipi di strumenti raccolgono metriche non elaborate, ma per segnalare i dati, dobbiamo registrare questi monitor.

Si noti che ogni singolo monitor configurato deve essere registrato una e solo una volta per garantire la correttezza delle metriche. Quindi possiamo registrare i monitor usando il pattern Singleton.

La maggior parte delle volte, possiamo usare DefaultMonitorRegistry per registrare i monitor:

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

Se si desidera registrare dinamicamente un monitor, è possibile utilizzare DynamicTimer e DynamicCounter :

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

Si noti che la registrazione dinamica causerebbe costose operazioni di ricerca ogni volta che il valore viene aggiornato.

Servo fornisce anche diversi metodi di supporto per registrare i monitor dichiarati negli oggetti:

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

Il metodo registerObject utilizzerà la riflessione per aggiungere tutte le istanze di monitor dichiarate dall'annotazione @Monitor e aggiungere i tag dichiarati da @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
    
      metrics = observer.getObservations(); assertThat(metrics, hasSize(greaterThanOrEqualTo(1))); Iterator
     
       metricIterator = metrics.iterator(); metricIterator.next(); //skip first empty observation while (metricIterator.hasNext()) { assertThat(metricIterator.next(), hasItem( hasProperty("config", hasProperty("name", is("integerCounter"))))); } }
     
    

4. Pubblica metriche

Con le metriche raccolte, possiamo pubblicarlo in qualsiasi formato, come il rendering di grafici di serie temporali su varie piattaforme di visualizzazione dei dati. Per pubblicare le metriche, dobbiamo eseguire periodicamente il polling dei dati dalle osservazioni del monitor.

4.1. MetricPoller

MetricPoller viene utilizzato come fetcher di metriche. Possiamo recuperare le metriche di MonitorRegistries , JVM, JMX. Con l'aiuto delle estensioni, possiamo eseguire il polling di metriche come lo stato del server Apache e le metriche di 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
    
      metrics = observer.getObservations(); assertThat(metrics, hasSize(greaterThanOrEqualTo(1))); List keys = extractKeys(metrics); assertThat(keys, hasItems("loadedClassCount", "initUsage", "maxUsage", "threadCount"));
    

Here we created a JvmMetricPoller to poll metrics of JVM. When adding the poller to the scheduler, we let the poll task to run every second. System default poller configurations are defined in Pollers, but we can specify pollers to use with system property servo.pollers.

4.2. MetricObserver

When polling metrics, observations of registered MetricObservers will be updated.

MetricObservers provided by default are MemoryMetricObserver, FileMetricObserver, and AsyncMetricObserver. We have already shown how to use MemoryMetricObserver in the previous code sample.

Currently, several useful extensions are available:

  • AtlasMetricObserver: publish metrics to Netflix Atlas to generate in memory time series data for analytics
  • CloudWatchMetricObserver: push metrics to Amazon CloudWatch for metrics monitoring and tracking
  • GraphiteObserver: publish metrics to Graphite to store and graph

We can implement a customized MetricObserver to publish application metrics to where we see fit. The only thing to care about is to handle the updated metrics:

public class CustomObserver extends BaseMetricObserver { //... @Override public void updateImpl(List metrics) { //TODO } }

4.3. Publish to Netflix Atlas

Atlas is another metrics-related tool from Netflix. It's a tool for managing dimensional time series data, which is a perfect place to publish the metrics we collected.

Now, we'll demonstrate how to publish our metrics to Netflix Atlas.

First, let's append the servo-atlas dependency to the pom.xml:

 com.netflix.servo servo-atlas ${netflix.servo.ver}   0.12.17 

This dependency includes an AtlasMetricObserver to help us publish metrics to Atlas.

Then, we shall set up an Atlas server:

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

To save our time for the test, let's set the step size to 1 second in memory.conf, so that we can generate a time series graph with enough details of the metrics.

The AtlasMetricObserver requires a simple configuration and a list of tags. Metrics of the given tags will be pushed to Atlas:

System.setProperty("servo.pollers", "1000"); System.setProperty("servo.atlas.batchSize", "1"); System.setProperty("servo.atlas.uri", "//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);

After starting up a PollScheduler with the PollRunnable task, we can publish metrics to Atlas automatically:

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"));

Sulla base delle metriche, possiamo generare un grafico a linee utilizzando l'API del grafico di Atlas:

5. Riepilogo

In questo articolo, abbiamo introdotto come utilizzare Netflix Servo per raccogliere e pubblicare le metriche delle applicazioni.

Se non hai letto la nostra introduzione a Dropwizard Metrics, dai un'occhiata qui per un rapido confronto con Servo.

Come sempre, il codice di implementazione completo di questo articolo può essere trovato su Github.