使用 Micrometer 記錄 Java 應用性能指標


https://www.ibm.com/developerworks/cn/java/j-using-micrometer-to-record-java-metric/index.html

運行良好的應用離不開對性能指標的收集。這些性能指標可以有效地對生產系統的各方面行為進行監控,幫助運維人員掌握系統運行狀態和查找問題原因。性能指標監控通常由兩個部分組成:第一個部分是性能指標數據的收集,需要在應用程序代碼中添加相應的代碼來完成;另一個部分是后台監控系統,負責對數據進行聚合計算和提供 API 接口。在應用中使用計數器、計量儀和計時器來記錄關鍵的性能指標。在專用的監控系統中對性能指標進行匯總,並生成相應的圖表來進行可視化分析。

目前已經有非常多的監控系統,常用的如 Prometheus、New Relic、Influx、Graphite 和 Datadog,每個系統都有自己獨特的數據收集方式。這些監控系統有的是需要自主安裝的軟件,有的則是雲服務。它們的后台實現千差萬別,數據接口也是各有不同。在指標數據收集方面,大多數時候都是使用與后台監控系統對應的客戶端程序。此外,這些監控系統一般都會提供不同語言和平台使用的第三方庫,這不可避免的會帶來供應商鎖定的問題。一旦針對某監控系統的數據收集代碼添加到應用程序中,當需要切換監控系統時,也要對應用程序進行大量的修改。Micrometer 的出現恰好解決了這個問題,其作用可以類比於 SLF4J 在 Java 日志記錄中的作用。

Micrometer 簡介

Micrometer 為 Java 平台上的性能數據收集提供了一個通用的 API,應用程序只需要使用 Micrometer 的通用 API 來收集性能指標即可。Micrometer 會負責完成與不同監控系統的適配工作。這就使得切換監控系統變得很容易。Micrometer 還支持推送數據到多個不同的監控系統。

在 Java 應用中使用 Micrometer 非常的簡單。只需要在 Maven 或 Gradle 項目中添加相應的依賴即可。Micrometer 包含如下三種模塊,分組名稱都是 io.micrometer:

  • 包含數據收集 SPI 和基於內存的實現的核心模塊 micrometer-core
  • 針對不同監控系統的實現模塊,如針對 Prometheus 的 micrometer-registry-prometheus
  • 與測試相關的模塊 micrometer-test

在 Java 應用中,只需要根據所使用的監控系統,添加所對應的模塊即可。比如,使用 Prometheus 的應用只需要添加 micrometer-registry-prometheus 模塊即可。模塊 micrometer-core 會作為傳遞依賴自動添加。本文使用的 Micrometer 版本是 1.1.1。清單 1 給出了使用 Micrometer 的 Maven 項目的示例:

清單 1. 使用 Micrometer 的 Maven 項目
1
2
3
4
5
< dependency >
     < groupId >io.micrometer</ groupId >
     < artifactId >micrometer-registry-prometheus</ artifactId >
     < version >1.1.1</ version >
</ dependency >

計量器注冊表

Micrometer 中有兩個最核心的概念,分別是計量器(Meter)和計量器注冊表(MeterRegistry)。計量器表示的是需要收集的性能指標數據,而計量器注冊表負責創建和維護計量器。每個監控系統有自己獨有的計量器注冊表實現。模塊 micrometer-core 中提供的類 SimpleMeterRegistry 是一個基於內存的計量器注冊表實現。SimpleMeterRegistry 不支持導出數據到監控系統,主要用來進行本地開發和測試。

Micrometer 支持多個不同的監控系統。通過計量器注冊表實現類 CompositeMeterRegistry 可以把多個計量器注冊表組合起來,從而允許同時發布數據到多個監控系統。對於由這個類創建的計量器,它們所產生的數據會對 CompositeMeterRegistry 中包含的所有計量器注冊表都產生影響。在清單 2 中,我創建了一個 CompositeMeterRegistry 對象,並在其中添加了兩個 SimpleMeterRegistry 對象。一個 SimpleMeterRegistry 對象在創建時通過實現 SimpleConfig 接口提供了不同的名稱前綴。

清單 2. CompositeMeterRegistry 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CompositeMeterRegistryExample {
 
   public static void main(String[] args) {
     CompositeMeterRegistry registry = new CompositeMeterRegistry();
     registry.add(new SimpleMeterRegistry());
     registry.add(new SimpleMeterRegistry(new MyConfig(), Clock.SYSTEM));
 
     Counter counter = registry.counter("simple");
     counter.increment();
   }
 
   private static class MyConfig implements SimpleConfig {
 
     public String get(final String key) {
       return null;
     }
 
     public String prefix() {
       return "my";
     }
   }
}

