在上一篇文章(Prometheus在k8s上的安裝與使用)中墨吓,我們?cè)趉8s集群成功搭建了Prometheus服務(wù)。今天纹磺,我們將在springboot2.x中使用prometheus記錄指標(biāo)帖烘。
一、我們需要什么指標(biāo)
對(duì)于DDD橄杨、TDD等秘症,大家比較熟悉了,但是對(duì)于MDD可能就比較陌生了式矫。MDD是Metrics-Driven Development的縮寫(xiě)乡摹,主張開(kāi)發(fā)過(guò)程由指標(biāo)驅(qū)動(dòng),通過(guò)實(shí)用指標(biāo)來(lái)驅(qū)動(dòng)快速采转、精確和細(xì)粒度的軟件迭代聪廉。MDD可使所有可以測(cè)量的東西都得到量化和優(yōu)化,進(jìn)而為整個(gè)開(kāi)發(fā)過(guò)程帶來(lái)可見(jiàn)性故慈,幫助相關(guān)人員快速板熊、準(zhǔn)確地作出決策,并在發(fā)生錯(cuò)誤時(shí)立即發(fā)現(xiàn)問(wèn)題并修復(fù)察绷。依照MDD的理念干签,在需求階段就應(yīng)該考慮關(guān)鍵指標(biāo),在應(yīng)用上線后通過(guò)指標(biāo)了解現(xiàn)狀并持續(xù)優(yōu)化克婶。
有一些基于指標(biāo)的方法論筒严,建議大家了解一下:
- Google的四大黃金指標(biāo):延遲Latency、流量Traffic情萤、錯(cuò)誤Errors鸭蛙、飽和度Saturation
- Netflix的USE方法:使用率Utilization、飽和度Saturation筋岛、錯(cuò)誤Error
- WeaveCloud的RED方法:速率Rate娶视、錯(cuò)誤Errors、耗時(shí)Duration
二、在SrpingBoot中引入prometheus
SpringBoot2.x集成Prometheus非常簡(jiǎn)單肪获,首先引入maven依賴(lài):
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.7.3</version>
</dependency>
<dependency>
<groupId>io.github.mweirauch</groupId>
<artifactId>micrometer-jvm-extras</artifactId>
<version>0.2.2</version>
</dependency>
然后寝凌,在application.properties中將prometheus的endpoint放出來(lái)。
management:
endpoints:
web:
exposure:
include: info,health,prometheus
接下來(lái)就可以進(jìn)行指標(biāo)埋點(diǎn)了孝赫,Prometheus的四種指標(biāo)類(lèi)型此處不再贅述较木,請(qǐng)自行學(xué)習(xí)。一般指標(biāo)埋點(diǎn)代碼實(shí)現(xiàn)上有兩種形式:AOP青柄、侵入式伐债,建議盡量使用AOP記錄指標(biāo),對(duì)于無(wú)法使用aop的場(chǎng)景就只能侵入代碼了致开。常用的AOP方式有:
- @Aspect(通用)
- HandlerInterceptor (SpringMVC的攔截器)
- ClientHttpRequestInterceptor (RestTemplate的攔截器)
- DubboFilter (dubbo接口)
我們選擇通用的@Aspect峰锁,結(jié)合自定義指標(biāo)注解來(lái)實(shí)現(xiàn)。首先自定義指標(biāo)注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodMetrics {
String name() default "";
String desc() default "";
String[] tags() default {};
//是否記錄時(shí)間間隔
boolean withoutDuration() default false;
}
然后是切面實(shí)現(xiàn):
@Aspect
public class PrometheusAnnotationAspect {
@Autowired
private MeterRegistry meterRegistry;
@Pointcut("@annotation(com.smac.prometheus.annotation.MethodMetrics)")
public void pointcut() {}
@Around(value = "pointcut()")
public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
Method targetMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
Method currentMethod = ClassUtils.getUserClass(joinPoint.getTarget().getClass()).getDeclaredMethod(targetMethod.getName(), targetMethod.getParameterTypes());
if (currentMethod.isAnnotationPresent(MethodMetrics.class)) {
MethodMetrics methodMetrics = currentMethod.getAnnotation(MethodMetrics.class);
return processMetric(joinPoint, currentMethod, methodMetrics);
} else {
return joinPoint.proceed();
}
}
private Object processMetric(ProceedingJoinPoint joinPoint, Method currentMethod, MethodMetrics methodMetrics) {
String name = methodMetrics.name();
if (!StringUtils.hasText(name)) {
name = currentMethod.getName();
}
String desc = methodMetrics.desc();
if (!StringUtils.hasText(desc)) {
desc = currentMethod.getName();
}
//不需要記錄時(shí)間
if (methodMetrics.withoutDuration()) {
Counter counter = Counter.builder(name).tags(methodMetrics.tags()).description(desc).register(meterRegistry);
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new IllegalStateException(e);
} finally {
counter.increment();
}
}
//需要記錄時(shí)間(默認(rèn))
Timer timer = Timer.builder(name).tags(methodMetrics.tags()).description(desc).register(meterRegistry);
return timer.record(() -> {
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new IllegalStateException(e);
}
});
}
}
代碼很容易双戳,沒(méi)什么可說(shuō)明的虹蒋,接下來(lái)就是在需要記監(jiān)控的地方加上這個(gè)注解就行,比如:
@MethodMetrics(name="sms_send",tags = {"vendor","aliyun"})
public void send(String mobile, SendMessage message) throws Exception {
...
}
至此飒货,aop形式的指標(biāo)實(shí)現(xiàn)方式就完成了魄衅。如果是侵入式的話,直接使用meterRegistry就行:
meterRegistry.counter("sms.send","vendor","aliyun").increment();
啟動(dòng)服務(wù)膏斤,打開(kāi)http://localhost:8080/actuator/prometheus查看指標(biāo)徐绑。
三、高級(jí)指標(biāo)之分位數(shù)
分位數(shù)(P50/P90/P95/P99)是我們常用的一個(gè)性能指標(biāo)莫辨,Prometheus提供了兩種解決方案:
client側(cè)計(jì)算方案
summery類(lèi)型,設(shè)置percentiles毅访,在本地計(jì)算出Pxx沮榜,作為指標(biāo)的一個(gè)tag被直接收集。
Timer timer = Timer.builder("sms.send").publishPercentiles(0.5, 0.9, 0.95,0.99).register(meterRegistry);
timer.record(costTime, TimeUnit.MILLISECONDS);
會(huì)出現(xiàn)四個(gè)帶quantile的指標(biāo)喻粹,如圖:server側(cè)計(jì)算方案
開(kāi)啟histogram蟆融,將所有樣本放入buckets中,在server側(cè)通過(guò)histogram_quantile函數(shù)對(duì)buckets進(jìn)行實(shí)時(shí)計(jì)算得出守呜。注意:histogram采用了線性插值法型酥,buckets的劃分對(duì)誤差的影響比較大,需合理設(shè)置查乒。
Timer timer = Timer.builder("sms.send")
.publishPercentileHistogram(true)
.serviceLevelObjectives(Duration.ofMillis(10),Duration.ofMillis(20),Duration.ofMillis(50))
.minimumExpectedValue(Duration.ofMillis(1))
.maximumExpectedValue(Duration.ofMillis(100))
.register(meterRegistry);
timer.record(costTime, TimeUnit.MILLISECONDS);
會(huì)出現(xiàn)一堆xxxx_bucket的指標(biāo)弥喉,如圖:
然后,使用
histogram_quantile(0.95, rate(sms_send_seconds_bucket[5m]))
就可以看到P95的指標(biāo)了玛迄,如圖:結(jié)論:方案1適用于單機(jī)或只關(guān)心本地運(yùn)行情況的指標(biāo)欧引,比如gc時(shí)間咒钟、定時(shí)任務(wù)執(zhí)行時(shí)間秘蛇、本地緩存更新時(shí)間等跺株;方案2則適用于分布式環(huán)境下的整體運(yùn)行情況的指標(biāo),比如搜索接口的響應(yīng)時(shí)間马胧、第三方接口的響應(yīng)時(shí)間等。