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 只有在任務完成之后才會記錄時間。更好的選擇是使用 LongTaskTimer
。LongTaskTimer
可以在任務進行中記錄已經耗費的時間,它通過注冊表的 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.5
、0.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 提供的多種計量器,可以收集多種類型的性能指標數據,並通過計量器注冊表發送到不同的監控系統。