Micrometer 本身提供了一個靜態的全局計量器注冊表對象 Metrics.globalRegistry。該注冊表是一個組合注冊表。使用 Metrics 類中的靜態方法創建的計量器,都會被添加到該全局注冊表中。對於大多數應用來說,這個全局注冊表對象就可以滿足需求,不需要額外創建新的注冊表對象。不過由於該對象是靜態的,在某些場合,尤其是進行單元測試時,會產生一些問題。在 清單 3 中,Metrics.addRegistry() 方法直接在全局注冊表對象中添加新的注冊表對象,而 Metrics.counter() 方法創建的計數器自動添加到全局注冊表中。

清單 3. 使用全局計量器注冊表對象
1
2
3
4
5
6
7
8
public class GlobalRegistryExample {
 
   public static void main(String[] args) {
     Metrics.addRegistry(new SimpleMeterRegistry());
     Counter counter = Metrics.counter("simple");
     counter.increment();
   }
}

使用計量器

計量器用來收集不同類型的性能指標信息。Micrometer 提供了不同類型的計量器實現。計量器對象由計量器注冊表創建並管理。

計量器名稱和標簽

每個計量器都有自己的名稱。由於不同的監控系統有自己獨有的推薦命名規則,Micrometer 使用句點 . 分隔計量器名稱中的不同部分,如 a.b.c。Micrometer 會負責完成所需的轉換,以滿足不同監控系統的需求。

每個計量器在創建時都可以指定一系列標簽。標簽以名值對的形式出現。監控系統使用標簽對數據進行過濾。除了每個計量器獨有的標簽之外,每個計量器注冊表還可以添加通用標簽。所有該注冊表導出的數據都會帶上這些通用標簽。

在清單 4 中,使用 MeterRegistry 的 config() 方法可以得到該注冊表對象的 MeterRegistry.Config 對象,再使用 commonTags() 方法來設置通用標簽。多個標簽按照名稱和值依次排列的方式來指定。在創建計量器時,在提供了名稱之后,以同樣的方式指定該計量器的標簽。

清單 4. 計量器注冊表的通用標簽
1
2
3
4
SimpleMeterRegistry registry = new SimpleMeterRegistry();
registry.config().commonTags("tag1", "a", "tag2", "b");
Counter counter = registry.counter("simple", "tag3", "c");
counter.increment();

計數器

計數器(Counter)表示的是單個的只允許增加的值。通過 MeterRegistry 的 counter() 方法來創建表示計數器的 Counter 對象。還可以使用 Counter.builder() 方法來創建 Counter 對象的構建器。Counter 所表示的計數值是 double 類型,其 increment() 方法可以指定增加的值。默認情況下增加的值是 1.0。

如果已經有一個方法返回計數值,可以直接從該方法中創建類型為 FunctionCounter 的計數器。在清單 5 中,方法 counter() 使用了兩種不同的方法來創建 Counter 對象。方法 functionCounter() 同樣使用了兩種不同的方法來創建 FunctionCounter 對象。

清單 5. 計數器使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Counters {
 
   private SimpleMeterRegistry registry = new SimpleMeterRegistry();
 
   private double value = 0.0;
 
   public void counter() {
     Counter counter1 = registry.counter("simple1");
     counter1.increment(2.0);
     Counter counter2 = Counter.builder("simple2")
         .description("A simple counter")
         .tag("tag1", "a")
         .register(registry);
     counter2.increment();
   }
 
   public void functionCounter() {
     List< Tag > tags = new ArrayList<>();
     registry.more().counter("function1", tags, this, Counters::getValue);
 
     FunctionCounter functionCounter = FunctionCounter.builder("function2", this, Counters::getValue)
         .description("A function counter")
         .tags(tags)
         .register(registry);
     functionCounter.count();
   }
 
   private double getValue() {
     return value++;
   }
}

計量儀

計量儀(Gauge)表示的是單個的變化的值。與計數器的不同之處在於,計量儀的值並不總是增加的。與創建 Counter 對象類似,Gauge 對象可以從計量器注冊表中創建,也可以使用 Gauge.builder() 方法返回的構造器來創建。清單 6 中給出了計量儀的使用示例,其中 gauge() 方法創建的是記錄任意 Number 對象的值,gaugeCollectionSize() 方法記錄集合的大小,gaugeMapSize() 方法記錄 Map 的大小。需要注意的是,這 3 個方法返回的並不是 Gauge 對象,而是被記錄的對象。這是由於 Gauge 對象一旦被創建,就不能手動對其中的值進行修改。在每次取樣時,Gauge 會返回當前值。正因為如此,得到一個 Gauge 對象,除了進行測試之外,沒有其他的意義。

