开始使用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
8:00 销售额(类目:水果 商品:苹果) 10元
8:00 销售额(类目:水果 商品:香蕉) 0元
8:00 销售额(类目:饮料 商品:可乐) 6元

8:01 销售额(类目:水果 商品:苹果) 10元
8:01 销售额(类目:水果 商品:香蕉) 3元
8:01 销售额(类目:饮料 商品:可乐) 6元

8:02 销售额(类目:水果 商品:苹果) 10元
8:02 销售额(类目:水果 商品:香蕉) 3元
8:02 销售额(类目:饮料 商品:可乐) 12元

8:03 销售额(类目:水果 商品:苹果) 15元
8:03 销售额(类目:水果 商品:香蕉) 3元
8:03 销售额(类目:饮料 商品:可乐) 18元
……

你想知道在 8:00 到 8:03 之间,苹果卖了多少钱,你把账本的数一对:10元 -> 10元 -> 10元 -> 15元。

你又想知道在 8:00 到 8:03 之间,总共赚了多少钱,你简单运算后得出了结果:16元 -> 19元 -> 25元 -> 36元。

Prometheus 的监控原理就是这样,它每隔一段时间收集一次监控数据,想看哪段时间的监控数据,查出来再连起来就可以了,下图是 Prometheus 的真实图表截图:

Prometheus监控界面

Prometheus 采集的数据,需要遵循一定的格式:指标名{标签1, 标签2...}

还是举例来说:

1
2
3
sales_amount_total{category="drinks", goods="CocaCola"} 15
sales_amount_total{category="fruits", goods="apple"} 3
sales_amount_total{category="fruits", goods="banana"} 18

Prometheus格式

(value 样本值实际上是 float64 浮点数类型,我们这里为了方便,只显示成整型数。)

这种格式要记住(实际上也很简单不是吗),我们之后查询时会经常用到它。

有关这部分,推荐阅读《理解时间序列》和《剖析Prometheus的内部存储机制》。


Prometheus 的查询语句

Prometheus 有自己的查询语言,叫做 PromQL,语法简单但是内容比较杂,我按照自己的日常使用归类介绍一下。

我们继续使用超市卖水果和饮料的例子,一共有三条曲线:

1
2
3
sales_amount_total{category="drinks", goods="CocaCola"}
sales_amount_total{category="fruits", goods="apple"}
sales_amount_total{category="fruits", goods="banana"}

简单查询

  1. 最简单的查询语句,就是直接查询 metric name 指标名称,sales_amount_total,能够查到三条曲线:

    监控示意图1

  2. 在查询指标名的基础上,还可以再指明 label 标签,比如查询水果类目的监控信息,sales_amount_total{category="fruits"},能够查到两条曲线:

    监控示意图2

  3. 查询 label 标签时除了等于,还可以不等于!=、正则匹配=~、正则不匹配!~

    监控示意图3

聚合查询

  1. 聚合查询指的是,把原来的多条查询曲线,合并成一条(或几条),比如使用 sum(...) 求和:

    监控示意图4

    上图就是把类目为水果的两条曲线(苹果和香蕉)合并成一条。

  2. 除了求和,我平常使用比较多的还有计数 count(...)、最大值 max(...)、前N个 topk(n, ...),其中前N个这个聚合查询稍有不同,要注意一下。

    监控示意图5

  3. 聚合查询还支持分类功能,比如按类目 category 进行求和,就能够看到两条曲线:水果的销售总额和饮料的销售总额:

    监控示意图6

    by(...) 支持多重分类,比如 by(label1, label2, label3),我这里 label 标签有限,就不展示啦。

    还有一种不太常见的用法是 without(...),它是指除了这几个标签之外,其他的都分类。比如 sum(sales_amount_total)by(category)sum(sales_amount_total)without(goods) 的查询效果是相同的(毕竟我们只有两个标签嘛)。

内置函数

PromQL 内置了几个函数,我平常使用最多的是 rate(...) 函数和 increase(...) 函数,它们都代表增长的作用,前者是增长率,后者是增长量。

为什么这两个函数使用比较多呢,因为当数据量变大之后,数据变化会非常不明显。下图是某 6 小时内,我监控的一个服务的接口调用量,左边是接口调用量曲线,右边是接口调用量增长率曲线:

rate函数的作用

使用 rate(...) 函数和 increase(...) 函数时,用法稍有不同,要指定统计的时间间隔:

监控示意图7

rate(...[1m]) 中的方括号 [1m] 就是在指定时间范围,再给出几个例子:[10m](最近十分钟)、[1h](最近一小时)、[1d](最近一天)、[1w](最近一月)。

有两条需要注意的地方:

  1. 内置函数只能对 Prometheus 的存储内容(也就是向量,前面没有提到)进行处理,不能对聚合结果进行处理。

    比如我想查询,所有所有接口的总调用增长率是多少,不可以先求和再求增长率,而是要先求出增长率之后再求和:

    • rate( sum(http_server_requests_seconds_count)[1m] ) 错误
    • sum( rate(http_server_requests_seconds_count[1m]) ) 正确
  2. 实际上我并不知道 rate(...) 函数和 increase(...) 函数的算法……因此我不知道查询出来的数值,具体代表了什么含义。但是平常看一看相对趋势,分析什么时候是业务高峰期,还是可以的。

其他查询

  1. PromQL 支持数学运算,加减乘除取余求幂(+ - * / % ^)都是支持的,比如 sales_amount_total + 10

  2. 不知道怎么描述,直接看图吧,如图查出来的是数值大于 50 的部分

    监控示意图8

  3. 过来人的忠告:PromQL 不复杂,但是很繁琐,需要漫长的学习过程,多练多试才能掌握。


Grafana

Grafana 是一个可视化工具,把 Prometheus 存储的内容以好看的形式展示出来。

Grafana_dashboard

跟 Prometheus 自带的可视化图表相比,我觉得 Grafana 主要有这么几点好处:

  1. 好看(符合恶俗的 RGB 审美hhh),而且图表类型多(比如有饼图、热点图、仪表盘图等)
  2. 可以同时展示多图表,对比多图表时很方便,并且可以保存下来
  3. 写 PromQL 时能够联想词

配置 Grafana 很简单(但是需要一个个配,很繁琐),通常的配置过程如下图所示(看不清可以在新标签中打开图片):

快速配置Grafana

具体的配置教程网上很多,自己体验半个小时也就理解的七七八八了,这些我就不写了。我在这里只写一块内容:如何配置共用的搜索标签。

Grafana标签

如图,红框中的内容是公共查询标签,比如我们指定 Application 为 Service1,那么下面的所有查询,都是 Service1 的监控图表。

配置主要分两步,第一步增加全局标签配置,第二步为每个 PromQL 查询关联上全局标签配置。

  1. 增加全局标签配置

    增加全局标签配置

    label_values(application) 还可以写得更确切一点,例如取 http_server_requests_seconds_count 指标中实例为 XXX 的 application 可以这么写:label_values(http_server_requests_seconds_count{instance="XXX"}, application)

  2. 为每个 PromQL 查询关联上全局标签配置

    关联上全局标签配置

    如果想配置成同时勾选多个标签,那么 = 要换成正则匹配 =~,例如 {application=~"$application"}


Java 怎么使用 Prometheus 监控

在本文前面提过,Prometheus 本质上是一个时序数据库,每隔一段时间收集一次数据。

这里更明确地指出来,Prometheus 是自己收集数据的,配置间隔时间一秒,那 Prometheus 就一秒采集一次监控数据,配置间隔时间一小时,那 Prometheus 就一小时采集一次监控数据。被采集的服务,需要暴露出来一个接口,供 Prometheus 自行采集数据。

简洁地说,Prometheus 基于 PULL 模式采集数据,被监控的服务提供一个采集接口,供 Prometheus 采集。


如果是 Java 程序想被监控,那么它要暴露一个供 Prometheus 采集的接口。这个工作已经有前人做好了,就是 Micrometer(实际上它也是 Spring Boot 2 的 actuator 具体实现)。

我们只需要做两步工作:

  1. Maven 中引入 Micrometer(因为要符合 Prometheus 的数据格式)

    1
    2
    3
    4
    <dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
  2. application.yml 文件配置一下,暴露接口给 Prometheus(默认的接口暴露地址是 /actuator/prometheus

    1
    2
    3
    4
    5
    management:
    endpoints:
    web:
    exposure:
    include: prometheus

更详细的配置过程可以参考腾讯云的文档《Spring Boot 接入 Prometheus》。


配置好之后,调用 /actuator/prometheus 就能看到默认的采集数据:

actuator暴露给Prometheus采集数据

Micrometer 默认会采集一些监控指标,包括以下内容:

  • CPU
  • 内存
  • JVM 缓冲区
  • 线程
  • 垃圾回收
  • 运行时
  • 类加载
  • 日志数量
  • http 请求接口数量和时间
  • Spring Integration(这是啥?)

如果在此之外你还想监控更多的东西,就要自己写代码了。代码非常简单,基本一两行就可以写完。

Prometheus 一共有四种类型的数据,分别是 CounterGaugeHistogramSummary,其中也就前两种常用(这块应该在上文提及,但是我觉得不重要,就略过了)。

  • Counter 是只增不减的数据类型(比如记录日志数量、接口调用次数)

    1
    2
    3
    4
    5
    6
    7
    8
    private 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
    8
    private 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

具体使用都很简单,试一下就清楚了。

就写到这里吧。