介绍

Prometheus是一套成熟且流行的系统和服务监控系统,它几乎满足了监控的所有能力。 Grafana, 它和Prometheus相比更侧重的是图形化展示,有强大、灵活的仪表盘体系,我们会把基于Prometheus收集的数据作为数据源导入到Grafana。

监控模式

目前,监控系统采集指标有两种方式,一种是『推』,另一种就是『拉』:

推的代表有 ElasticSearch,InfluxDB,OpenTSDB 等,需要你从程序中将指标使用 TCP,UDP 等方式推送至相关监控应用,只是使用 TCP 的话,一旦监控应用挂掉或存在瓶颈,容易对应用本身产生影响,而使用 UDP 的话,虽然不用担心监控应用,但是容易丢数据。

拉的代表,主要代表就是 Prometheus,让我们不用担心监控应用本身的状态。而且可以利用 DNS-SRV 或者 Consul 等服务发现功能就可以自动添加监控。

如何监控

Prometheus 监控应用的方式非常简单,只需要进程暴露了一个用于获取当前监控样本数据的 HTTP 访问地址。这样的一个程序称为Exporter,Exporter 的实例称为一个 Target 。Prometheus 通过轮训的方式定时从这些 Target 中获取监控数据样本,对于应用来讲,只需要暴露一个包含监控数据的 HTTP 访问地址即可,当然提供的数据需要满足一定的格式,这个格式就是 Metrics 格式.

1
metric name>{<label name>=<label value>, ...}

主要分为三个部分 各个部分需符合相关的正则表达式

  • metric name:指标的名称,主要反映被监控样本的含义 a-zA-Z_:*_
  • label name: 标签 反映了当前样本的特征维度 [a-zA-Z0-9_]*
  • label value: 各个标签的值,不限制格式

需要注意的是,label value 最好使用枚举值,而不要使用无限制的值,比如用户 ID,Email 等,不然会消耗大量内存,也不符合指标采集的意义。

四种指标类型

计数器(Counter)

Counter类型指标被用于单调增加的测量结果。因此它们总是累积的数值,值只能上升。唯一的例外是Counter重启,在这种情况下,它的值会被重置为零。

Counter的实际值通常本身并不十分有用。一个计数器的值经常被用来计算两个时间戳之间的delta或者随时间变化的速率。

例如,Counter的一个典型用例是记录API调用次数,这是一个总是会增加的测量值。

1
2
3
# HELP http_requests_total Total number of http api requests
# TYPE http_requests_total counter
http_requests_total{api="add_product"} 4633433

指标名称是http_requests_total,它有一个名为api的标签,值为add_product,Counter的值为4633433。这意味着自从上次服务启动或Counter重置以来,add_product的API已经被调用了4633433次。按照惯例,Counter类型的指标通常以_total为后缀。

这个绝对数字并没有给我们提供多少信息,但当与PromQL的rate函数(或其他监控后端的类似函数)一起使用时,它可以帮助我们了解该API每秒收到的请求数。下面的PromQL查询计算了过去5分钟内每秒的平均请求数。

1
rate(http_requests_total{api="add_product"}[5m])

为了计算一段时期内的绝对变化,我们将使用delta函数,在PromQL中称为increate():

1
increase(http_requests_total{api="add_product"}[5m])

这将返回过去5分钟内的总请求数,这相当于用每秒的速率乘以间隔时间的秒数(在我们的例子中是5分钟):

1
rate(http_requests_total{api="add_product"}[5m]) * 5 * 60

其他你可能会使用Counter类型指标的例子:测量电子商务网站的订单数量,在网络接口上发送和接收的字节数,或者应用程序中的错误数量。如果它是一个会一直上升的指标,那么就使用一个Counter。

下面是一个例子,说明如何使用Prometheus客户端库在Python中创建和增加一个计数器指标:

1
2
3
4
5
6
7
from prometheus_client import Counter
api_requests_counter = Counter(
'http_requests_total',
'Total number of http api requests',
['api']
)
api_requests_counter.labels(api='add_product').inc()

需要注意的是,由于Counter可以被重置为零,你要确保你用来存储和查询指标的后端能够支持这种情况,并且在Counter重启的情况下仍然提供准确的结果。Prometheus和兼容PromQL的Prometheus远程存储系统,如Promscale,可以正确处理Counter重启。

仪表(Gauge)

Gauge指标用于可以任意增加或减少的测量。这是你可能更熟悉的指标类型,因为即使没有经过额外处理的实际值也是有意义的,它们经常被使用到。例如,测量温度、CPU和内存使用的指标,或者队列的大小都是Gauge。

例如,为了测量一台主机的内存使用情况,我们可以使用一个Gauge指标,比如:

1
2
3
# HELP node_memory_used_bytes Total memory used in the node in bytes
# TYPE node_memory_used_bytes gauge
node_memory_used_bytes{hostname="host1.domain.com"} 943348382

上面的指标表明,在测量时,节点host1.domain.com使用的内存约为900 MB。该指标的值是有意义的,不需要任何额外的计算,因为它告诉我们该节点上消耗了多少内存。

与使用Counter指标时不同,rate和delta函数对Gauge没有意义。然而,计算特定时间序列的平均数、最大值、最小值或百分比的函数经常与Gauge一起使用。在Prometheus中,这些函数的名称是avg_over_time、max_over_time、min_over_time和quantile_over_time。要计算过去10分钟内在host1.domain.com上使用的平均内存,你可以这样做:

1
avg_over_time(node_memory_used_bytes{hostname="host1.domain.com"}[10m])

要使用Prometheus客户端库在Python中创建一个Gauge指标,你可以这样做:

1
2
3
4
5
6
7
from prometheus_client import Gauge
memory_used = Gauge(
'node_memory_used_bytes',
'Total memory used in the node in bytes',
['hostname']
)
memory_used.labels(hostname='host1.domain.com').set(943348382)

直方图(Histogram)

Histogram指标对于表示测量的分布很有用。它们经常被用来测量请求持续时间或响应大小。

直方图将整个测量范围划分为一组区间,称为桶,并计算每个桶中有多少测量值。

一个直方图指标包括几个项目:

  • 一个包含测量次数的Counter。指标名称使用_count后缀。

  • 一个包含所有测量值之和的Counter。指标名称使用_sum后缀。

  • 直方图桶被暴露为一系列的Counter,使用指标名称的后缀_bucket和表示桶的上限的le label。Prometheus中的桶是包含桶的边界的,即一个上限为N的桶(即le label)包括所有数值小于或等于N的数据点。

例如,测量运行在host1.domain.com实例上的add_productAPI端点实例的响应时间的Histogram指标可以表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# HELP http_request_duration_seconds Api requests response time in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_sum{api="add_product" instance="host1.domain.com"} 8953.332
http_request_duration_seconds_count{api="add_product" instance="host1.domain.com"} 27892
http_request_duration_seconds_bucket{api="add_product" instance="host1.domain.com" le="0"}
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="0.01"} 0
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="0.025"} 8
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="0.05"} 1672
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="0.1"} 8954
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="0.25"} 14251
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="0.5"} 24101
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="1"} 26351
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="2.5"} 27534
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="5"} 27814
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="10"} 27881
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="25"} 27890
http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com", le="+Inf"} 27892

上面的例子包括sum、counter和12个桶。sum和counter可以用来计算一个测量值随时间变化的平均值。在PromQL中,过去5分钟的平均请求响应时间可以通过如下方式计算得到。

1
rate(http_request_duration_seconds_sum{api="add_product", instance="host1.domain.com"}[5m]) / rate(http_request_duration_seconds_count{api="add_product", instance="host1.domain.com"}[5m])

它也可以被用来计算各时间序列的平均数。下面的PromQL查询将计算出所有API和实例在过去5分钟内的平均请求响应时间。

1
sum(rate(http_request_duration_seconds_sum[5m])) / sum(rate(http_request_duration_seconds_count[5m]))

利用Histogram,你可以在查询时计算单个时间序列以及多个时间序列的百分位。在PromQL中,我们将使用histogram_quantile函数。Prometheus使用分位数而不是百分位数。它们本质上是一样的,但是以0到1的比例表示的,而百分位数是以0到100的比例表示的。要计算在host1.domain.com上运行的add_product API响应时间的第99百分位数(0.99四分位数),你可以使用以下查询。

1
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{api="add_product", instance="host1.domain.com"}[5m]))

Histograms的一大优势是可以进行汇总。下面的查询返回所有API和实例的响应时间的第99个百分点‍:

1
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

在云原生环境中,通常有许多相同组件的多个实例在运行,能否跨实例汇总数据是关键。

Histograms有三个主要的缺点:

  • 首先,桶必须是预定义的,这需要一些前期的设计。

如果你的桶没有被很好地定义,你可能无法计算出你需要的百分比,或者会消耗不必要的资源。例如,如果你有一个总是需要超过一秒钟的API,那么拥有上限(le label)小于一秒钟的桶将是无用的,只会消耗监控后端服务器的计算和存储资源。另一方面,如果99.9%的API请求耗时少于50毫秒,那么拥有一个上限为100毫秒的初始桶将无法让你准确测量API的性能。

  • 第二,他们提供的是近似的百分位数,而不是精确的百分位数。

这通常没什么问题,只要你的桶被设计为提供具有合理准确性的结果。

  • 第三,由于百分位数需要在服务器端计算,当有大量数据需要处理时,它们的计算成本会非常高。

在Prometheus中减轻这种情况的一个方法是使用录制规则来预先计算所需的百分位数。

下面的例子显示了如何使用Prometheus的Python客户端库创建一个带有自定义桶的直方图指标。

1
2
3
4
5
6
7
8
9
10
11
from prometheus_client import Histogram
api_request_duration = Histogram(
name='http_request_duration_seconds',
documentation='Api requests response time in seconds',
labelnames=['api', 'instance'],
buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 25 )
)
api_request_duration.labels(
api='add_product',
instance='host1.domain.com'
).observe(0.3672)

汇总(Summary)

像直方图一样,Summary指标对于测量请求持续时间和响应体大小很有用。

像直方图一样,汇总度量对于测量请求持续时间和响应大小很有用。

一个Summary指标包括这些指标:

  • 一个包含总测量次数的Counter。指标名称使用_count后缀。

  • 一个包含所有测量值之和的Counter。指标名称使用_sum后缀。可以选择使用带有分位数标签的指标名称,来暴露一些测量值的分位数指标。由于你不希望这些量值是从应用程序运行的整个时间内测得的,Prometheus客户端库通常会使用流式的分位值,这些分位值是在一个滑动的(通常是可配置的)时间窗口上计算得到的。

例如,测量在host1.domain.com上运行的add_productAPI端点实例的响应时间的Summary指标可以表示为:

1
2
3
4
5
6
7
8
9
10
# HELP http_request_duration_seconds Api requests response time in seconds
# TYPE http_request_duration_seconds summary
http_request_duration_seconds_sum{api="add_product" instance="host1.domain.com"} 8953.332
http_request_duration_seconds_count{api="add_product" instance="host1.domain.com"} 27892
http_request_duration_seconds{api="add_product" instance="host1.domain.com" quantile="0"}
http_request_duration_seconds{api="add_product" instance="host1.domain.com" quantile="0.5"} 0.232227334
http_request_duration_seconds{api="add_product" instance="host1.domain.com" quantile="0.90"} 0.821139321
http_request_duration_seconds{api="add_product" instance="host1.domain.com" quantile="0.95"} 1.528948804
http_request_duration_seconds{api="add_product" instance="host1.domain.com" quantile="0.99"} 2.829188272
http_request_duration_seconds{api="add_product" instance="host1.domain.com" quantile="1"} 34.283829292

上面这个例子包括总和和计数以及五个分位数。分位数0相当于最小值,分位数1相当于最大值。分位数0.5是中位数,分位数0.90、0.95和0.99相当于在host1.domain.com上运行的add_product API端点响应时间的第90、95和99个百分位。

像直方图一样,Summary指标包括总和和计数,可用于计算随时间的平均值以及不同时间序列的平均值。

Summary提供了比Histogram更精确的百分位计算结果,但这些百分位有三个主要缺点:

  • 首先,客户端计算百分位是很昂贵的。这是因为客户端库必须保持一个有序的数据点列表,以进行这种计算。在Prometheus SDK中的实现限制了内存中保留和排序的数据点的数量,这降低了准确性以换取效率的提高。注意,并非所有的Prometheus客户端库都支持汇总指标中的量值。例如,Python SDK就不支持。

  • 第二,你要查询的量值必须由客户端预先定义。只有那些已经提供了指标的量值才能通过查询返回。没有办法在查询时计算其他百分位。增加一个新的百分位指标需要修改代码,该指标才可以被使用。

  • 第三,也是最重要的一点,不可能把多个Summary指标进行聚合计算。这使得它们对动态现代系统中的大多数用例毫无用处,在这些用例中,通常我们对一个特定的组件感兴趣,这个视角是全局的,它不与特定的实例关联。

因此,想象一下,在我们的例子中,add_product的API端点运行在10个主机上,在这些服务之前有一个负载均衡器。我们没有任何聚合函数可以用来计算add_product API接口在所有请求中响应时间的第99百分位数,无论这些请求被发送到哪个后端实例上。我们只能看到每个主机的第99个百分点。同样地,我们也只能知道某个接口,比如add_productAPI端点的(在某个实例上的)第99百分位数,而不能对不同的接口进行聚合。

下面的代码使用Prometheus的Python客户端库创建了一个Summary指标。

1
2
3
4
5
6
7
from prometheus_client import Summary
api_request_duration = Summary(
'http_request_duration_seconds',
'Api requests response time in seconds',
['api', 'instance']
)
api_request_duration.labels(api='add_product', instance='host1.domain.com').observe(0.3672)

上面的代码没有定义任何量化指标,只会产生总和和计数指标。Prometheus的Python SDK不支持Summary指标中的分位数计算。

Histogram还是Summary?

在大多数情况下,直方图是首选,因为它更灵活,并允许汇总百分位数。

在不需要百分位数而只需要平均数的情况下,或者在需要非常精确的百分位数的情况下,汇总是有用的。例如,在履行关键系统的合约责任的情况下。

下表总结了直方图和汇总表的优点和缺点。

指标信息

以Nodejs + Koa举例, 整合prom-client

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// npm install -S prom-client
import {register, collectDefaultMetrics, Counter, Histogram} from 'prom-client'
// 用于收集推荐的指标,确保collectDefaultMetrics只执行一次
collectDefaultMetrics()

// 建立一个 counter 统计指标
const counter = new Counter({
name: 'http_requests_total',
help: 'Counter for total requests received',
labelNames: ['code', 'path', 'method', 'status'], // labels 为了统计分组, 可以区分不同维度的指标
})
const hrtime2ms = (hrtime) => (hrtime[0] * 1e9 + hrtime[1]) / 1e6 // hrtime 转化成 ms

const histogram = new Histogram({
name: `http_request_duration_ms`,
help: 'Duration of HTTP requests in ms',
labelNames: ['code', 'path', 'method', 'status'],
// 可以减少这里的buckets数量以缓解Prometheus计算资源的紧张
buckets: [5, 10, 25, 50, 100, 250, 500, 1000], // buckets 单位 ms
})

const app = new Koa();
// 监控指标中间件
app.use(async (ctx, next) => {
// 路径 /metrics 为监控指标 route, 返回指标
if (ctx.path === '/metrics') {
ctx.set('Content-Type', register.contentType)
ctx.body = await register.metrics()
return
}
// 其他路由均统计指标
const start = process.hrtime() // 开始时间
try {
await next()
} catch (ignored) {
}
let code
// 优先记录http error status
if (ctx.response.status && ctx.response.status !== 200) {
code = ctx.response.status
} else {
code = ctx.response.body ? ctx.response.body.code : 1
}
counter.inc({
code,
code: ctx.body.code,
path: ctx.path,
method: ctx.method,
status: ctx.status,
}, 1)
const dur = hrtime2ms(process.hrtime(start)) // 计算请求处理时间
histogram.observe({
code: ctx.body.code,
path: ctx.path,
method: ctx.method,
status: ctx.status,
}, dur) // 统计响应时间
})
app.listen(port, '0.0.0.0')
.on('listening', () => {
});

访问/metrics接口会返回以下信息

1
2
3
4
5
6
7
8
9
10
11
......
# HELP http_requests_total Counter for total requests received
# TYPE http_requests_total counter
http_requests_total{code="0",path="/api/manager/v2.0/messages",method="GET",status="200"} 107
http_requests_total{code="0",path="/api/manager/v2.0/tenant/brands",method="GET",status="200"} 47
# HELP http_request_duration_ms Duration of HTTP requests in ms
# TYPE http_request_duration_ms histogram
http_request_duration_ms_bucket{le="5",code="0",path="/api/manager/v2.0/messages",method="GET",status="200"} 0
http_request_duration_ms_bucket{le="10",code="0",path="/api/manager/v2.0/messages",method="GET",status="200"} 0
http_request_duration_ms_sum{code="0",path="/api/manager/v2.0/messages",method="GET",status="200"} 8670.540011999998
http_request_duration_ms_count{code="0",path="/api/manager/v2.0/messages",method="GET",status="200"} 107

#HELP是一个指标的描述文案,可以解释这个指标的功能 #TYPE是一个指标的类型描述,前面的代表这个指标的名称nodejs_heap_space_size_total_bytes 空格后边的代表这个指标的类型,gauge 不带#的就是指标的真实值,它的形式是指标名+值{}中的内容代表了这个指标的label,也就是这个指标还可以再分属性,比如这个指标是nodejs 堆内存的大小,里边还可以根据space属性分成read_only,old等等。 所有的指标都是这种形式。

PromQL

Prometheus 提供了一种称为 PromQL(Prometheus Query Language)的功能性查询语言,让用户可以实时选择和聚合时间序列数据。

时间序列的理解

我们定义的各种指标是定时被Prometheus抓取的,那么它的存储结构就是以时间为横轴的数据。 我们在prometheus中输入一个指标的时候 获取的是最新一次的值

如果我们加上一个时间范围[1m]代表获取1分钟内的数据。 下图可以看到每一个指标可以获取4个值,因为我们是15秒抓取一次数据。

可以理解成是点数据和时间段数据的区别。

当时点数据的时候我们切换到Graph面板就会展示以时间做为横轴,值作为纵轴的图表。

即时矢量选择器

我们一个指标中可能定义了label,只有指标名称是无法区分label,即时矢量选择器用于对label进行选择。 {}用于写label的选择器,支持的语法有:

  • =:选择与提供的字符串完全相等的标签。
  • !=:选择不等于提供的字符串的标签。
  • =~:选择与提供的字符串进行正则表达式匹配的标签。
  • !~:选择与提供的字符串不匹配的标签。

例如:

1
2
3
nodejs_heap_space_size_total_bytes{space="large_object"}
nodejs_heap_space_size_total_bytes{space!="large_object"}
nodejs_heap_space_size_total_bytes{space=~"new.*"}

其中正则表达式的匹配可以认为是 /^$/完全匹配, 例如:

范围矢量选择器

是选择一定范围内的多个样本。持续时间可以写到[]中,支持的单位包括:

  • ms- 毫秒
  • s- 秒
  • m- 分钟
  • h- 小时
  • d- 天 - 假设一天总是 24 小时
  • w- 周 - 假设一周总是 7 天
  • y- 年 - 假设一年总是 365d

例如:

1
nodejs_heap_space_size_total_bytes{space=~"new.*"}[1m]

偏移修改器(不常用)

offset修饰符允许更改查询中各个瞬间和范围向量的时间偏移量。 例如

1
nodejs_heap_space_size_total_bytes[1m]

得到的时间是1656819991.26

1
nodejs_heap_space_size_total_bytes[1m] offset 1m

得到的时间是1656819931.333 向后偏移了一分钟

  • @修饰符(不常用) 默认指标获取的都是当前时间的指标值,而@修饰符可以指定获取哪一个时间点的值,例如:
1
nodejs_heap_space_size_total_bytes @1656820184

运算

支持的全量运算参考文档 这里说一些常用的运算。 算术运算 Prometheus 中存在以下二元算术运算符:

  • +(添加)
  • -(减法)
  • *(乘法)
  • /(分配)
  • %(模数)
  • ^(幂/幂)

比如内存默认单位是byte 我们展示的时候展示MB就可以

1
nodejs_heap_size_total_bytes/1024/1024

常用的聚合运算

因为我们的服务一般都是多实例的,所以需要在统计的时候把所有实例的数据聚合到一起。

  • sum(计算维度总和)
  • min(选择最小尺寸)
  • max(选择最大尺寸)
  • avg(计算尺寸的平均值)
  • group(结果向量中的所有值都是 1)
  • stddev(计算维度上的总体标准偏差)
  • stdvar(计算维度上的总体标准方差)
  • count(计算向量中的元素个数)
  • count_values(计算具有相同值的元素个数)
  • bottomk(样本值的最小 k 个元素)
  • topk(按样本值计算的最大 k 个元素)
  • quantile(在维度上计算 φ-quantile (0 ≤ φ ≤ 1))
  • histogram_quantile(可以计算不同分位指标)

举例:

1
2
sum(nodejs_heap_space_size_total_bytes) by (space)
topk(2, nodejs_heap_space_size_total_bytes)

常用的函数

全量函数参见文档, 这里讲解3种计算增长率的方法用到的函数及他们的区别。

  • increase()计算区间向量中时间序列的增量,它只计算增量,所以想要计算增长率则需要手动的去除以时间。
1
increase(http_requests_total[1m])/60
  • rate()方法用于计算区间向量时间范围内的增长率,秒为单位。
1
rate(http_requests_total[1m])

它计算增长率的方式是时间范围内最后的样本和第一个样本的差值除以时间,所以可能会出现中间某一个时间内增长率高而无法统计到,可能被整个时间范围给平均了,所以一种方法是把时间范围设置的短一些,第二种就是使用irate

  • irate()也是计算区间向量时间范围内的增长率,但是他是瞬时增长率。基于时间范围内最后两个数据点计算。 所以总结就是irate更灵敏而rate侧重时间段内的趋势。

摘自微雨微语(介绍的很详细)

基本查询

查询指标最新的值:

1
2
3
4
5
6
7
8
9
10
{__name__="http_request_total", handler="/home"}

# 语法糖:
http_request_total{handler="/home"}

# 等价于 mysql:
select * from http_request_total
where
handler="/home" AND
create_time=《now()》

区间时间段查询

查询过去一分钟内的数据

1
2
3
4
5
6
# promQL
http_request_total[1m]

# 等价于
SELECT * from http_requests_total
WHERE create_time BETWEEN 《now() - 1min》 AND 《now()》;

时间偏移查询

PS: promQL 不支持指定时间点进行查询,只能通过 offset 来查询历史某个点的数据

查询一个小时前的数据。

1
2
3
4
5
6
# promQL
http_request_total offset 1h

# 等价于
SELECT * from http_requests_total
WHERE create_time=《now() - 1 hour》;

promQL 查询函数

根据以上的查询语法,我们可以简单组合出一些指标数据:

例如,查询最近一天内的 /home 页请求数

1
http_request_total{handler="/home"}  - http_request_total{handler="/home"} offset 1d

那么实际上面这个写法很明显比较不简洁,我们可使用内置 increase 函数来替换:

1
2
# 和上述写法等价
increase(http_request_total{handler="/home"}[1d])

除了 increase 外,还有很多其他好用的函数,例如,
rate 函数计算 QPS

1
2
3
4
5
// 过去的 2 分钟内平均每秒请求数
rate(http_request_total{code="400"}[2m])

// 等价于
increase(http_request_total{code="400"}[2m]) / 120

指标聚合查询

除了上述基础查询外,我们可能还需要聚合查询

假如我们有以下数据指标:

1
2
3
4
credit_insight_spl_id_all_pv{url="/home",channel="none"} 
credit_insight_spl_id_all_pv{url="/home",channel="mepage"}
credit_insight_spl_id_all_pv{url="/error",channel="none"}
credit_insight_spl_id_all_pv{url="/error",channel="mepage"}

将所有指标数据以某个维度进行聚合查询时,例如:查询 url="/home" 最近一天的访问量,channel 是 none还是mepage 的 /home 访问量都包括在内。

我们理所当然地会写出:

1
increase(credit_insight_spl_id_all_pv{url="/home"}[1d])

但实际上我们会得出这样的两条指标结果:

1
2
credit_insight_spl_id_all_pv{url="/home",channel="none"} 233
credit_insight_spl_id_all_pv{url="/home",channel="mepage"} 666

并非我们预期中的:

1
credit_insight_spl_id_all_pv{url="/home"} 899

而要是我们想要得到这样的聚合查询结果,就需要用到 sum by

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 聚合 url="/home" 的数据
sum(increase(credit_insight_spl_id_all_pv{url="/home"}[1d])) by (url)
# 得出结果:
credit_insight_spl_id_all_pv{url="/home"} 899 # 所有 channel 中 /home 页访问量累加值


# 聚合所有的 url 则可以这样写:
sum(increase(credit_insight_spl_id_all_pv{}[1d])) by (url)
# 得出结果:
credit_insight_spl_id_all_pv{url="/home"} 899
credit_insight_spl_id_all_pv{url="/error"} 7


# 等价于 mysql
SELECT url, COUNT(*) AS total FROM credit_insight_spl_id_all_pv
WHERE create_time between <now() - 1d> and <now()>
GROUP BY url;

指标时序曲线

以上的所有例子的查询数值,其实都是最近时间点的数值,

而我们更关注的是一个时间段的数值变化。

要实现这个原理也很简单,只需要在历史的每个时间点都执行一次指标查询,

1
2
3
4
5
6
7
8
9
# 假如今天7号
# 6号到7号的一天访问量
sum(increase(credit_insight_spl_id_all_pv{}[1d] )) by (url)

# 5号到6号的一天访问量 offset 1d
sum(increase(credit_insight_spl_id_all_pv{}[1d] offset 1d)) by (url)

# 4号到5号的一天访问量
sum(increase(credit_insight_spl_id_all_pv{}[1d] offset 2d)) by (url)

而 Prometheus 已经内置了时间段查询功能,并对此优化处理。

可通过 /api/v1/query_range 接口进行查询,获的 grpah:

Prometheus 查询瓶颈

数据存储:

指标数据有 “Writes are vertical,reads are horizontal” 的(垂直写,水平读)模式:
“Writes are vertical,reads are horizontal” 的意思是 tsdb 通常按固定的时间间隔收集指标并写入,会 “垂直” 地写入最近所有时间序列的数据,而读取操作往往面向一定时间范围的一个或多个时间序列,“横向” 地跨越时间进行查询

  • 每个指标(metric)根据指标数量不同,有 labelA labelB labelC * ... 个时序图
  • 每个时序图(time series)的一个点时序是 [timestamp, value], 例如 [1605607257, 233]。[时间戳-值] 可以确定图上的一个点,一个时间区间内的所有点连成一个时序曲线图。
  • 因为 Prometheus 每隔 15s 采集一次数据,所以 时序点的时间间距是 15s,即1分钟有60/15=4个时序点,1小时就有 4 * 60 = 240 个时序点。

而 Prometheus 的默认查询 sample 上限是 5000w

所以,如果指标的时序图数量过大,允许查询的时间区间相对就会较小了

一个图表查询时序数量的影响因素有 3 个,分别是:

  1. 查询条件的时序数量(n)
  2. 查询的时间区间(time)
  3. 图表曲线每个时序点之间的间隔(step)

credit_insight_spl_id_all_pv 指标为例,该指标总共大约有 n = 163698 种时序,

假如 step = 15s,如果搜索该指标过去 time = 60m 的全部时序图,那么,需要搜索的例子要
163698 * 60 * (60/15) = 39287520,将近 4kw,是可以搜出来的。

但如果搜的是过去 90m 的数据,163698 * 90 * 4 = 58931280,超过了 5000w,你就发现数据请求异常:
Error executing query: query processing would load too many samples into memory in query execution

所以,目测可得一个图的查询时序点数量公式是:total = n * time / step, time 和 step 的时间单位必须一致,total 必须不超过 5000w。

反推一下得出,time < 5000w / n * step 。要扩大搜索时间范围,增大 step ,或者降低 n 即可做到。

  • step 不变, 降低 n 【指定label值可减少搜索条件的结果数】 : credit_insight_spl_id_all_pv{systemType="Android", systemVersion="10"},n = 18955
  • 增大 step 到 30s, n 不变:

当然,一般情况下,我们的 n 值只有几百,而 step 基本是大于 60s 的,所以一般情况下都能查询 2 个多月以上的数据图。

Admin Api

prom默认从tsdb只能查询,如果要删除数据,需要在启动时添加参数

1
-web.enable-admin-api

原生Prometheus Api

参考:https://prometheus.io/docs/prometheus/latest/querying/api

Springboot远程调用Prometheus Api获取指标数据

简言

  • 使用RestTemplate作为远程调用工具调用prometheus原生api获取数据
  • 通过访问prometheus原生api,查看原生api返回的数据格式,定义对应的实体类格式
  • 下面所列功能代码,仅为部分调用api结果,仅供参考,如若需要调用其他api,可自行编写对应方法

远程调用类

pom依赖

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
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>

RestTemplate工具类

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.http.Header;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

/**
* RestTemplate工具类
*/
@Slf4j
@Component
@SuppressWarnings("all")
public class RestTemplateUtils {

/**
* http 请求 GET
*
* @param url 地址
* @param params 参数
* @return Http连接
*/
public String getHttp(String url, JSONObject params) {
return getRestConnection(url, params, "http");
}

/**
* https 请求 GET
*
* @param url 地址
* @param params 参数
* @return Https连接
*/
public String getHttps(String url, JSONObject params) {
return getRestConnection(url, params, "https");
}

/**
* 获取远程连接
*
* @param url 请求地址
* @param params JSON对象
* @param connectionFlag 请求标志
* @return 远程连接
*/
private String getRestConnection(String url, JSONObject params, String connectionFlag) {
String restConnection = null;
if (StringUtils.equals("http", connectionFlag)) {
restConnection = getRestHttpConnection(url, params, 10000, 60000, 3);
}

if (StringUtils.equals("https", connectionFlag)) {
restConnection = getRestHttpsConnection(url, params, 10000, 60000, 3);
}
return restConnection;
}

/**
* http 请求 GET
*
* @param url 地址
* @param params 参数
* @param connectTimeout 连接时间
* @param readTimeout 读取时间
* @param retryCount 重试机制
* @return 请求字符串
*/
public String getRestHttpConnection(String url, JSONObject params, int connectTimeout, int readTimeout, int retryCount) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(connectTimeout);
requestFactory.setReadTimeout(readTimeout);
RestTemplate restTemplate = new RestTemplate(requestFactory);
// 设置编码集
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
// 异常处理
restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
// 获取URI
URI uri = getUriByUrl(url, params);
// 重试机制
for (int i = 1; i <= retryCount; i++) {
try {
// 此处设置值为认证的用户名和密码信息, 请注意修改
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor("username", "password");
return restTemplate.getForEntity(uri, String.class).getBody();
} catch (Exception e) {
log.error("[GET/HTTP请求信息]异常, 重试次数:{}, 请求地址:{}, 请求参数:{}, 异常信息:{}", i, url, params, Throwables.getStackTraceAsString(e));
}
}
return null;
}

/**
* https 请求 GET
*
* @param url 地址
* @param params 参数
* @param connectTimeout 连接时间
* @param readTimeout 读取时间
* @param retryCount 重试机制
* @return 请求字符串
*/
public String getRestHttpsConnection(String url, JSONObject params, int connectTimeout, int readTimeout, int retryCount) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(connectTimeout);
requestFactory.setReadTimeout(readTimeout);
RestTemplate restTemplate = restTemplate();
clientHttpRequestFactory();
// 设置编码集
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
// 异常处理
restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
// 绕过https
restTemplate.setRequestFactory(clientHttpRequestFactory());
// 获取URI
URI uri = getUriByUrl(url, params);

for (int i = 1; i <= retryCount; i++) {
try {
// 此处设置值为认证的用户名和密码信息, 请注意修改
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor("username", "password");
return restTemplate.getForEntity(uri, String.class).getBody();
} catch (Exception e) {
log.error("[GET/HTTPS请求信息]异常, 重试次数:{}, 请求地址:{}, 请求参数:{}, 异常信息:{}", i, url, params, Throwables.getStackTraceAsString(e));
}
}
return null;
}

/**
* 获取RestTemplate实例对象,可自由调用其方法
*
* @return RestTemplate实例对象
*/
public HttpClient httpClient() {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
try {
//设置信任SSL访问
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build();
httpClientBuilder.setSSLContext(sslContext);
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
// 注册http和https请求
.register(RestConnectionConstants.HTTP_CONNECTION_FLAG, PlainConnectionSocketFactory.getSocketFactory())
.register(RestConnectionConstants.HTTPS_CONNECTION_FLAG, sslConnectionSocketFactory).build();

//使用Httpclient连接池的方式配置
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
// 最大连接数
poolingHttpClientConnectionManager.setMaxTotal(1000);
// 同路由并发数
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100);
// 配置连接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
// 重试次数
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(1, true));
// 设置默认请求头
List<Header> headers = new ArrayList<>();
httpClientBuilder.setDefaultHeaders(headers);
// 设置请求连接超时时间
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(10000)
.setConnectTimeout(10000)
.setSocketTimeout(60000).build();
httpClientBuilder.setDefaultRequestConfig(requestConfig);
return (HttpClient) httpClientBuilder.build();
} catch (Exception e) {
throw new RestException(RestStatus.SYSTEM_ERROR, Throwables.getStackTraceAsString(e));
}
}

/**
* 创建RestTemplate
*
* @return RestTemplate
*/
public RestTemplate restTemplate() {
return new RestTemplate(clientHttpRequestFactory());
}

/**
* 创建ClientHttpRequestFactory
*
* @return ClientHttpRequestFactory
*/
private ClientHttpRequestFactory clientHttpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}

/**
* 通过URL获取URI
*
* @param url url
* @param params 请求参数
* @return {@code URI}
*/
private URI getUriByUrl(String url, JSONObject params) {
String query = "query";
if (!params.isEmpty()) {
// 网关针对URL中特殊字符进行加密访问, 这里针对网关未处理特殊字符参数进行转义处理
if (params.containsKey(query)) {
String replaceQuery = params.getString(query)
.replace("=", "%3D").replace(" ", "%20")
.replace("{", "%7B").replace("}", "%7D")
.replace("\"", "%22").replace("/", "%2F")
.replace("|", "%7C").replace("+", "%2B")
.replace("[", "%5B").replace("]", "%5D")
.replace("<", "%3C").replace(">", "%3E")
.replace("\n", "%20");
params.put(query, replaceQuery);
} else {
params.keySet().forEach(key -> {
String decode = URLDecoder.decode(params.getString(key), StandardCharsets.UTF_8);
params.put(key, decode);
});
}
url = expandUrl(url, params);
}
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
if (params.containsKey(query)) {
return builder.build(true).toUri();
} else {
return builder.build().encode().toUri();
}
}

/**
* URL拼接
*
* @param url 请求URL
* @param jsonObject JSON对象
* @return 拼接之后的URL
*/
private String expandUrl(String url, JSONObject jsonObject) {
HashMap<String, Object> paramMap = new HashMap<>(16);
StringBuilder stringBuilder = new StringBuilder(url);
stringBuilder.append("?");

Set<String> keys = jsonObject.keySet();
keys.forEach(key -> paramMap.put(key, jsonObject.getString(key)));
String joinStr = Joiner.on("&").withKeyValueSeparator("=").join(paramMap);
return stringBuilder.append(joinStr).toString();
}
}

即时查询获取数据

即时查询实体类

Prometheus查询结果实体类:PromQueryResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import lombok.Data;

import java.util.List;
import java.util.Map;

/**
* Prometheus查询结果对象信息
*/
@Data
public class PromQueryResult {
/**
* prometheus指标属性
*/
private Map<String, Object> metric;

/**
* prometheus即时查询指标值
*/
private List<String> value;
}

Prometheus查询数据结果实体类:PromQueryData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import lombok.Data;

import java.util.List;

/**
* Prometheus查询数据结果对象
*/
@Data
public class PromQueryData {
/**
* prometheus结果类型
* vector--瞬时向量
* matrix--区间向量
* scalar--标量
* string--字符串
*/
private String resultType;

/**
* prometheus指标属性和值
*/
private List<PromQueryResult> result;
}

Prometheus查询响应实体类:PromQueryResponse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.Data;

/**
* Prometheus查询响应对象
*/
@Data
public class PromQueryResponse {
/**
* 状态
* 成功-- success
*/
private String status;

/**
* prometheus指标属性和值
*/
private PromQueryData data;
}

即时查询接口实现

即时查询接口类:PromQueryService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import xxx.entity.pojo.PromQueryData;

/**
* 指标查询接口类
*
*/
public interface PromQueryService {

/**
* Prometheus即时查询
*
* @param query 查询
* @param time 时间戳, 单位: 秒
* @return {@code PromQueryData}
*/
PromQueryData getQueryDataInfo(String query, String time);
}

即时查询接口实现类:PromQueryServiceImpl

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Throwables;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import xxx.entity.pojo.PromQueryData;
import xxx.entity.pojo.PromQueryResponse;
import xxx.service.PromQueryService;
import xxx.util.RestTemplateUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
* 指标查询接口实现类
*/
@Slf4j
@Service("promQueryService")
public class PromQueryServiceImpl implements PromQueryService {
@Resource
private RestTemplateUtils restTemplateUtils;

@Override
public PromQueryData getQueryDataInfo(String query, String time) {
if (StringUtils.isBlank(time)) {
time = String.valueOf(DateUtil.currentSeconds());
}

JSONObject param = new JSONObject();
param.put("query", query);
param.put("time", time);

// prometheus的URL连接地址, 根据需要修改
String url = "http://localhost:9090" + "/api/v1/query";
return (PromQueryData) getDataInfo(url, param);
}

/**
* 获取查询结果数据
*
* @param promUrl 调用的prometheus的URL
* @param param 请求参数
* @return 查询结果对象
*/
private Object getDataInfo(String promUrl, JSONObject param) {
String http = getHttp(promUrl, param);
PromQueryResponse responseInfo = JSON.parseObject(http, PromQueryResponse.class);
log.info("即时查询请求地址: {}, 请求参数: {}", promUrl, param);
if (Objects.isNull(responseInfo)) {
return null;
}

String status = responseInfo.getStatus();
if (StringUtils.isBlank(status) || !StringUtils.equals("success", status)) {
return null;
}
return responseInfo.getData();
}

/**
* 获取http连接
*
* @param promUrl 连接URL
* @param param 请求参数
* @return http连接
*/
private String getHttp(String promUrl, JSONObject param) {
String http = null;
try {
http = restTemplateUtils.getHttp(promUrl, param);
} catch (Exception e) {
log.error("请求地址: {}, 请求参数: {}, 异常信息: {}", promUrl, param, Throwables.getStackTraceAsString(e));
}
return http;
}
}

范围查询获取数据

范围查询实体类

范围查询实体类:PromQueryRange

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
import lombok.Data;

/**
* Prometheus范围查询实体类
*/
@Data
public class PromQueryRange {
/**
* 查询指标
*/
private String query;

/**
* 区间范围查询开始时间
* 格式为:时分秒时间戳
*/
private String start;

/**
* 区间范围查询结束时间
* 格式为:时分秒时间戳
*/
private String end;

/**
* 时间区间步长, 即:时间间隔
*/
private Integer step;
}

Prometheus范围区间查询结果实体类:PromQueryRangeResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import lombok.Data;

import java.util.List;
import java.util.Map;

/**
* Prometheus范围区间查询结果对象信息
*/
@Data
public class PromQueryRangeResult {
/**
* prometheus指标属性
*/
private Map<String, Object> metric;

/**
* prometheus范围查询指标值
*/
private List<List<String>> values;
}

Prometheus范围区间查询数据结果对象实体类:PromQueryRangeData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import lombok.Data;

import java.util.List;

/**
* Prometheus范围区间查询数据结果对象
*/
@Data
public class PromQueryRangeData {
/**
* prometheus结果类型
* vector--瞬时向量
* matrix--区间向量
* scalar--标量
* string--字符串
*/
private String resultType;

/**
* prometheus指标属性和值
*/
private List<PromQueryRangeResult> result;
}

Prometheus范围区间查询响应对象实体类:PromQueryRangeResponse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.Data;

/**
* Prometheus范围区间查询响应对象
*/
@Data
public class PromQueryRangeResponse {
/**
* 状态
* 成功-- success
*/
private String status;

/**
* prometheus范围查询指标属性和值
*/
private PromQueryRangeData data;
}

范围查询接口实现

范围查询接口类:PromQueryService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import xxx.entity.pojo.PromQueryRangeData;
import xxx.entity.pojo.PromQueryRange;

/**
* 指标查询接口类
*/
public interface PromQueryService {

/**
* Prometheus范围区间查询
*
* @param queryRangeDto 查询范围类
* @return {@code PromQueryRangeData}
*/
PromQueryRangeData getQueryRangeDataInfo(PromQueryRange queryRange);
}

范围查询接口实现类:PromQueryServiceImpl

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Throwables;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import xxx.entity.pojo.PromQueryRange;
import xxx.entity.pojo.PromQueryRangeData;
import xxx.entity.pojo.PromQueryRangeResponse;
import xxx.service.PromQueryService;
import xxx.util.RestTemplateUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
* 指标查询接口实现类
*/
@Slf4j
@Service("promQueryService")
public class PromQueryServiceImpl implements PromQueryService {
@Resource
private RestTemplateUtils restTemplateUtils;

@Override
public PromQueryRangeData getQueryRangeDataInfo(PromQueryRange queryRange) {
JSONObject param = new JSONObject();
handleQueryRangeParams(param, queryRange);

// prometheus的URL连接地址, 根据需要修改
String url = "http://localhost:9090" + "/api/v1/query_range";
return (PromQueryRangeData) getDataInfo(url, param);
}

/**
* 处理范围查询参数
*
* @param param 参数
* @param queryRange PromQueryRange对象
*/
private void handleQueryRangeParams(JSONObject param, PromQueryRange queryRange) {
String start = queryRange.getStart();
if (StringUtils.isBlank(start)) {
// 开始时间为空, 则设置默认值为当前时间
start = String.valueOf(DateUtil.currentSeconds());
}

String end = queryRange.getEnd();
if (StringUtils.isBlank(end)) {
// 结束时间为空, 则设置默认值为当前时间向后偏移1小时
end = String.valueOf(DateUtil.offsetHour(DateUtil.parse(start), 1).getTime());
}

param.put("query", queryRange.getQuery());
param.put("start", start);
param.put("end", end);
param.put("step", queryRange.getStep());
}

/**
* 获取查询结果数据
*
* @param promUrl 调用的prometheus的URL
* @param param 请求参数
* @return 查询结果对象
*/
private Object getDataInfo(String promUrl, JSONObject param) {
String http = getHttp(promUrl, param);
PromQueryRangeResponse rangeResponse = JSON.parseObject(http, PromQueryRangeResponse.class);
log.info("范围区间查询请求地址: {}, 请求参数: {}", promUrl, param);
if (Objects.isNull(rangeResponse)) {
return null;
}

String status = rangeResponse.getStatus();
if (StringUtils.isBlank(status) || !StringUtils.equals("success", status)) {
return null;
}
return rangeResponse.getData();
}

/**
* 获取http连接
*
* @param promUrl 连接URL
* @param param 请求参数
* @return http连接
*/
private String getHttp(String promUrl, JSONObject param) {
String http = null;
try {
http = restTemplateUtils.getHttp(promUrl, param);
} catch (Exception e) {
log.error("请求地址: {}, 请求参数: {}, 异常信息: {}", promUrl, param, Throwables.getStackTraceAsString(e));
}
return http;
}
}

根据标签匹配器获取时序数据

时序数据实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import lombok.Data;

import java.util.LinkedHashMap;
import java.util.List;

/**
* Prometheus时序对象
*/
@Data
public class PromSeries {
/**
* 状态
* 成功-- success
*/
private String status;

/**
* 时序数据列表
*/
private List<LinkedHashMap<String, Object>> data;
}

时序数据接口实现

根据标签匹配器获取时序数据接口类:PromQueryService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 指标查询接口类
*/
public interface PromQueryService {

/**
* 获取时序数据
*
* @param start 开始时间戳, 单位:秒
* @param end 结束时间戳, 单位:秒
* @param match 查询指标
* @return {@code List<LinkedHashMap<String, Object>>}
*/
List<LinkedHashMap<String, Object>> getSeriesList(String start, String end, String match);
}

根据标签匹配器获取时序数据接口实现类:PromQueryServiceImpl

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Throwables;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import xxx.entity.pojo.PromQueryData;
import xxx.entity.pojo.PromQueryResponse;
import xxx.service.PromQueryService;
import xxx.util.RestTemplateUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
* 指标查询接口实现类
*/
@Slf4j
@Service("promQueryService")
public class PromQueryServiceImpl implements PromQueryService {
@Resource
private RestTemplateUtils restTemplateUtils;

@Override
public List<LinkedHashMap<String, Object>> getSeriesList(String start, String end, String match, Integer datasource) {
JSONObject param = new JSONObject();
param.put("start", start);
param.put("end", end);
param.put("match[]", match);

// prometheus的URL连接地址, 根据需要修改
String url = "http://localhost:9090" + "/api/v1/series";
return getSeriesDataList(url, param);
}

/**
* 获取时序数据列表
*
* @param promUrl 时序URL
* @param param 请求参数
* @return 时序数据列表
*/
private List<LinkedHashMap<String, Object>> getSeriesDataList(String promUrl, JSONObject param) {
String http = getHttp(promUrl, param);
PromSeries promSeries = JSON.parseObject(http, PromSeries.class);
if (Objects.nonNull(promSeries)) {
String status = promSeries.getStatus();
if (StringUtils.isBlank(status) || !StringUtils.equals(PromConstants.SUCCESS, status)) {
return Collections.emptyList();
}
} else {
return Collections.emptyList();
}

return promSeries.getData();
}

/**
* 获取http连接
*
* @param promUrl 连接URL
* @param param 请求参数
* @return http连接
*/
private String getHttp(String promUrl, JSONObject param) {
String http = null;
try {
http = restTemplateUtils.getHttp(promUrl, param);
} catch (Exception e) {
log.error("请求地址: {}, 请求参数: {}, 异常信息: {}", promUrl, param, Throwables.getStackTraceAsString(e));
}
return http;
}
}

样例

最近 2 分钟平均 QPS, 根据路由分组

1
sum(rate(http_requests_total{job=~"koa-app", path=~".*"}[2m])) by (path)

最近 1 分钟平均响应时间, 根据路由分组

1
avg(increase(http_request_duration_ms_sum{job=~"koa-app", path=~".*"}[1m]) / increase(http_request_duration_ms_count{job=~"koa-app", path=~".*"}[1m]) >0) by (path)

最近 1 分钟 90 分位响应时间, 根据路由分组

1
histogram_quantile(0.90, sum(irate(http_request_duration_ms_bucket{job=~"koa-app", path=~".*"}[1m])) by (path, le))

最近 5 分钟, 非 200 请求率, 根据路由分组

1
sum(irate(http_requests_total{status!~"200",job=~"koa-app", path=~".*"}[5m])) BY (job, path, status)