清單 6. 計量儀使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Gauges {
 
   private SimpleMeterRegistry registry = new SimpleMeterRegistry();
 
   public void gauge() {
     AtomicInteger value = registry.gauge("gauge1", new AtomicInteger(0));
     value.set(1);
 
     List< String > list = registry.gaugeCollectionSize("list.size", Collections.emptyList(), new ArrayList<>());
     list.add("a");
 
     Map< String , String> map = registry.gaugeMapSize("map.size", Collections.emptyList(), new HashMap<>());
     map.put("a", "b");
 
     Gauge.builder("value", this, Gauges::getValue)
         .description("a simple gauge")
         .tag("tag1", "a")
         .register(registry);
   }
 
   private double getValue() {
     return ThreadLocalRandom.current().nextDouble();
   }
}

計時器

計時器(Timer)通常用來記錄事件的持續時間。計時器會記錄兩類數據:事件的數量和總的持續時間。在使用計時器之后,就不再需要單獨創建一個計數器。計時器可以從注冊表中創建,或者使用 Timer.builder() 方法返回的構建器來創建。Timer 提供了不同的方式來記錄持續時間。第一種方式是使用 record() 方法來記錄 Runnable 和 Callable 對象的運行時間;第二種方式是使用 Timer.Sample 來保存計時狀態。

在清單 7 中,方法 record() 使用 Timer 對象的 record() 方法來記錄一個 Runnable 對象的運行時間。方法 sample() 中首先使用 Timer.start() 來創建一個新的 Timer.Sample 對象並啟動計時。調用 Timer.Sample 的 stop() 方法把記錄的時間保存到 Timer 對象中。

清單 7. 計時器使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Timers {
   private SimpleMeterRegistry registry = new SimpleMeterRegistry();
 
   public void record() {
     Timer timer = registry.timer("simple");
     timer.record(() -> {
       try {
         Thread.sleep(3000);
       } catch (InterruptedException e) {
         e.printStackTrace();
       }
     });
   }
 
   public void sample() {
     Timer.Sample sample = Timer.start();
     new Thread(() -> {
       try {
         Thread.sleep(2000);
       } catch (InterruptedException e) {
         e.printStackTrace();
       }
       sample.stop(registry.timer("sample"));
     }).start();
   }
}

如果一個任務的耗時很長,直接使用 Timer 並不是一個好的選擇,因為 Timer 只有在任務完成之后才會記錄時間。更好的選擇是使用 LongTaskTimerLongTaskTimer 可以在任務進行中記錄已經耗費的時間,它通過注冊表的 more().longTaskTimer() 來創建,如清單 8 所示:

清單 8. LongTaskTimer 使用示例
1
2
3
4
5
6
7
8
9
10
public void longTask() {
   LongTaskTimer timer = registry.more().longTaskTimer("long");
   timer.record(() -> {
     try {
       Thread.sleep(3000);
     } catch (InterruptedException e) {
       e.printStackTrace();
     }
   });
}

分布概要

分布概要(Distribution summary)用來記錄事件的分布情況。計時器本質上也是一種分布概要。表示分布概要的類 DistributionSummary 可以從注冊表中創建,也可以使用 DistributionSummary.builder() 提供的構建器來創建。分布概要根據每個事件所對應的值,把事件分配到對應的桶(bucket)中。Micrometer 默認的桶的值從 1 到最大的 long 值。可以通過 minimumExpectedValue 和 maximumExpectedValue 來控制值的范圍。如果事件所對應的值較小,可以通過 scale 來設置一個值來對數值進行放大。與分布概要密切相關的是直方圖和百分比(percentile)。大多數時候,我們並不關注具體的數值,而是數值的分布區間。比如在查看 HTTP 服務響應時間的性能指標時,通常關注是的幾個重要的百分比,如 50%,75%和 90%等。所關注的是對於這些百分比數量的請求都在多少時間內完成。Micrometer 提供了兩種不同的方式來處理百分比。

  • 對於 Prometheus 這樣本身提供了對百分比支持的監控系統,Micrometer 直接發送收集的直方圖數據,由監控系統完成計算。
  • 對於其他不支持百分比的系統,Micrometer 會進行計算,並把百分比結果發送到監控系統。

在清單 9 中,創建的 DistributionSummary 所發布的百分比包括 0.50.75 和 0.9。使用 record() 方法來記錄數值,而 takeSnapshot() 方法返回當前數據的快照。

