系统开发中需要统计很多应用指标, 比如: 接口请求QPS, 接口响应时间统计, 接口耗时发布等. SpringBoot自带的spring-actuator
中集成了Micrometer
进行度量统计, 然后我们再用Promethues
收集存储指标后, 在Grafana
中以图标展示.
1. Micrometer
1.1 Micrometer提供的度量类库
在Micrometer
中, Meter
接口是用于收集应用中度量数据的, 它是由MeterRegistry
创建和保存的, 可以将MeterRegistry
理解为Meter
的工厂和缓存中心. 一般而言, 每个JVM应用在使用Micrometer
时都需要创建一个MeterRegistry
的具体实现.
MeterRegistry
在Micrometer
中是一个抽象类, 主要实现包括:
SimpleMeterRegistry
: 每个Meter的最新数据可以收集到SimpleMeterRegistry
实例中,但是这些数据不会发布到其他系统,也就是说数据是位于应用的内存中的。适合调试的时候使用CompositeMeterRegistry
: 多个MeterRegistry
的聚合- 全局的
MeterRegistry
: 工厂类io.micrometer.core.instrument.Metrics
中持有一个静态final类型的CompositeMeterRegistry
实例
1.2 Meter
Counter
Counter
是一种比较简单的Meter
, 它的值只能单调递增. 可以用来统计比如: 接口调用次数, 订单总量等. 使用实例
1 | MeterRegistry meterRegistry = new SimpleMeterRegistry(); |
通过Tag可以区分不同的场景,对于下单,可以使用不同的Tag标记不同的业务来源或者是按日期划分,对于Http请求总量记录,可以使用Tag区分不同的URL。用下单业务举个例子:
1 | //实体 |
控制台输出
1 | name:order.create,tags:[tag(channel=CHANNEL_A), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}] |
上面的例子是使用全局静态方法工厂类Metrics去构造Counter实例,实际上,io.micrometer.core.instrument.Counter
接口提供了一个内部建造器类Counter.Builder
去实例化Counter
,Counter.Builder的使用方式如下:
1 | public static void main(String[] args) throws Exception{ |
FunctionCounter
FunctionCounter
是Counter
的特化类型, 它把计数器值增加的动作抽象成JDK1.8的接口类型ToDoubleFunction
. 它的使用场景与Counter
一致, 下面介绍下其语法:
1 | public static void main(String[] args) throws Exception { |
FunctionCounter
使用的一个明显的好处是,我们不需要感知FunctionCounter实例的存在,实际上我们只需要操作作为FunctionCounter实例构建元素之一的AtomicInteger实例即可,这种接口的设计方式在很多框架里面都可以看到。
Timer
Timer
适用于记录耗时比较短的事件的执行时间, 通过时间发布展示事件的序列和发生频率. 所有的Timer
实现至少记录了发生的事件的数量和这些事件的总耗时, 从而生成一个时间序列. Timer
的基本单位基于服务端的指标而定, 但是实际上我们不需要过于关注Timer的基本单位,因为Micrometer在存储生成的时间序列的时候会自动选择适当的基本单位。Timer接口提供的常用方法如下:
1 | public interface Timer extends Meter { |
使用下单业务做个例子:
1 | public class TimerMain { |
在实际生产环境中,可以通过spring-aop把记录方法耗时的逻辑抽象到一个切面中,这样就能减少不必要的冗余的模板代码。上面的例子是通过Mertics构造Timer实例,实际上也可以使用Builder构造:
1 | MeterRegistry registry = ... |
另外,Timer的使用还可以基于它的内部类Timer.Sample,通过start和stop两个方法记录两者之间的逻辑的执行耗时。例如:
1 | Timer.Sample sample = Timer.start(registry); |
FunctionTimer
FunctionTimer
是Timer
的特化类型, 它需要两个函数:一个用于计数, 一个用于统计总耗时. 它的构造器方法入参如下:
1 | public interface FunctionTimer extends Meter { |
简单的使用方式如下:
1 | public static void main(String[] args) throws Exception { |
LongTaskTimer
LongTaskTimer
也是Timer
的一种特化类型, 主要用于记录长时间执行的任务的持续时间. 在Spring中可以简单地使用@Scheduled和@Timed注解,基于spring-aop完成定时调度任务的总耗时记录:
1 | "aws.scrape", longTask = true) (value = |
也可以手动使用LongTaskTimer
1 | public static void main(String[] args) throws Exception{ |
Gauge
Gauge
是用来记录可以上下浮动的单数值度量Meter
, 测量值用ToDoubleFunction
参数的返回值, 如当前的内存使用情况, 队列中的消息数量等. Gauge一般用于监测有自然上界的事件或者任务,而Counter一般使用于无自然上界的事件或者任务的监测,所以像Http请求总量计数应该使用Counter而非Gauge。 MeterRegistry
中提供了一些便于构建用于观察数值、函数、集合和映射的Gauge相关的方法:
1 | List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size); |
上面的三个方法通过MeterRegistry
构建Gauge并且返回了集合或者映射实例,使用这些集合或者映射实例就能在其size变化过程中记录这个变更值。更重要的优点是,我们不需要感知Gauge接口的存在,只需要像平时一样使用集合或者映射实例就可以了。此外,Gauge还支持java.lang.Number
的子类,java.util.concurrent.atomic
包中的AtomicInteger
和AtomicLong
,还有Guava提供的AtomicDouble
:
1 | AtomicInteger n = registry.gauge("numberGauge", new AtomicInteger(0)); |
除了使用MeterRegistry创建Gauge之外,还可以使用建造器流式创建:
1 | //一般我们不需要操作Gauge实例 |
举个相对实际的例子,假设我们需要对登录后的用户发送一条短信或者推送,做法是消息先投放到一个阻塞队列,再由一个线程消费消息进行其他操作:
1 | private static final MeterRegistry REGISTRY = new SimpleMeterRegistry(); |
TimeGauge
TimeGauge
是Gauge
的特化类型, 相比Gauge
, 它的构造器中多了一个TimeUnit
类型的参数, 用于指定ToDoubleFunction
入参的时间单位.
1 | private static final SimpleMeterRegistry REGISTRY = new SimpleMeterRegistry(); |
控制台输出
1 | name:timeGauge,tags:[],type:GAUGE,value:[Measurement{statistic='VALUE', value=10086.0}] |
DistributionSummary
Summary
主要用于跟踪事件的发布, 使用方式与Timer
类型, 但是它的记录值并不依赖与时间单位. 常见的场景: 记录请求的有效负载大小. 使用MeterRegistry
创建DistributionSummary
的方式如下:
1 | DistributionSummary summary = registry.summary("response.size"); |
使用构造器流式创建:
1 | DistributionSummary summary = DistributionSummary |
使用例子:
1 | private static final DistributionSummary SUMMARY = DistributionSummary.builder("cacheHitPercent") |
2. SpringBoot应用开发
2.1 Maven依赖配置
在pom.xml
中增加如下依赖
1 | <dependency> |
然后spring的配置类中, 为所有指标配置默认标签(应用名, 本机IP)
1 |
|
在spring配置文件中将actuator相关端点暴露出去
1 | management: |
2.2 自定义指标开发
下面在一个接口内演示所有指标的用法
1 |
|
应用启动后, 请求http://localhost:端口/user
接口, 模拟业务接口访问, 然后在打开http://localhost:端口/actuator/prometheus
就可以看到所有指标了. 除了我们自定义的指标, 还是许多默认的JVM, http请求的指标, 这些指标在Grafana
中使用已经配置好的图标模板JVM (Micrometer) 或者 Spring Boot Statistics就可以展示了.
3.Promethues/Grafana配置
3.1 Prometheus配置
应用端配置好以后, 还需要配置Promethues
去定时从应用拉取数据并保存起来. Promethues
安装好以后, 编辑其配置文件(prometheus.yml
)
1 | scrape_configs: |
然后重启Promethues
. 然后打开Promethues
的管理页面http://IP:9090/targets
, 选择Status
> Targets
, 查看应用端点状态是否为UP
. 点击Graph
可以通过PromQL
查询指标数据, 如: long_task_timer_user_seconds_duration_sum
3.2 Grafana图标配置
Promethues
中有数据以后, 还需要在Grafana
中以图表展示, 方便查看. 首先在Grafana
配置好Promethues
数据源, 然后可以新建一个Dashboard
, 在其中配置不同指标的Panel
. 配置语法如下:
Counter
- counter_user_total{instance=”$instance”, application=”$application”}
- rate(counter_user_total{instance=”$instance”, application=”$application”}[1m])
Timer
- timer_user_seconds_count{instance=”$instance”, application=”$application”}
- rate(timer_user_seconds_sum{instance=”$instance”, application=”$application”}[1m])/rate(timer_user_seconds_count{instance=”$instance”, application=”$application”}[1m])
Gauge
- guage_user{application=”$application”, instance=”$instance”}
Summary
irate(http_server_requests_seconds_count{instance=”$instance”, application=”$application”, uri!~”.actuator.“}[3m])
可以在
Legend
填写标签, 这样可以根据标签值自动新建折线 [] -
3.3 Promethues 查询语法PromQL
3.3.1 操作符
数学运算
PromQL
支持的数学运算符:+
(加)-
(减)*
(乘)/
(除)%
(取余)^
(幂运算)1
node_memory_free_bytes_total / (1024 * 1024)
布尔运算
支持的布尔运算符:
==
(相等)!=
(不等)>
(大于)<
(小于)>=
(大于等于)<=
(小于等于)1
(node_memory_bytes_total - node_memory_free_bytes_total) / node_memory_bytes_total > 0.95
bool运算符
通过bool运算符对布尔运算结果进行修改
1
2# 如果大于1000返回1(true) 否则返回0(false)
http_requests_total > bool 1000
集合运算符
支持的集合运算符:
and
(且)or
(或)unless
(排除)vector1 and vector2 会产生一个由vector1的元素组成的新的向量。该向量包含vector1中完全匹配vector2中的元素组成。
vector1 or vector2 会产生一个新的向量,该向量包含vector1中所有的样本数据,以及vector2中没有与vector1匹配到的样本数据。
vector1 unless vector2 会产生一个新的向量,新向量中的元素由vector1中没有与vector2匹配的元素组成。
操作符优先级
1
100 * (1 - avg (irate(node_cpu{mode='idle'}[5m])) by(job) )
- ^
- *, /, %
- +, -
- ==, !=, <=, <, >=, >
- and, unless
- or
3.3.2 集合函数
sum(求和)
1
2
3
4
5
6
7
8
9# 假设系统的日志counter指标如下
logback_events_total{application="untitled",ip="172.17.0.1",level="info",} 12.0
logback_events_total{application="untitled",ip="172.17.0.1",level="debug",} 707.0
logback_events_total{application="untitled",ip="172.17.0.1",level="trace",} 0.0
logback_events_total{application="untitled",ip="172.17.0.1",level="warn",} 106.0
logback_events_total{application="untitled",ip="172.17.0.1",level="error",} 101.0
# 计算各个level日志的总量
sum(logback_events_total)min(最小值)
max(最大值)
avg(平均值)
stddev(标准差)
stdvar(标准方差)
count(计数)
count_values(相同数值分组计数)
bottomk (最小的n条数据)
- topk (最大的n条数据)
3.3.3 内置函数
increase
取时间范围内的最后一个值减去第一个值, 得到增长量
1
2
3
4
5# 计算5分钟内的增长量
increase(http_requests_total{job="api-server"}[5m])
# 计算5分钟内的每秒增长率
increase(http_requests_total{job="api-server"}[5m])/300rate
计算每秒的增长率
1
2
3
4
5# 计算5分钟内的每秒增长率
rate(http_requests_total{job="api-server"}[5m])
# 等同于
increase(http_requests_total{job="api-server"}[5m])/300irate
使用
increase
或者1函数计算平均增长率时, 会有”长尾问题”, 无法反应时间窗口内的数据突变. 比如某一时刻数据突增, 但是平摊到时间窗口后增长率并不突出. 为了解决该问题, 可以使用irate
函数, 它通过时间窗口内的最后两个数据的差值来计算正常率, 所以irate
反应的是数据的瞬时增长率.由于
irate
灵敏度更高, 所以在需要分享长期趋势或在告警规则中, 这种灵敏度反而会造成干扰, 因此此时更推荐使用rate
函数.1
2# 5m内的数据增长率
irate(http_requests_total{job="api-server"}[5m])predict_linear 预测
基于简单线性回归, 对时间范围内的数据进行统计, 预测某段时间后的数值. 只适用于Gauge类型数据
1
2# 基于5分钟的数据, 预测对300秒后数值
predict_linear(guage_user[5m], 300)
histogram_quantile 分位数
统计不同百分比
1
2
3
4
5
6
7
8
9
10
11
12
13
14# TYPE timer_user_seconds histogram
timer_user_seconds{application="untitled",endpoint="user",ip="172.17.0.1",quantile="0.5",} 0.484442112
timer_user_seconds{application="untitled",endpoint="user",ip="172.17.0.1",quantile="0.75",} 0.70254592
timer_user_seconds{application="untitled",endpoint="user",ip="172.17.0.1",quantile="0.9",} 0.903872512
timer_user_seconds_bucket{application="untitled",endpoint="user",ip="172.17.0.1",le="0.001",} 0.0
timer_user_seconds_bucket{application="untitled",endpoint="user",ip="172.17.0.1",le="0.001048576",} 0.0
timer_user_seconds_bucket{application="untitled",endpoint="user",ip="172.17.0.1",le="0.001398101",} 0.0
timer_user_seconds_bucket{application="untitled",endpoint="user",ip="172.17.0.1",le="0.001747626",} 0.0
timer_user_seconds_bucket{application="untitled",endpoint="user",ip="172.17.0.1",le="0.002097151",} 0.0
timer_user_seconds_bucket{application="untitled",endpoint="user",ip="172.17.0.1",le="0.002446676",} 0.0
...
# 在过去1小时内的P95(95%的请求耗时都小于等于这个值)
histogram_quantile(0.95, rate(timer_user_seconds_bucket[1h]))
参考: