微米級標記模式
1.概述
在本文中,我們將探討 Micrometer 指標,並專注於標記。我們將在 Spring Boot 應用程式中使用 Micrometer,並應用各種模式來建立簡單的指標,例如計數器和計時器。
我們將首先使用 Micrometer 的 Builder API 建立具有可變標籤值的計量器。此外,我們將研究MeterProvider
作為一種替代方案,它可以避免潛在的效能問題。我們也將使用 Spring AOP 和 Micrometer 特有的註解,以宣告式的方式記錄方法呼叫。
最後,我們將介紹命名約定和選擇標籤值的最佳實踐,以及應避免的常見陷阱。
2. 使用 Builder API
我們將使用一個簡單的 Spring Boot 服務,其中包含一個名為foo()
的公用函數,該函數將客戶端裝置類型作為String
類型傳入。在本文的程式碼範例中,我們將嘗試對每次呼叫foo()
進行計數和計時,並使用作為參數傳遞的裝置類型標記指標:
@Service
class DummyService {
// logger, constructor
public String foo(String deviceType) {
log.info("foo({})", deviceType);
String response = invokeSomeLogic();
return response;
}
private void invokeSomeLogic() { /* ... */ }
}
首先,讓我們將 Spring Boot Actuator 依賴項新增到我們的pom.xml
中,其中將包含 Micrometer 依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
一種簡單的方法是使用 Micrometer 流暢的 API 來建立不同類型的儀表。當呼叫foo()
時,我們建立一個帶有儀表名稱的Counter.builder
,然後將標籤作為鍵值對添加。
Micrometer 使用MeterRegistry
來管理和儲存我們建立的所有儀表。因此,我們需要將其註入到我們的服務中,並在建構器鏈的最後一步使用它——透過呼叫register()
。這實際上建立並註冊了儀表。
最後,我們只要在已建立的Counter
實例上呼叫increment()
方法:
@Component
class DummyService {
private final MeterRegistry meterRegistry;
// logger, constructor
public String foo(String deviceType) {
log.info("foo({})", deviceType);
Counter.builder("foo.count")
.tag("device.type", deviceType)
.register(meterRegistry)
.increment();
String response = Timer.builder("foo.time")
.tag("device.type", deviceType)
.register(meterRegistry)
.record(this::invokeSomeLogic);
return response;
}
private void invokeSomeLogic() { /* ... */ }
}
可以看出,註冊Timer
與註冊Counter
非常相似。主要差異在於, Timer
不像Counter
那樣使用increment()
,而是使用record()
之類的方法。在本例中,我們使用record()
傳入一個要執行的函數。 Timer Timer
包裝函數調用,測量執行時間,記錄時間,並傳回函數的結果。
需要注意的是,一個計量器由其名稱和一組標籤唯一標識。例如,即使我們使用建構器模式建立多個名為“ foo.count
”的Counter
,並將相同的標籤“ device.type
”設為“ mobile
”,它們實際上也指向同一個底層指標。因此,增加任何一個計數器的值都會更新同一個計數器。
另一方面,這種方法的缺點是每次呼叫foo()
時都會建立建構器物件。根據該方法的呼叫頻率,這可能會增加垃圾收集的開銷,並隨著時間的推移影響效能。
3. 公開指標
現在我們正在監控foo()
方法的調用,我們可以透過 Spring Boot Actuator 公開這些指標。為此,我們更新配置中的management.endpoints.web.exposure.include
屬性並將其設為metrics
,或使用'*'
公開所有端點:
management:
endpoints.web.exposure.include: '*'
因此,我們現在可以在/actuator/metrics/foo.count
和/actuator/metrics/foo.time
查看foo()
的指標。例如,以下是foo.time
公開的內容:
{
"name": "foo.time",
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 100
},
{
"statistic": "TOTAL_TIME",
"value": 5.5068953
},
{
"statistic": "MAX",
"value": 0
}
],
"availableTags": [
{
"tag": "device.type",
"values": [ "mobile", "tablet", "smart_tv", "wearable", "desktop" ]
}
]
}
如我們所見,每個指標也顯示了可用標籤的清單。我們可以使用這些標籤中的任何一個來縮小指標的範圍。例如,要檢查desktop
設備類型的調用,我們可以使用端點/actuator/metrics/foo.time?tag=device.type:desktop
。
4. 使用 Meter Provider
如上所述,使用 Micrometer 的 Builder API 的缺點是每次函數呼叫時都需要建立建構器物件。我們可以利用MeterProvider
API 來避免這個問題。 MeterProvider MeterProvider
模板設計模式的變體,使我們能夠重複使用 Meter 並減少垃圾收集開銷。
在建構子中,我們仍然使用Counter.builder()
,但不再呼叫register()
,而是使用withRegistry(meterRegistry)
變體。這將傳回一個MeterProvider<Counter>
,我們可以將其作為欄位儲存在服務中,並在需要時重複使用。
同樣的方法也適用於其他儀表類型,如Timers
,API 返回MeterProvider<Timer>:
@Service
class DummyService {
private final MeterRegistry meterRegistry;
private final Meter.MeterProvider<Counter> counterProvider;
private final Meter.MeterProvider<Timer> timerProvider;
// logger
public DummyService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.counterProvider = Counter.builder("bar.count")
.withRegistry(meterRegistry);
this.timerProvider = Timer.builder("bar.time")
.withRegistry(meterRegistry);
}
// ...
}
現在,我們可以使用counterProvider
和meterProvider
欄位來記錄指標,同時攔截對新方法的調用,我們稱之為bar()
:
public String bar(String device) {
log.info("bar({})", device);
counterProvider.withTag("device.type", device)
.increment();
String response = timerProvider.withTag("device.type", device)
.record(this::invokeSomeLogic);
return response;
}
如我們所見,我們可以使用MeterProvider
動態新增標籤,然後呼叫特定於儀表的方法,例如Counter
的increment()
或Timer
的record()
。
5.使用AOP
除了 Builder 和MeterProvider
API 之外,我們還可以使用面向方面程式設計 (AOP) 以宣告方式定義儀表。
我們的元件是一個使用@Service
註解的 Spring 管理的 Bean;因此,我們可以利用 Spring 的 AOP 支援自動記錄方法呼叫的指標,避免手動管理計量器。為此,我們只需使用適當的 Micrometer 註解來註解要觀察的方法。
例如,我們可以建立一個新方法buzz()
,並使用 Micrometer 的@Counted
和@Timed:
@Timed("buzz.time")
@Counted("buzz.time")
public String buzz(String device) {
log.info("buzz({})", device);
return invokeSomeLogic();
}
透過這樣做,我們將自動記錄方法的調用,並透過Actuator
公開指標。但是,由於這依賴 AOP,因此對buzz()
的呼叫必須通過 Spring 代理;同一類別內的直接內部呼叫不會被攔截。
@Timed
和@Counted
註解支援動態標記,但需要額外設定。首先,我們需要在組態中啟用註解觀察:
management:
observations.annotations.enabled: true
# ...
此外,我們需要定義一個MeterTagAnnotationHandler
bean 來幫助儀表評估帶有註解的參數。雖然它可以配置為解析 SpEL 表達式,但我們會盡量簡化,並讓它始終傳回帶有註解物件的toString()
方法:
@Bean
MeterTagAnnotationHandler meterTagAnnotationHandler() {
return new MeterTagAnnotationHandler(
aClass -> Object::toString,
aClass -> (exp, param) -> pram.toString()
);
}
最後,我們將使用@MeterTag.
註解buzz()
方法的參數。
@Timed(value = "buzz.time")
@Counted(value = "buzz.count")
public String buzz(@MeterTag("device.type") String device) {
log.info("buzz({})", device);
return invokeSomeLogic();
}
因此,攔截buzz()
呼叫的Timer
將動態附加“device.type”
標籤。
另一方面,如果我們想要配置@Counted
以相同的方式運作,我們需要註冊CountedAspect
bean並手動設定CountedMeterTagAnnotationHandler
實作:
@Bean
public CountedAspect countedAspect() {
CountedAspect aspect = new CountedAspect();
CountedMeterTagAnnotationHandler tagAnnotationHandler = new CountedMeterTagAnnotationHandler(
aClass -> Object::toString,
aClass -> (exp, param) -> "");
aspect.setMeterTagAnnotationHandler(tagAnnotationHandler);
return aspect;
}
6. 最佳實踐和慣例
Micrometer 鼓勵使用描述性的、小寫的名稱,並用點分隔指標和標籤 - 例如“foo.time”
和“device.type”
。指標名稱本身應該提供有意義的上下文,而標籤則為過濾或分組添加額外的維度。
我們應該避免使用像「 service
」這樣過於通用的標籤,因為這些標籤會聚合不相關的指標。通用名稱會使人們難以理解指標的含義,並降低所收集資料的實用性。
我們應該謹慎使用動態標籤值。高度可變的值可能會創建許多獨特的指標組合,從而增加基數並對監控系統造成壓力。在我們的示範中,使用“device.type”
是安全的,因為它的值集有限,可以保持較低的基數。
7. 結論
在本教學中,我們探索了使用變數標籤值建立 Micrometer 指標的不同方法。我們首先了解了指標由其名稱和標籤唯一標識,然後使用 Builder API 手動建立了Counter
和Timer
指標。
接下來,我們發現這種方法會導致每次方法呼叫時都需要建立建構器實例,這會對垃圾收集器造成壓力。為了解決這個問題,我們探索了使用MeterProvider
來重複使用 Meter 並降低開銷。
最後,我們利用 Spring AOP 和 Micrometer 特有的註解(例如@Timed
和@Counted
以宣告式的方式記錄方法呼叫。我們也討論了使用動態標籤和遵循一致命名約定的最佳實務。
與往常一樣,本文中提供的程式碼可在 GitHub 上找到。