为什么需要指标监控告警
一个复杂的应用,往往由很多个模块组成,而且往往会存在各种各样奇奇怪怪的使用场景,谁也不能保证自己维护的服务永远不会出问题,等用户投诉才发现问题再去处理问题就为时已晚,损失已无法挽回。
所以,通过数据指标来衡量一个服务的稳定性和处理效率,是否正常运作,监控指标曲线的状态,指标出现异常时及时主动告警,这一套工具就十分重要。
常见的一些指标,包括但不限于:
- QPS
- 请求处理耗时
- 进程占用内存
- 进程占用CPU
- golang 服务的 goroutine
- nodejs 的 event loop lag
- 前端应用的 Performance 耗时
- ...
举个例子,假如一个服务:
- 使用内存随着时间逐渐上涨
- CPU 占用越来越高
- 请求耗时越来越高,请求成功率下降
- 磁盘空间频频被挤爆
又或者一个前端单页面应用:
- 前端重定向到 /error 页,/excption 页的次数越来越多
- 某个页面打开次数越来越少
- 某个系统/某个版本的设备的激活率越来越低
- ...
这到底是人性的扭曲还是道德的沦丧
一旦应用存在某些缺陷导致这些问题,通过服务日志,很难直观快速地察觉到这些指标的变化波动。
而对于前端,则很可能根本无法感知到用户的行为,只能通过埋点进行一定程度地监控。
通过监控和告警手段可以有效地覆盖了「发现」和「定位」问题,从而更有效率地排查和解决问题。
指标监控系统:Prometheus
Prometheus 是一个开源的服务监控系统和时间序列数据库。
工作流可以简化为:
- client 采集当前 机器/服务/进程 的状态等相关指标数据
- Prometheus server 按一定的时间周期主动拉取 client 的指标数据,并存储到时序数据库中
- 发现指标异常后,通过 alert manager 将告警通知给相关负责人
具体的架构设计如下:
为什么不用 mysql 存储?
Prometheus 用的是自己设计的时序数据库(TSDB),那么为什么不用我们更加熟悉,更加常用的 mysql, 或者其他关系型数据库呢?
假设需要监控 WebServerA 每个API的请求量为例,需要监控的维度包括:服务名(job)、实例IP(instance)、API名(handler)、方法(method)、返回码(code)、请求量(value)。
如果以SQL为例,演示常见的查询操作:
# 查询 method=put 且 code=200 的请求量
SELECT * from http_requests_total WHERE code=”200” AND method=”put” AND created_at BETWEEN 1495435700 AND 1495435710;
# 查询 handler=prometheus 且 method=post 的请求量
SELECT * from http_requests_total WHERE handler=”prometheus” AND method=”post” AND created_at BETWEEN 1495435700 AND 1495435710;
# 查询 instance=10.59.8.110 且 handler 以 query 开头 的请求量
SELECT * from http_requests_total WHERE handler=”query” AND instance=”10.59.8.110” AND created_at BETWEEN 1495435700 AND 1495435710;
通过以上示例可以看出,在常用查询和统计方面,日常监控多用于根据监控的维度进行查询与时间进行组合查询。如果监控100个服务,平均每个服务部署10个实例,每个服务有20个API,4个方法,30秒收集一次数据,保留60天。那么总数据条数为:100(服务)* 10(实例)* 20(API)* 4(方法)* 86400(1天秒数)* 60(天) / 30(秒)= 138.24 亿条数据
,写入、存储、查询如此量级的数据是不可能在Mysql类的关系数据库上完成的。 因此 Prometheus 使用 TSDB 作为 存储引擎。
时序数据库(Time Series Database/TSDB)
时序数据库主要用于指处理带时间标签(按照时间的顺序变化,即时间序列化)的数据,带时间标签的数据也称为时序数据。
对于 prometheus 来说,每个时序点结构如下:
- metric: 指标名,当前数据的标识,有些系统中也称为name。
- label: 标签属性
- timestamp: 数据点的时间,表示数据发生的时间。
- value: 值,数据的数值
每个指标,有多个时序图;多个时序数据点连接起来,构成一个时序图
假如用传统的关系型数据库来表示时序数据,就是以下结构:
create_time | __metric_name__ | path | value |
---|---|---|---|
2020-10-01 00:00:00 | http_request_total | /home | 100 |
2020-10-01 00:00:00 | http_request_total | /error | 0 |
2020-10-01 00:00:15 | http_request_total | /home | 120 |
2020-10-01 00:01:00 | http_request_total | /home | 160 |
2020-10-01 00:01:00 | http_request_total | /error | 1 |
指标 request_total{path="/home"} 在 2020-10-01 00:01:00 时的 qps = (160 - 100)/60 = 1 , 同理,
指标 request_total{path="/error"} 在 2020-10-01 00:01:00 时的 qps = 1/60
相比于 MySQL,时序数据库核心在于时序,其查询时间相关的数据消耗的资源相对较低,效率相对较高,而恰好指标监控数据带有明显的时序特性,所以采用时序数据库作为存储层
数据类型
- counter: 计数器,只能线性增加,不断变大,场景:qps
- gauge:绝对值,非线性,值可大可小,场景:机器温度变化,磁盘容量,CPU 使用率,
- histogram:,聚合数据查询耗时分布【服务端计算,模糊,不精确】
- summary:不能聚合查询的耗时分布【客户端计算,精确】
nodejs 指标采集与数据拉取
- 定义一个 Counter 的数据类型,记录指标
const reqCounter = new Counter({
name: `credit_insight_spl_id_all_pv`,
help: 'request count',
labelNames: ['deviceBrand','systemType', 'appVersion', 'channel']
})
reqCounter.inc({
deviceBrand: 'Apple',
systemType: 'iOS',
appVersion: '26014',
channel: 'mepage'
},1)
- 定义访问路径为
/metrics
的controller
@Get('metrics')
getMetrics(@Res() res) {
res.set('Content-Type', register.contentType)
res.send(register.metrics())
}
- Prometheus 主动请求 node client 的
/metrics
接口,获得
promQL
promQL 是 prometheus 的查询语言,语法十分简单
基本查询
查询指标最新的值:
{__name__="http_request_total", handler="/home"}
# 语法糖:
http_request_total{handler="/home"}
# 等价于 mysql:
select * from http_request_total
where
handler="/home" AND
create_time=《now()》
区间时间段查询
查询过去一分钟内的数据
# promQL
http_request_total[1m]
# 等价于
SELECT * from http_requests_total
WHERE create_time BETWEEN 《now() - 1min》 AND 《now()》;
时间偏移查询
PS: promQL 不支持指定时间点进行查询,只能通过 offset 来查询历史某个点的数据
查询一个小时前的数据。
# promQL
http_request_total offset 1h
# 等价于
SELECT * from http_requests_total
WHERE create_time=《now() - 1 hour》;
promQL 查询函数
根据以上的查询语法,我们可以简单组合出一些指标数据:
例如,查询最近一天内的 /home 页请求数
http_request_total{handler="/home"} - http_request_total{handler="/home"} offset 1d
那么实际上面这个写法很明显比较不简洁,我们可使用内置 increase 函数来替换:
# 和上述写法等价
increase(http_request_total{handler="/home"}[1d])
除了 increase 外,还有很多其他好用的函数,例如,
rate 函数计算 QPS
// 过去的 2 分钟内平均每秒请求数
rate(http_request_total{code="400"}[2m])
// 等价于
increase(http_request_total{code="400"}[2m]) / 120
指标聚合查询
除了上述基础查询外,我们可能还需要聚合查询
假如我们有以下数据指标:
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 访问量都包括在内。
我们理所当然地会写出:
increase(credit_insight_spl_id_all_pv{url="/home"}[1d])
但实际上我们会得出这样的两条指标结果:
credit_insight_spl_id_all_pv{url="/home",channel="none"} 233
credit_insight_spl_id_all_pv{url="/home",channel="mepage"} 666
并非我们预期中的:
credit_insight_spl_id_all_pv{url="/home"} 899
而要是我们想要得到这样的聚合查询结果,就需要用到 sum by
# 聚合 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;
指标时序曲线
以上的所有例子的查询数值,其实都是最近时间点的数值,
而我们更关注的是一个时间段的数值变化。
要实现这个原理也很简单,只需要在历史的每个时间点都执行一次指标查询,
# 假如今天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 个,分别是:
- 查询条件的时序数量(n)
- 查询的时间区间(time)
- 图表曲线每个时序点之间的间隔(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 个多月以上的数据图。
可视化平台: Grafana
grafana 是一个开源的,高度可配置的数据图表分析,监控,告警的平台,也是一款前端可视化的产品。
自定义图表
grafana 内置提供多种图表模板,具体是以下类型:
Prometheus 作为数据源的情况下,一般用的 graph 类型画时序图比较多。
对于一些基础的数据大盘监控,这些图表类型已经足够满足我们的需求。
但对于复杂的需求,这些类型无法满足我们的需要时,我们安装 pannel 插件,来更新可用的图表类型,也可以根据官方文档 build a panel plugin 开发自己的前端图表 panel。
图表配置
在时序图表配置场景下,我们需要核心关注配置的有:
- promQL: 查询语句
- Legend: 格式化图例文本
- step/interval: 采集点间隔,每隔一段时间,采集一次数据。
一条曲线的数据点数量 = 图表时长 / 采样间隔。例如查看最近24小时的数据,采样 间隔5min,数据点数量=24*60/5=288。
采集间隔时间越短,采样率越大,图表数据量越大,曲线越平滑。 采集间隔默认自动计算生成,也可以自定义配置。 - metric time range: 每个点的数据统计时间区间时长。
以QPS为例,图表上每个时间点的数据的意义是:在这时间点上,过去n秒间的访问量。
从上图可以看到,
- 如果采样间隔 > 统计区间时长: 数据采样率 < 100%。未能采集到的数据丢弃,不会再图表上展示。采样率过小可能会错误异常的数据指标。。
- 如果采样间隔 == 统计区间时长,采样率100%。
- 如果采样间隔 < 统计区间时长,数据被重复统计,意义不大。
自定义变量
为了实现一些常用的筛选过滤场景,grafana 提供了变量功能
- 变量配置:变量配置有多种方式(Type),可以自定义选项,也可以根据prometheus 指标的 label 动态拉取。
- 变量使用:变量通过
$xxx
形式去引用。
告警
除了 Prometheus 本身可以配置告警表达式之外:
grafana 也可以配置告警:
数据源
Prometheus 通常用于后端应用的指标数据实时上报,主要用于异常告警,问题排查,所以数据存在时效性,我们不会关注几个月前的一个已经被排查并 fixed 的指标异常波动告警。
但是,要是我们将 Prometheus 用于业务指标监控,那么我们可能会关注更久远的数据。
例如我们可能想要看过去一个季度的环比同比增长,用 Prometheus 作为数据源就不合适,因为 Prometheus 是时序数据库,更多关注实时数据,数据量大,当前数据保存的时效设定只有 3 个月。
那么这个时候可能我们要维护一个长期的统计数据,可能就需要存储在 mysql 或者其他存储方式。
grafana 不是 Prometheus 的专属产品,还支持多种数据源,包括但不限于:
常见数据库
- MySql
- SQL Server
- PostgreSQL
- Oracle
日志、文档数据库
- Loki
- Elasticsearch
时序数据库
- Prometheus
- graphite
- openTSDB
- InfluxDB
链路追踪
- Jaeger
- Zipkin
- ....
如果没有自己需要的数据源配置,还可以安装 REST API Datasource Plugin, 通过 http 接口查询作为数据源
总结
了解 grafana 的高度可配置性设计后,有值得思考的几点:
- 关注其设计思想,如果要自己实现一个类似的可视化的 web app,自己会怎么设计?
- 自己要做一个高度可配置化的功能,又应该怎么设计?
- 深入到业务,例如我们常用的 admin 管理 系统,一些常用的业务功能是否可以高度可配置化?业务强关联的如何做到配置与业务的有机结合?
等等这些,其实都是值得我们去思考的。
此外,Prometheus 和 grafana 都有些进阶的玩法,大家有兴趣也可以去探索下。
参考文章
- Prometheus 的数据存储实现【理论篇】
- prometheus tsdb 的存储与索引
- query processing would load too many samples into memory in query execution
本文首发于 github 博客
如文章对你有帮助,你的 star 是对我最大的支持其他文章:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。