开始使用 Prometheus 吧
来整理一下 Prometheus 相关知识。
由于某些契机,我所在的项目通过 Prometheus 进行监控,已经使用了半年时间,未来监控的覆盖面也越来越大。我跟同为程序员的朋友约饭时,两次听到他们有监控的需求,甚至直接表示想使用 Prometheus,因此我来整理一下我学习到的 Prometheus 内容。
本篇文章主要包括:
- Prometheus 的使用场景
- Prometheus 的基础知识
- Prometheus 的查询语句
- 怎么通过 Grafana 展示 Prometheus 数据
- Java 怎么使用 Prometheus 监控
Prometheus 的使用场景
尽管 Prometheus 真的很好用,但是在使用之前,你还是应该认真思考自己的需求:自己究竟想监控什么?
目前市面上有三大类监控方向:日志、链路追踪、数值指标。
监控方向 | 关注内容 | 代表性工具 |
---|---|---|
日志监控 | 采集和查询日志,排查特定异常请求 | ELK |
链路追踪监控 | 一个请求进来,经过了哪些微服务,操作了哪些数据库,在每个节点耗费多少时间,在哪里发生了异常 | SkyWalking |
数值监控 | 各种数值指标,比如 CPU 和内存的占用、服务并发量、系统 QPS 等 | Prometheus |
下面罗列了一些监控需求,方便对号入座:
需求 | 是否应该使用 Prometheus |
---|---|
采集日志,查询日志 | ×(去看日志监控) |
排查特定异常请求 | ×(去看日志监控) |
一个请求经过很多微服务,各个节点耗费多长时间 | ×(去看链路追踪监控) |
一个请求经过很多微服务,在哪里抛出了异常 | ×(去看链路追踪监控) |
一个请求多次操作数据库,每次 IO 耗费多长时间 | ×(去看链路追踪监控) |
CPU、内存、垃圾回收等指标是否正常 | ✓ |
在高峰期时,所有接口的调用量有多少 | ✓ |
高并发场景下,寻找可能的性能瓶颈 | ✓ |
我平常在使用 Prometheus 的场景如下:
- 日常监控,观察 CPU、内存、垃圾回收时间、线程状态、异常日志数量等是否正常
- 日常监控,观察接口调用数量,HTTP 请求时间是否正常
- 新服务上线,持续观察各种指标是否正常,接口请求量有多少(可以判断使用人数有多少)
- 压测时,观察并发量有多少,异常具体有哪些类型,各自抛出多少次
- 发生线上事故时,观察当时 CPU、内存、线程数、接口调用数、HTTP 最大请求时间、数据库 ops、数据库请求耗时,分析可能的问题原因
再推荐两篇博文,用于思考监控:《APM 介绍与实现》、《Prometheus监控告警——总结与思考》。
Prometheus 的基础知识
(有关 Prometheus 的历史和江湖地位自行谷歌,不提了)
简单地说,Prometheus 就是一个时序数据库,每隔一段时间收集一次数据,连起来就是一段时间的监控数据。
我们举一个例子:
你是一个超市老板,店里卖两种水果(苹果、香蕉)和一种饮料(可乐),源源不断有客人来买。你有一本账本,每分钟都会记录一次销售额,记录如下:
1 | 8:00 销售额(类目:水果 商品:苹果) 10元 |
你想知道在 8:00 到 8:03 之间,苹果卖了多少钱,你把账本的数一对:10元 -> 10元 -> 10元 -> 15元。
你又想知道在 8:00 到 8:03 之间,总共赚了多少钱,你简单运算后得出了结果:16元 -> 19元 -> 25元 -> 36元。
Prometheus 的监控原理就是这样,它每隔一段时间收集一次监控数据,想看哪段时间的监控数据,查出来再连起来就可以了,下图是 Prometheus 的真实图表截图:
Prometheus 采集的数据,需要遵循一定的格式:指标名{标签1, 标签2...}
。
还是举例来说:
1 | sales_amount_total{category="drinks", goods="CocaCola"} 15 |
(value 样本值实际上是 float64 浮点数类型,我们这里为了方便,只显示成整型数。)
这种格式要记住(实际上也很简单不是吗),我们之后查询时会经常用到它。
有关这部分,推荐阅读《理解时间序列》和《剖析Prometheus的内部存储机制》。
Prometheus 的查询语句
Prometheus 有自己的查询语言,叫做 PromQL,语法简单但是内容比较杂,我按照自己的日常使用归类介绍一下。
我们继续使用超市卖水果和饮料的例子,一共有三条曲线:
1 | sales_amount_total{category="drinks", goods="CocaCola"} |
简单查询
最简单的查询语句,就是直接查询 metric name 指标名称,
sales_amount_total
,能够查到三条曲线:在查询指标名的基础上,还可以再指明 label 标签,比如查询水果类目的监控信息,
sales_amount_total{category="fruits"}
,能够查到两条曲线:查询 label 标签时除了等于,还可以不等于
!=
、正则匹配=~
、正则不匹配!~
:
聚合查询
聚合查询指的是,把原来的多条查询曲线,合并成一条(或几条),比如使用
sum(...)
求和:上图就是把类目为水果的两条曲线(苹果和香蕉)合并成一条。
除了求和,我平常使用比较多的还有计数
count(...)
、最大值max(...)
、前N个topk(n, ...)
,其中前N个这个聚合查询稍有不同,要注意一下。聚合查询还支持分类功能,比如按类目 category 进行求和,就能够看到两条曲线:水果的销售总额和饮料的销售总额:
by(...)
支持多重分类,比如by(label1, label2, label3)
,我这里 label 标签有限,就不展示啦。还有一种不太常见的用法是
without(...)
,它是指除了这几个标签之外,其他的都分类。比如sum(sales_amount_total)by(category)
和sum(sales_amount_total)without(goods)
的查询效果是相同的(毕竟我们只有两个标签嘛)。
内置函数
PromQL 内置了几个函数,我平常使用最多的是 rate(...)
函数和 increase(...)
函数,它们都代表增长的作用,前者是增长率,后者是增长量。
为什么这两个函数使用比较多呢,因为当数据量变大之后,数据变化会非常不明显。下图是某 6 小时内,我监控的一个服务的接口调用量,左边是接口调用量曲线,右边是接口调用量增长率曲线:
使用 rate(...)
函数和 increase(...)
函数时,用法稍有不同,要指定统计的时间间隔:
rate(...[1m])
中的方括号 [1m]
就是在指定时间范围,再给出几个例子:[10m]
(最近十分钟)、[1h]
(最近一小时)、[1d]
(最近一天)、[1w]
(最近一月)。
有两条需要注意的地方:
内置函数只能对 Prometheus 的存储内容(也就是向量,前面没有提到)进行处理,不能对聚合结果进行处理。
比如我想查询,所有所有接口的总调用增长率是多少,不可以先求和再求增长率,而是要先求出增长率之后再求和:
rate( sum(http_server_requests_seconds_count)[1m] )
错误sum( rate(http_server_requests_seconds_count[1m]) )
正确
实际上我并不知道
rate(...)
函数和increase(...)
函数的算法……因此我不知道查询出来的数值,具体代表了什么含义。但是平常看一看相对趋势,分析什么时候是业务高峰期,还是可以的。
其他查询
PromQL 支持数学运算,加减乘除取余求幂(+ - * / % ^)都是支持的,比如
sales_amount_total + 10
不知道怎么描述,直接看图吧,如图查出来的是数值大于 50 的部分
过来人的忠告:PromQL 不复杂,但是很繁琐,需要漫长的学习过程,多练多试才能掌握。
Grafana
Grafana 是一个可视化工具,把 Prometheus 存储的内容以好看的形式展示出来。
跟 Prometheus 自带的可视化图表相比,我觉得 Grafana 主要有这么几点好处:
- 好看(符合恶俗的 RGB 审美hhh),而且图表类型多(比如有饼图、热点图、仪表盘图等)
- 可以同时展示多图表,对比多图表时很方便,并且可以保存下来
- 写 PromQL 时能够联想词
配置 Grafana 很简单(但是需要一个个配,很繁琐),通常的配置过程如下图所示(看不清可以在新标签中打开图片):
具体的配置教程网上很多,自己体验半个小时也就理解的七七八八了,这些我就不写了。我在这里只写一块内容:如何配置共用的搜索标签。
如图,红框中的内容是公共查询标签,比如我们指定 Application 为 Service1,那么下面的所有查询,都是 Service1 的监控图表。
配置主要分两步,第一步增加全局标签配置,第二步为每个 PromQL 查询关联上全局标签配置。
增加全局标签配置
label_values(application)
还可以写得更确切一点,例如取http_server_requests_seconds_count
指标中实例为 XXX 的application
可以这么写:label_values(http_server_requests_seconds_count{instance="XXX"}, application)
为每个 PromQL 查询关联上全局标签配置
如果想配置成同时勾选多个标签,那么
=
要换成正则匹配=~
,例如{application=~"$application"}
。
Java 怎么使用 Prometheus 监控
在本文前面提过,Prometheus 本质上是一个时序数据库,每隔一段时间收集一次数据。
这里更明确地指出来,Prometheus 是自己收集数据的,配置间隔时间一秒,那 Prometheus 就一秒采集一次监控数据,配置间隔时间一小时,那 Prometheus 就一小时采集一次监控数据。被采集的服务,需要暴露出来一个接口,供 Prometheus 自行采集数据。
简洁地说,Prometheus 基于 PULL 模式采集数据,被监控的服务提供一个采集接口,供 Prometheus 采集。
如果是 Java 程序想被监控,那么它要暴露一个供 Prometheus 采集的接口。这个工作已经有前人做好了,就是 Micrometer(实际上它也是 Spring Boot 2 的 actuator 具体实现)。
我们只需要做两步工作:
Maven 中引入 Micrometer(因为要符合 Prometheus 的数据格式)
1
2
3
4<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>application.yml
文件配置一下,暴露接口给 Prometheus(默认的接口暴露地址是/actuator/prometheus
)1
2
3
4
5management:
endpoints:
web:
exposure:
include: prometheus
更详细的配置过程可以参考腾讯云的文档《Spring Boot 接入 Prometheus》。
配置好之后,调用 /actuator/prometheus
就能看到默认的采集数据:
Micrometer 默认会采集一些监控指标,包括以下内容:
- CPU
- 内存
- JVM 缓冲区
- 线程
- 垃圾回收
- 运行时
- 类加载
- 日志数量
- http 请求接口数量和时间
- Spring Integration(这是啥?)
如果在此之外你还想监控更多的东西,就要自己写代码了。代码非常简单,基本一两行就可以写完。
Prometheus 一共有四种类型的数据,分别是 Counter
、Gauge
、Histogram
、Summary
,其中也就前两种常用(这块应该在上文提及,但是我觉得不重要,就略过了)。
Counter
是只增不减的数据类型(比如记录日志数量、接口调用次数)1
2
3
4
5
6
7
8private final MeterRegistry meterRegistry;
public void collect() {
// 监控指标名(用.分隔) 标签(按照 key1, value1, key2, value2... 的顺序)
meterRegistry.counter("metric.xxx.xxx", "label1", "xxx", "label2", "xxx" ).increment();
// 这样暴露给 Prometheus 的格式是:
// metric_xxx_xxx{label1="xxx",label2="xxx"} 1
}Gauge
是瞬时数据类型(比如 CPU 和内存的实时指标)。1
2
3
4
5
6
7
8private final MeterRegistry meterRegistry;
public void collect() {
// 监控指标名(用.分隔) 标签(按照 key1, value1, key2, value2... 的顺序) 关联的Number对象
meterRegistry.gauge("metric.xxx.xxx", Tags.of("label1", "xxx", "label2", "xxx"), new AtomicInteger(0));
// 这样暴露给 Prometheus 的格式是:
// metric_xxx_xxx{label1="xxx",label2="xxx"} 1
}gauge 的逻辑稍有不同,它实际上是关联了一个
Number
对象,每当这个对象发生了修改,Prometheus 调用接口都能看到:1
2
3
4
5
6
7
8
9
10
11
12// metric.xxx.xxx 指标关联上一个 AtomicInteger 类型的数字 i
AtomicInteger i = meterRegistry.gauge("metric.xxx.xxx", new AtomicInteger(0));
// 此时 Prometheus 调用接口可见:
// metric_xxx_xxx{} 0
i.set(1);
// 此时 Prometheus 调用接口可见:
// metric_xxx_xxx{} 1
i.set(2);
// 此时 Prometheus 调用接口可见:
// metric_xxx_xxx{} 2
具体使用都很简单,试一下就清楚了。
就写到这里吧。