Prometheus 監(jiān)控實踐:Java 開發(fā)者如何通過 PromQL 和 Grafana 優(yōu)化監(jiān)控策略
因為近期工作比較忙碌,所以文章的更新相對慢了一些,近期筆者集成了一些比較核心的監(jiān)控指標(biāo)交由Prometheus采集,并通過promQL進(jìn)行查詢分析實現(xiàn)圖表渲染。因為這一套完整的監(jiān)控流程涉及計量器指標(biāo)采集再通過Prometheus構(gòu)建時間序列,再通過grafana結(jié)合promQL查詢渲染,所以了解每一個環(huán)節(jié)的實現(xiàn)和理念,才能準(zhǔn)確串聯(lián)上述流程。
遺憾的是,就筆者近期了解的情況來看,這方面的資料要么面向全流程搭建的新手教程,要么就是非常突兀的promQL基本說明,并沒有做到筆者所認(rèn)為的全流程泛化梳理,所以筆者打算綜合這些理念,結(jié)合一個比較有代表意義的案例將這些概念串聯(lián),以幫助讀者更好的理念和運(yùn)用監(jiān)控。

一、案例項目前置說明
本文通過spring boot web項目作為演示案例,所有的采集指標(biāo)都會通過Prometheus數(shù)據(jù)源發(fā)布到rgafana上并通過promQL進(jìn)行增強(qiáng)渲染,所以該項目主要會引入暴露springboot監(jiān)控指標(biāo)進(jìn)行和prometheus套件依賴:
<!--暴露spring監(jiān)控指標(biāo)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.4.1</version>
</dependency>
<!--用于導(dǎo)出prometheus系統(tǒng)類型的指標(biāo)數(shù)據(jù)-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.1.4</version>
</dependency>同時筆者也變寫了一個測試的TestController:
- 聲明計時器采集test0的耗時(本質(zhì)上通過休眠模擬)指標(biāo)
- 其余兩個接口通過@Timed注解收集接口時間維度的各項指標(biāo)
@RestController
@Slf4j
public class TestController {
@Autowired
private MeterRegistry registry;
private Timer timer;
@PostConstruct
private void init() {
//名稱設(shè)置為http.timer,標(biāo)簽設(shè)置為uri為/hello,選用合適的名稱輔助開發(fā)推斷理解
timer = Timer
.builder("http.timer")
.publishPercentiles(0.1, 0.5, 0.95) //發(fā)布百分位數(shù)區(qū)間
.description("接口請求耗時統(tǒng)計") // 指標(biāo)的描述
.tags("uri", "/hello") // url標(biāo)簽指明為hello
.register(registry);
}
@GetMapping("/test0")
public String function() {
timer.record(RandomUtil.randomInt(200), TimeUnit.MILLISECONDS);
return "test0";
}
@GetMapping("/test1")
@Timed
public String test1() {
ThreadUtil.sleep(RandomUtil.randomInt(200));
return "test1";
}
@GetMapping("/test2")
@Timed
public String test2() {
ThreadUtil.sleep(RandomUtil.randomInt(200));
return "test2";
}
}因為用到的計時器注解 @Timed,所以我們還需要配置TimedAspect創(chuàng)建注解的代理是使之生效:
@Configuration
public class TimedConfiguration {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}這里筆者也簡單普及一下TimedAspect這個切面的工作原理,在springboot進(jìn)行自動裝配的時候掃描到TimedAspect,該切面會針對所有所有帶有Timed注解的bean的方法做一個環(huán)繞增強(qiáng),在連接點(diǎn)前后記錄耗時并通過Timer記錄耗時到計時器中:

對應(yīng)的TimedAspect的切點(diǎn)實現(xiàn)timedMethod如下:
@Around("execution (@io.micrometer.core.annotation.Timed * *.*(..))")
public Object timedMethod(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Timed timed = method.getAnnotation(Timed.class);
//通過注解獲取方法的元信息
if (timed == null) {
method = pjp.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());
timed = method.getAnnotation(Timed.class);
}
final String metricName = timed.value().isEmpty() ? DEFAULT_METRIC_NAME : timed.value();
//啟動計時器
Timer.Sample sample = Timer.start(registry);
String exceptionClass = "none";
try {
//調(diào)用方法
return pjp.proceed();
} catch (Exception ex) {
//......
} finally {
try {
//記錄方法耗時
sample.stop(Timer.builder(metricName)
.description(timed.description().isEmpty() ? null : timed.description())
.tags(timed.extraTags())
.tags(EXCEPTION_TAG, exceptionClass)
.tags(tagsBasedOnJoinPoint.apply(pjp))
.publishPercentileHistogram(timed.histogram())
.publishPercentiles(timed.percentiles().length == 0 ? null : timed.percentiles())
.register(registry));
} catch (Exception e) {
// ignoring on purpose
}
}最后就是指標(biāo)暴露和端口發(fā)布的配置:
server.port=8080
spring.application.name=web-service
# 暴露并開啟所有的端點(diǎn),Spring Boot Actuator會自動配置一個 URL 為 /actuator/Prometheus 的 HTTP 服務(wù)來供 Prometheus 抓取數(shù)據(jù)
management.endpoints.web.exposure.include=*
# 展示所有的健康信息
management.endpoint.health.show-details=always
# 默認(rèn)/actuator/Prometheus,添加這個tag方便區(qū)分不同的工程
management.metrics.tags.applicatinotallow=${spring.application.name}
# Actuator 監(jiān)控端點(diǎn)獨(dú)立端口設(shè)置為 18080(與主應(yīng)用端口分離)
management.server.port=18080二、詳解各大計量器工作原理
1. counter(計數(shù)器)
(1) 應(yīng)用場景
counter從名字即可了解這個計量器本質(zhì)上是一個只增不減的計數(shù)器,它是有狀態(tài)的(即依賴于歷史的值),從使用方法上來看,它是單調(diào)遞增的且上界是不可確定的,所以使用counter進(jìn)行監(jiān)控的指標(biāo)一般是需要存在不斷累加且需要針對累加的趨勢進(jìn)行分析的。
最典型的場景就是接口請求總數(shù),例如我們需要針對上述的test1接口請求進(jìn)行計數(shù),從而構(gòu)成時間序列存儲這些數(shù)據(jù),同時針對單位時間內(nèi)這個接口增量趨勢進(jìn)行分析,主流的做法就是通過counter采集每一次請求,并將該指標(biāo)通過prometheus交給grafana通過promQL進(jìn)行即席查詢分析:

(2) 使用示例
針對counter計數(shù)器的核心本質(zhì),即只要做到針對并發(fā)請求進(jìn)行高效計數(shù)即可,其余一些分析維度的工作全部交由prometheus等數(shù)據(jù)源進(jìn)行定期的采集分析即可,對應(yīng)筆者項目中的應(yīng)用方式就如下這個環(huán)繞切面的代碼段:
- 攔截所有帶有http注解的接口
- 拉取該接口方法名并生成標(biāo)簽
- 針對該url的counter進(jìn)行累加
@Around("execution(@org.springframework.web.bind.annotation.GetMapping * *(..)) || " +
"execution(@org.springframework.web.bind.annotation.PostMapping * *(..)) || " +
"execution(@org.springframework.web.bind.annotation.RequestMapping * *(..))")
public Object countHttpRequest(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拉取接口方法名
String url = extractUrl(method);
//針對url創(chuàng)建切面
Counter counter = Counter.builder("http_requests_total")
.tag("url", url)
.register(meterRegistry);
//針對接口請求數(shù)進(jìn)行累加
counter.increment();
return joinPoint.proceed();
}(3) 核心原理
所以其實現(xiàn)的核心主要還是強(qiáng)調(diào)計數(shù)的準(zhǔn)確性和高效性,考慮到counter在并發(fā)場景下更多是針對計數(shù)進(jìn)行累加,且只有在grafana等監(jiān)控系統(tǒng)查詢時才需要獲取計數(shù)值,所以針對這種寫多讀少且需要保證并發(fā)安全的場景。
所以counter底層采用了基于數(shù)組分散并發(fā)累加壓力的計數(shù)器DoubleAdder:

對此我們可以查看counter底層的源碼實現(xiàn)即PrometheusCounter的increment印證這一點(diǎn):
public class PrometheusCounter extends AbstractMeter implements Counter {
private DoubleAdder count = new DoubleAdder();
//......
@Override
public void increment(double amount) {
if (amount > 0)
//通過DoubleAdder完成并發(fā)累加
count.add(amount);
}
}2. guage(儀表類型)
(1) 應(yīng)用場景
guage也是我們常用的儀表盤,和counter有所不同,guage計數(shù)器可以增減,它是無狀態(tài)的,即此刻的數(shù)值與歷史數(shù)值并沒有依賴關(guān)系,它更常用于觀察帶有上下界的指標(biāo),即側(cè)重于那些系統(tǒng)狀態(tài)的指標(biāo):
- cpu利用率
- 內(nèi)存利用率
- 網(wǎng)絡(luò)帶寬
所以針對gauge的使用理念,我們也還是通過gauge采集單位時間下的指標(biāo)的數(shù)值,然后交給grafana讓其通過promQL分析其增減趨勢亦或者上下浮動情況以準(zhǔn)確針對系統(tǒng)情況進(jìn)行深入分析:

(2) 使用示例
類似的我們通過spring boot的registry全局注冊一個guage,通過原子類進(jìn)行設(shè)置值,即直接通過原子類記錄當(dāng)前cpu使用率,讓prometheus定期采集交給grafana進(jìn)行即席查詢分析:
AtomicInteger cost = registry.gauge("cpu.usage", Tags.of("core-number", "0"), new AtomicInteger(0));
//隨機(jī)數(shù)模擬cpu使用率
cost.set(RandomUtil.randomInt(100));(3) 工作原理
我們注冊gauge的時候通過構(gòu)造函數(shù)指明底層采用AtomicInteger進(jìn)行數(shù)值維護(hù),后續(xù)我們就可以直接操作這個原子類引用完成數(shù)值維護(hù)修改:
AtomicInteger cost = registry.gauge("cpu.usage", Tags.of("core-number", "0"), new AtomicInteger(0));為什么可以采用原子類AtomicInteger呢?查看gauge底層實現(xiàn),從源碼可以看到該參數(shù)為泛型T只需繼承Number類,保證獲取數(shù)值時可以通過doubleValue方法返回值即可。
而AtomicInteger恰好繼承Number類且保證并發(fā)計數(shù)安全,所以才適用于作為gauge底層的計數(shù)器:
@Nullable
public <T extends Number> T gauge(String name, Iterable<Tag> tags, T number) {
return gauge(name, tags, number, Number::doubleValue);
}結(jié)合這個泛型構(gòu)造,我們也可以直接采用LongAdder作為gauge底層的計量器,由于其底層采用數(shù)組三列并發(fā)累加壓力,所以更適用于作為監(jiān)控計數(shù)的指標(biāo):
LongAdder gauge = registry.gauge("cpu.usage", Tags.of("core-number", "0"), new LongAdder());3. timer
(1) 應(yīng)用場景
timer計時器主要是跟蹤大量短耗時的事件進(jìn)行多維度的采集,通過計時器統(tǒng)計某個事件耗時時,其底層會維護(hù)針對此事件:
- 事件總數(shù):采用LongAdder維護(hù)
- 事件總耗時:同樣采用LongAdder維護(hù)
- 事件耗時最大值:通過TimeWindowMax時間窗口進(jìn)行維護(hù)
后續(xù)我們就可以通過prometheus構(gòu)成時間序列將其交給grafana,此時我們就可以根據(jù)這些指標(biāo)計算:
- 當(dāng)前一段時間請求總數(shù)
- 當(dāng)前一段時間的平均耗時
- 當(dāng)前一段時間的最大值

(2) 使用示例
對應(yīng)的用法上文已經(jīng)介紹過,我們可以自定義注冊一個timer,后續(xù)直接用這個timer的record方法記錄耗時:
@Autowired
private MeterRegistry registry;
private Timer timer;
@PostConstruct
private void init() {
//名稱設(shè)置為http.timer,標(biāo)簽設(shè)置為uri為/hello,選用合適的名稱輔助開發(fā)推斷理解
timer = Timer
.builder("http.timer")
.publishPercentiles(0.5, 0.95) //發(fā)布百分位數(shù)區(qū)間
.description("接口請求耗時統(tǒng)計") // 指標(biāo)的描述
.tags("url", "/test0") // url標(biāo)簽指明為hello
.register(registry);
}耗時記錄使用示例如下:
@GetMapping("/test0")
public String function() {
int sleepTime = RandomUtil.randomInt(200);
ThreadUtil.sleep(sleepTime);
timer.record(sleepTime, TimeUnit.MILLISECONDS);
return "test0";
}(3) 工作原理
關(guān)于timer針對上述三個度量指標(biāo),從上文表述我們就知道大體就是通過:
- count記錄請求總數(shù)
- totalTime記錄總耗時
- max窗口工具類維護(hù)最大耗時
對應(yīng)的我們也可以通過timer底層實現(xiàn)PrometheusTimer印證這一點(diǎn):
public class PrometheusTimer extends AbstractTimer {
//......
//記錄請求總數(shù)
private final LongAdder count = new LongAdder();
//記錄總耗時
private final LongAdder totalTime = new LongAdder();
//窗口內(nèi)記錄最大耗時
private final TimeWindowMax max;
//......
}當(dāng)我們的通過timer記錄本次接口耗時,record方法本質(zhì)做的是:
- count原子自增請求總數(shù)
- totalTime原子累加記錄總耗時
- max通過一個環(huán)形緩沖區(qū)維護(hù)1min以內(nèi)請求的最大值
對應(yīng)第一點(diǎn)和第二點(diǎn)都是簡單的原子累加操作,這里就不多做贅述了,我們著重的說明一下最大耗時這個操作的底層工作原理,這個記錄最大值的工具類TimeWindowMax本質(zhì)上是用一個喚醒緩沖區(qū)實現(xiàn)(本質(zhì)上就是一個數(shù)組),數(shù)組3個元素分別代表:
- 當(dāng)前1min內(nèi)的最大值
- 當(dāng)前2min內(nèi)的最大值
- 當(dāng)前3min內(nèi)的最大值:

我們都知道這個max計數(shù)器記錄的都是當(dāng)前1min內(nèi)耗時最大的值,假設(shè)我們當(dāng)前這分鐘的最大值為200ms,那么ringbuffer[0]記錄的最大值就是200ms。 注意:TimeWindowMax維護(hù)最大值是會遍歷數(shù)組中每個元素進(jìn)行比對,然后將最大值寫入:

一旦ringbuffer[0]使用時間超過1min,例如當(dāng)前時間是3:50距離ringbuffer[0]使用開始時間1:00已經(jīng)超過170s,TimeWindowMax就會執(zhí)行如下步驟:
- 計算時間差為170,已經(jīng)超過ringbuffer[0]的窗口區(qū)間(當(dāng)前1min內(nèi)的最大值),所以將該原子類重置為0,指針移動到ringbuffer[1]
- ringbuffer[1]代表當(dāng)前2min內(nèi)的最大值,170s也大于其窗口時間區(qū)間120s,所以這個窗口也過期直接重置為0,指針移動到ringbuffer[1]
- ringbuffer[2]代表當(dāng)前3min內(nèi)的數(shù)組,對應(yīng)窗口活躍保質(zhì)期為180s大于170s,所以沒過期
所以ringbuffer[2]這個窗口后續(xù)作為當(dāng)前1min內(nèi)的窗口,其他窗口循環(huán)重置后循環(huán)復(fù)用作為當(dāng)前2min、3min內(nèi)的窗口,這就是這個算法的巧妙所在:

對應(yīng)的我們也可以通過源碼印證這一點(diǎn),可以看到timer底層調(diào)用record入口來自AbstractTimer,這個抽象類對外暴露recordNonNegative這個抽象方法,對應(yīng)也就是我們的工具類PrometheusTimer的recordNonNegative方法:
@Override
public final void record(long amount, TimeUnit unit) {
if (amount >= 0) {
//......
//記錄請求總數(shù)、耗時、最大值
recordNonNegative(amount, unit);
//......
}
}查看PrometheusTimer的recordNonNegative可以發(fā)現(xiàn)他做了如下三件事:
- counter自增維護(hù)請求總數(shù)
- totalTime累加計算總耗時
- max.record記錄最大耗時
@Override
protected void recordNonNegative(long amount, TimeUnit unit) {
//累計請求總數(shù)
count.increment();
long nanoAmount = TimeUnit.NANOSECONDS.convert(amount, unit);
//累加總耗時
totalTime.add(nanoAmount);
//維護(hù)最大值
max.record(nanoAmount, TimeUnit.NANOSECONDS);
//......
}關(guān)于請求和耗時累計邏輯比較直觀,筆者就不多做介紹了,步入max的record就可以看到核心所在:
- 調(diào)用rotate執(zhí)行我們上述圖解的窗口滑動算法整理三個窗口
- 基于必要整理重置后的窗口數(shù)組和當(dāng)前耗時進(jìn)行比對,維護(hù)最新的最大值
public void record(double sample, TimeUnit timeUnit) {
//窗口旋轉(zhuǎn)維護(hù)
rotate();
//遍歷各個緩沖區(qū)并維護(hù)最大值
final long sampleNanos = (long) TimeUtils.convert(sample, timeUnit, TimeUnit.NANOSECONDS);
for (AtomicLong max : ringBuffer) {
updateMax(max, sampleNanos);
}
}查看rotate源碼中可以看到,rotate就是實現(xiàn)窗口旋轉(zhuǎn)的核心,其內(nèi)部做了如下幾件事:
- 它會獲取當(dāng)前時間距離上次窗口旋轉(zhuǎn)時間,判斷是否超期,若超過60s則說明存在過期窗口需要滑動窗口,進(jìn)入步驟2
- cas上鎖保證只有一個線程執(zhí)行此操作
- 遍歷各個元素,通過距離上次旋轉(zhuǎn)時間timeSinceLastRotateMillis不斷循環(huán)減去60s和durationBetweenRotatesMillis比較以做到
1. 第1次循環(huán)減去0個60,即查看第一個窗口得到的timeSinceLastRotateMillis是否超過60,若超過則說明過期
2. 第2次循環(huán)減去1個60,即查看第2個窗口得到的timeSinceLastRotateMillis-60s是否超過60(即是否超過2min),若超過則說明過期
3. ......完成窗口重置和滑動后,將本次的耗時分別于各個窗口進(jìn)行比對,如果比窗口值大則直接寫入,這個算法比較巧妙,讀者可以結(jié)合筆者的說明自行理解:
private void rotate() {
//計算上次旋轉(zhuǎn)窗口的時間
long timeSinceLastRotateMillis = clock.wallTime() - lastRotateTimestampMillis;
//如果沒有超過60s則返回
if (timeSinceLastRotateMillis < durationBetweenRotatesMillis) {
// Need to wait more for next rotation.
return;
}
//上個自旋鎖保證并發(fā)互斥,進(jìn)行窗口滑動操作
if (!rotatingUpdater.compareAndSet(this, 0, 1)) {
// Being rotated by other thread already.
return;
}
try {
int iterations = 0;
synchronized (this) {
do {
//重置當(dāng)前窗口
ringBuffer[currentBucket].set(0);
//移動到下一個窗口,如果超過上界則回到索引0位置
if (++currentBucket >= ringBuffer.length) {
currentBucket = 0;
}
//減去60s查看這個窗口是否超過區(qū)間,因為是do while循環(huán),所以多次循環(huán)就可以做到查看60s、120s(循環(huán)1次減去一個60和60進(jìn)行比對)、180s(循環(huán)2次減去2個60和60進(jìn)行比對)對應(yīng)的3個緩沖區(qū)是否過期
timeSinceLastRotateMillis -= durationBetweenRotatesMillis;
//上次滑動窗口時間加上60s,即代表這個窗口區(qū)間理論上的旋轉(zhuǎn)窗口時間
lastRotateTimestampMillis += durationBetweenRotatesMillis;
//
} while (timeSinceLastRotateMillis >= durationBetweenRotatesMillis && ++iterations < ringBuffer.length);
}
} finally {
rotating = 0;
}
}三、詳解promQL
1. promQL 指標(biāo)的基本構(gòu)成說明
在正式介紹promQL表達(dá)式之前,我們需要先針對Prometheus風(fēng)格的指標(biāo)構(gòu)成進(jìn)行一下必要的對齊:
- #號部分為必要的描述和注釋說明,如下注釋分別對應(yīng)我們自定義的指標(biāo)描述和Prometheus的計量器說明(本例則是guage)
- jvm_threads_states_threads為指標(biāo)名稱
- 后續(xù){}部分則是針對jvm_threads_states_threads各個不同維度區(qū)分的標(biāo)簽
# HELP jvm_threads_states_threads The current number of threads having NEW state
# TYPE jvm_threads_states_threads gauge
jvm_threads_states_threads{applicatinotallow="web-service",state="blocked",} 0.0
jvm_threads_states_threads{applicatinotallow="web-service",state="waiting",} 23.0
jvm_threads_states_threads{applicatinotallow="web-service",state="runnable",} 11.0
jvm_threads_states_threads{applicatinotallow="web-service",state="timed-waiting",} 4.0
jvm_threads_states_threads{applicatinotallow="web-service",state="new",} 0.0
jvm_threads_states_threads{applicatinotallow="web-service",state="terminated",} 0.0我們以jvm_threads_states_threads{applicatinotallow="web-service",state="blocked",} 0.0為例說明一下,這是一個典型的Prometheus風(fēng)格的指標(biāo)值,通過標(biāo)簽名限定當(dāng)前指標(biāo)的語義,例如jvm_threads_states_threads就代表不同狀態(tài)的線程數(shù),同時通過標(biāo)簽聲明指標(biāo)的維度,以該指標(biāo)為例,則是通過應(yīng)用名稱application和狀態(tài)state區(qū)分單指標(biāo)下不同維度的數(shù)值,最后就是指標(biāo)的數(shù)值:

2. promQL常見表達(dá)式
(1) promQL核心概念
瞬時向量(Instant vector):一組時間序列上,每個時間上只有一個樣本,他們共享相同的時間戳,即表達(dá)式的返回值只會包含該時間中的最新的樣本值:

區(qū)間向量(Range vector):即一個時間范圍內(nèi)的每個時間序列包含一段時間范圍內(nèi)的樣本數(shù)據(jù):

時間向量:以時間為橫坐標(biāo),序列作為縱坐標(biāo)構(gòu)成一組反應(yīng)狀態(tài)變化的向量圖,該向量圖通過定時周期性采集,隨著時間的流逝生成一個離散的樣本數(shù)據(jù)序列。 通過指標(biāo)名稱結(jié)合標(biāo)簽生成多條趨勢線條,也就是多條時間序列,而序列也就是我們常說的vector:

(2) 匹配表達(dá)式
有了上述對于計量器的基本介紹,我們在針對promQL中幾個比較常見的表達(dá)式和函數(shù)展開介紹,promQL中也存在著邏輯表達(dá)式,這其中涉及匹配表達(dá)式和邏輯表達(dá)式。
我們先來說說匹配表達(dá)式:
- 完全匹配:與字符串完全匹配即=
- 不匹配:與字符串不匹配即!=
- 正則匹配:與字符串正則匹配=~
- 正則反向過濾:與字符串正則不匹配!~
它可以針對多維度的指標(biāo)進(jìn)行篩選和檢索,例如我們從spring actuator上看到j(luò)vm線程各個狀態(tài)的指標(biāo)及其對應(yīng)的線程數(shù):
# HELP jvm_threads_states_threads The current number of threads having NEW state
# TYPE jvm_threads_states_threads gauge
jvm_threads_states_threads{applicatinotallow="web-service",state="blocked",} 0.0
jvm_threads_states_threads{applicatinotallow="web-service",state="waiting",} 23.0
jvm_threads_states_threads{applicatinotallow="web-service",state="runnable",} 11.0
jvm_threads_states_threads{applicatinotallow="web-service",state="timed-waiting",} 4.0
jvm_threads_states_threads{applicatinotallow="web-service",state="new",} 0.0
jvm_threads_states_threads{applicatinotallow="web-service",state="terminated",} 0.0默認(rèn)情況下,它在Prometheus的console渲染顯示如下:

如果我們希望只希望查看timed-waiting的線程數(shù),此時我們就可以通過標(biāo)簽結(jié)合相等匹配器實現(xiàn),對應(yīng)的表達(dá)式為:
jvm_threads_states_threads{state="timed-waiting"}此時視圖就會準(zhǔn)確過濾篩選出狀態(tài)為timed-waiting的線程數(shù):

同理過濾出狀態(tài)非timed-waiting的表達(dá)式為:
jvm_threads_states_threads{state!="timed-waiting"}同理,如果我們希望匹配r開頭的表達(dá)式則是:
jvm_threads_states_threads{state=~"r.*"}(3) 邏輯表達(dá)式
promQL也存在和各種邏輯運(yùn)算的表達(dá)式匹配:
- and:即兩個序列即上述的vector進(jìn)行與運(yùn)算產(chǎn)生新的集合,只有兩個即可都存在的元素才會顯示
- or:只要左右任何一邊的vector表達(dá)式計算為真,就顯示左右vector的所有元素
- unless:即左右兩邊的vector進(jìn)行或運(yùn)算構(gòu)成新的并集,然后通過unless右邊的vector進(jìn)行過濾,將右邊vector存在的元素移除
對此我們不妨距離說明,關(guān)于邏輯表達(dá)式我們以一個針對http請求數(shù)計算的指標(biāo)http_requests_total為例進(jìn)行演示,對應(yīng)指標(biāo)如下:
# HELP http_requests_total
# TYPE http_requests_total counter
http_requests_total{applicatinotallow="web-service",url="/test2",} 1.0
http_requests_total{applicatinotallow="web-service",url="/test0",} 2.0
http_requests_total{applicatinotallow="web-service",url="/test1",} 340.0如果我們希望查詢請求數(shù)大于300且映射以test開頭,在promQL表達(dá)式則是采用and,對應(yīng)的表達(dá)式如下,最終的輸出結(jié)果也是:
http_requests_total > 300 and http_requests_total{url=~"/test.*"}最終輸出的也是test1:

同理如果希望查詢請求數(shù)大于300或者映射為test0,則表達(dá)式如下:
http_requests_total > 300 or http_requests_total{url="/test0"}需要注意的是promQL表達(dá)式中的or并非短路運(yùn)算,即表達(dá)式為真的情況下,左右vector都會輸出,也就是大于請求數(shù)大于300和test0映射都會輸出:

最后則是unless,相較于常規(guī)的邏輯表達(dá)式,該邏輯表達(dá)式的執(zhí)行邏輯為將左右或運(yùn)算得到交集后,結(jié)果交由右邊過濾得出目標(biāo)標(biāo)簽數(shù)據(jù),例如我們需要查詢出請求總數(shù)大于0但要排除test0,對應(yīng)的表達(dá)式就如下所示:
http_requests_total > 0 unless http_requests_total{url="/test0"}對應(yīng)的推算過程為:
- 將請求數(shù)大于0和/test0的指標(biāo)通過或運(yùn)算構(gòu)成新集合即/test0、/test1、/test2
- 基于右邊vector將非test0的元素過濾,最終得到/test1和/test2:

3. 常見函數(shù)
(1) 聚合函數(shù)
接下來就是介紹一些比較常見的函數(shù),和常見的sql語句一樣,promQL也有如下常見內(nèi)置函數(shù):
- sum:指標(biāo)求和
- avg:指標(biāo)平均數(shù)
- max:指標(biāo)最大值
- min:指標(biāo)最小值
我們還是以http_requests_total為例,對應(yīng)不同接口的請求總數(shù)如下:
# HELP http_requests_total
# TYPE http_requests_total counter
http_requests_total{applicatinotallow="web-service",url="/test2",} 1.0
http_requests_total{applicatinotallow="web-service",url="/test0",} 2.0
http_requests_total{applicatinotallow="web-service",url="/test1",} 340.0假設(shè)我們希望定位出http_requests_total的最大值,對應(yīng)的就可以使用max(http_requests_total),其余函數(shù)同理,這些函數(shù)本質(zhì)上就是基于當(dāng)前指標(biāo)通過函數(shù)聚合計算,比較簡單筆者就不多做演示了。
(2) 時間樣本分析常用函數(shù)
對于監(jiān)控來說,我們更希望看到監(jiān)控指標(biāo)的整體趨勢,觀察系統(tǒng)的動態(tài)以便進(jìn)行針對性的調(diào)優(yōu),這其中常見的函數(shù)有:
- max_over_time:指定一段時間的最大值
- avg_over_time:指定一段時間的平均值
- min_over_time:指定一段時間的最小值
- rate:計算指定時間范圍內(nèi)平均每秒增長率
- delta:觀察系統(tǒng)一段時間指標(biāo)上下浮動差
假設(shè)我們通過timer維護(hù)一份基于時間維度的各個接口耗時、請求總數(shù)、最大值等信息:
# HELP method_timed_seconds
# TYPE method_timed_seconds summary
method_timed_seconds_count{applicatinotallow="web-service",class="com.sharkchili.controller.TestController",exceptinotallow="none",method="test2",} 1.0
method_timed_seconds_sum{applicatinotallow="web-service",class="com.sharkchili.controller.TestController",exceptinotallow="none",method="test2",} 0.032457149
method_timed_seconds_count{applicatinotallow="web-service",class="com.sharkchili.controller.TestController",exceptinotallow="none",method="test1",} 840.0
method_timed_seconds_sum{applicatinotallow="web-service",class="com.sharkchili.controller.TestController",exceptinotallow="none",method="test1",} 84.248322878
# HELP method_timed_seconds_max
# TYPE method_timed_seconds_max gauge
method_timed_seconds_max{applicatinotallow="web-service",class="com.sharkchili.controller.TestController",exceptinotallow="none",method="test2",} 0.0
method_timed_seconds_max{applicatinotallow="web-service",class="com.sharkchili.controller.TestController",exceptinotallow="none",method="test1",} 0.198215155若我們希望查看過去1h耗時的最大值分布,對應(yīng)表達(dá)式為 max_over_time(method_timed_seconds_max[1h]),對應(yīng)輸出結(jié)果如下,其余平均值、最小值也是同理。

這其中還有用一個針對指標(biāo)整體浮動變化的函數(shù)delta,例如我們有一個cpu的guage指標(biāo):
# HELP system_cpu_usage The "recent cpu usage" for the whole system
# TYPE system_cpu_usage gauge
system_cpu_usage{applicatinotallow="web-service",} 0.005636978579481398如果我們希望通過cpu浮動情況判斷程序資源消耗穩(wěn)定性就可以通過delta即delta(system_cpu_usage[2h])檢測過去2h的cpu浮動變化:

我們在介紹一下比較實用的函數(shù),針對請求接口總數(shù)這種單向攀升的指標(biāo),我們也會關(guān)注它的增長趨勢已判斷服務(wù)器整體資源是否符合未來增長趨勢,我們就可以通過rate函數(shù)來分析如下接口請求總數(shù)指標(biāo):
# HELP http_requests_total
# TYPE http_requests_total counter
http_requests_total{applicatinotallow="web-service",url="/test2",} 1.0
http_requests_total{applicatinotallow="web-service",url="/test0",} 2.0
http_requests_total{applicatinotallow="web-service",url="/test1",} 840.0對應(yīng)表達(dá)式為rate(http_requests_total[1h]),對應(yīng)輸出發(fā)布圖如下,我們可以非常直觀的看到test1接口在單位時間內(nèi)瘋狂的攀升:

四、實踐——promQL與grafana的串聯(lián)
1. 長尾問題說明
監(jiān)控的目的本質(zhì)上是針對指標(biāo)的趨勢分析確保能夠?qū)ο到y(tǒng)有一個準(zhǔn)確的決策優(yōu)化思路,這其中就有一個比較經(jīng)典的長尾問題,以我們監(jiān)控接口耗時為例,1min內(nèi)平均耗時為200ms,但是偶發(fā)出現(xiàn)5s,這種偶發(fā)波動對于rate等函數(shù)進(jìn)行平均化之后就會被削平,從而無法及時的發(fā)現(xiàn)偶發(fā)飆升的數(shù)值進(jìn)而無法及時發(fā)現(xiàn)問題,這種情況也就是長尾問題。
對于此類問題,我們就需要綜合指標(biāo)多維度針對指標(biāo)進(jìn)行圖表分析,從而進(jìn)行準(zhǔn)確的進(jìn)一步?jīng)Q策。我們還是以接口請求總數(shù)的指標(biāo)為例,假設(shè)此時我們收到接口的請求總數(shù)counter情況如下,可以看到有大量請求打到test1上,所以test1的請求總數(shù)為910:
# HELP http_requests_total
# TYPE http_requests_total counter
http_requests_total{applicatinotallow="web-service",url="/test2",} 1.0
http_requests_total{applicatinotallow="web-service",url="/test0",} 2.0
http_requests_total{applicatinotallow="web-service",url="/test1",} 910.0針對這些接口,筆者也通過timer計時器針對性的進(jìn)行指標(biāo)采集,還是以test1說明:
- 請求總數(shù)為909(采集時間和上述有些誤差)
- 請求總耗時為91s
- 最大耗時為199ms
# HELP method_timed_seconds
# TYPE method_timed_seconds summary
# ......
method_timed_seconds_count{applicatinotallow="web-service",class="com.sharkchili.controller.TestController",exceptinotallow="none",method="test1",} 909.0
method_timed_seconds_sum{applicatinotallow="web-service",class="com.sharkchili.controller.TestController",exceptinotallow="none",method="test1",} 91.202510617
# HELP method_timed_seconds_max
# TYPE method_timed_seconds_max gauge
# ......
method_timed_seconds_max{applicatinotallow="web-service",class="com.sharkchili.controller.TestController",exceptinotallow="none",method="test1",} 0.199230275對應(yīng)的我們將http_requests_total寫入粘貼到grafana上渲染后如下圖所示:

2. 基于計數(shù)器分析http請求增量情況
我們先通過http_requests_total對接口請求情況進(jìn)行分析,從整體情況來看test1請求在不斷的飆升,所以我們希望針對該接口增量趨勢進(jìn)行分析,于是鍵入rate(http_requests_total{url="/test1"}[1m])分析了test1接口的增長情況。 可以看到整體是一段時間一段時間的波動,按照實際業(yè)務(wù)場景可以是服務(wù)定時任務(wù)在單位時間內(nèi)的feign請求:

3. 基于timer計時器分析接口耗時
看到此波動,就需要關(guān)心這個接口的耗時情況,通過timer計時器的指標(biāo)(method_timed_seconds_)篩選業(yè)務(wù)峰值的時間區(qū)間真是這種飆升的量級請求的各維度耗時進(jìn)行匯總分析,以確定的接口飆升是否存在瓶頸。
首先我們需要查詢接口最大耗時max_over_time(method_timed_seconds_sum[1h])查看過去1h的最大耗時,整體來看基本穩(wěn)定在200ms以內(nèi),符合團(tuán)隊指定的標(biāo)準(zhǔn):

明確沒有存在瓶頸的情況下,我們也需要判斷接口單位時間內(nèi)的平均耗時已確定系統(tǒng)過去一段時間是否穩(wěn)定運(yùn)行,已確定程序或者系統(tǒng)是否存在波動,已明確是否有隱患,表達(dá)式為method_timed_seconds_sum{method="test1"}/method_timed_seconds_count{method="test1"},結(jié)合上述指標(biāo)來看在00:10那一刻請求數(shù)飆升所以那段時間平均耗時增加,請求降下來后耗時也將下來了:

4. 基于gauge分析CPU負(fù)載情況
最后我們還是需要通過分析一下cpu和內(nèi)存使用情況已確定這種飆升對于系統(tǒng)的壓力如何,我們直接通過max_over_time(system_cpu_usage[1h])查看最大開始也就是3%并沒有超過業(yè)界認(rèn)定的瓶頸70%,基本確定沒有問題:

關(guān)于內(nèi)存筆者這里也直接關(guān)聯(lián)到j(luò)vm_memory_used_bytes這個guage,通過avg_over_time(jvm_memory_used_bytes[1h])查看過去1h的使用情況,可以看到在00:10新生代飆升到500m左右完成后直接壓降:

老年帶控制在30m以內(nèi)穩(wěn)定攀升,還未到達(dá)gc臨界點(diǎn),整體來看飆升的接口會很快被gc,所以系統(tǒng)整體情況良好:

為方便筆者直接通過jmap查看當(dāng)前java進(jìn)程情況,可以看到堆內(nèi)存分配的2g左右的堆內(nèi)存,此時的老年代也沒用到最大值僅僅動態(tài)擴(kuò)容到83MB,整體內(nèi)存使用情況良好:
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 2065694720 (1970.0MB)
NewSize = 42991616 (41.0MB)
MaxNewSize = 688390144 (656.5MB)
OldSize = 87031808 (83.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)五、小結(jié)
本文深入分析了java常用計量儀micrometer中:
- 有狀態(tài)累加計量器counter
- 無狀態(tài)儀表盤gauge
- 大量短耗時時間指標(biāo)采集工具timer
基于這些指標(biāo)我們結(jié)合通過promQL函數(shù)進(jìn)行多維度的演示并給出了日常生產(chǎn)故障分析和排查步驟,希望對你有幫助。




























