使用 Micrometer 記錄 Java 應用效能指標
執行良好的應用離不開對效能指標的收集。這些效能指標可以有效地對生產系統的各方面行為進行監控,幫助運維人員掌握系統執行狀態和查詢問題原因。效能指標監控通常由兩個部分組成:第一個部分是效能指標資料的收集,需要在應用程式程式碼中新增相應的程式碼來完成;另一個部分是後臺監控系統,負責對資料進行聚合計算和提供 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:
micrometer-core micrometer-registry-prometheus micrometer-test
在 Java 應用中,只需要根據所使用的監控系統,新增所對應的模組即可。比如,使用 Prometheus 的應用只需要新增micrometer-registry-prometheus
模組即可。模組micrometer-core
會作為傳遞依賴自動新增。本文使用的 Micrometer 版本是
1.1.1。清單 1 給出了使用 Micrometer 的 Maven 專案的示例:
清單 1. 使用 Micrometer 的 Maven 專案
<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 使用示例
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. 使用全域性計量器登錄檔物件
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. 計量器登錄檔的通用標籤
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. 計數器使用示例
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. 計量儀使用示例
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. 計時器使用示例
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 使用示例
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. 分佈概要使用示例
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 的配置示例
@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 物件的示例
@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 檔案
management: endpoints: web: exposure: include: "*"
結束語
作為良好 Java 應用中的重要一環,效能指標資料的收集已經是應用中不可或缺的部分。正如 SLF4J 在 Java 日誌記錄中的作用一樣,Micrometer 為 Java 平臺上的效能指標資料收集提供了一個通用的可依賴的 API,避免了可能的供應商鎖定問題。利用 Micrometer 提供的多種計量器,可以收集多種型別的效能指標資料,並通過計量器登錄檔傳送到不同的監控系統。