清單 9. 分布概要使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DistributionSummaries {
   private SimpleMeterRegistry registry = new SimpleMeterRegistry();
 
   public void summary() {
     DistributionSummary summary = DistributionSummary.builder("simple")
         .description("simple distribution summary")
         .minimumExpectedValue(1L)
         .maximumExpectedValue(10L)
         .publishPercentiles(0.5, 0.75, 0.9)
         .register(registry);
     summary.record(1);
     summary.record(1.3);
     summary.record(2.4);
     summary.record(3.5);
     summary.record(4.1);
     System.out.println(summary.takeSnapshot());
   }
 
   public static void main(String[] args) {
     new DistributionSummaries().summary();
   }
}

集成監控系統

Micrometer 提供了對多種不同的監控系統的支持。

JMX

JMX 是導出 Micrometer 收集的性能數據的最簡單有效的方式。雖然 JMX 所提供的功能比較弱,但是在很多情況下,JMX 就已經可以滿足需求了。如果需要導出數據到 JMX,只需要添加對庫 io.micrometer:micrometer-registry-jmx 的依賴即可。Micrometer 會根據計量器的名稱和標簽來生成對應的 JMX 對象名稱。默認的命名規則是在計量器名稱之后,加上按標簽名稱字母排序的以句點分隔的名值對。

Prometheus

Prometheus 與其他監控系統的不同在於,Prometheus 采取的是主動抽取數據的方式。因此客戶端需要暴露 HTTP 服務,並由 Prometheus 定期來訪問以獲取數據。Micrometer 的 Prometheus 注冊表已經提供了 HTTP 服務所需要返回的內容,只需要使用 Servlet 來提供 HTTP 服務即可。

集成 Spring Boot

從 Spring Boot 2.0 開始,Micrometer 就是 Spring Boot 默認提供的性能指標收集庫。Spring Boot Actuator 提供了對 Micrometer 的自動配置。Spring Boot 會自動配置一個組合注冊表對象,並把 CLASSPATH 上找到的所有支持的注冊表實現都添加起來。只需要在 CLASSPATH 上添加相應的依賴庫,Spring Boot 會完成所需的配置。這些注冊表對象也會被自動添加到全局注冊表對象中。如果需要對該注冊表進行配置,添加類型為 MeterRegistryCustomizer 的 bean 即可。在需要使用注冊表的地方,可以通過依賴注入的方式來使用 MeterRegistry 對象。

在清單 10 中,Spring 配置類 AppConfig 中聲明了一個類型為 MeterRegistryCustomizer<MeterRegistry> 的 bean,可以對 MeterRegistry 進行配置。這里使用 commonTags() 方法來添加通用標簽。

清單 10. Spring Boot 中 Micrometer 的配置示例
1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebMvc
@ComponentScan(basePackageClasses = AppConfig.class)
class AppConfig {
   @Bean
   MeterRegistryCustomizer< MeterRegistry > meterRegistryCustomizer() {
     return registry -> registry.config().commonTags("tag1", "a", "tag2", "b");
   }
}

清單 11 中的 REST 控制器 AppController 通過依賴注入獲取到 MeterRegistry 對象,並創建一個計數器。在方法 greeting() 中,對計數器進行遞增。

清單 11. 使用 MeterRegistry 對象的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/app")
public class AppController {
   private final Counter counter;
 
   public AppController(final MeterRegistry registry) {
     this.counter = registry.counter("greeting");
   }
 
   @RequestMapping("/greeting")
   public String greeting() {
     this.counter.increment();
     return "hello world #" + this.counter.count();
   }
}

對於 Prometheus 來說,Spring Boot Actuator 會自動配置一個 URL 為 /actuator/Prometheus 的 HTTP 服務來供 Prometheus 抓取數據。不過該 Actuator 服務默認是關閉的,需要通過 Spring Boot 的配置打開。清單 12 中的 application.yml 文件給出了如何打開該服務的示例。

清單 12. 啟用 Prometheus 服務端點的 application.yml 文件
1
2
3
4
5
management:
   endpoints:
     web:
       exposure:
         include: "*"

結束語

作為良好 Java 應用中的重要一環,性能指標數據的收集已經是應用中不可或缺的部分。正如 SLF4J 在 Java 日志記錄中的作用一樣,Micrometer 為 Java 平台上的性能指標數據收集提供了一個通用的可依賴的 API,避免了可能的供應商鎖定問題。利用 Micrometer 提供的多種計量器,可以收集多種類型的性能指標數據,並通過計量器注冊表發送到不同的監控系統。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM