系统开发中需要统计很多应用指标, 比如: 接口请求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 | (value = "aws.scrape", longTask = true) |
也可以手动使用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]))
参考: