Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NodeJS运维: 从 0 开始 Prometheus + Grafana 业务性能指标监控 #21

Open
SunshowerC opened this issue Nov 29, 2020 · 0 comments

Comments

@SunshowerC
Copy link
Owner

SunshowerC commented Nov 29, 2020

为什么需要指标监控告警

一个复杂的应用,往往由很多个模块组成,而且往往会存在各种各样奇奇怪怪的使用场景,谁也不能保证自己维护的服务永远不会出问题,等用户投诉才发现问题再去处理问题就为时已晚,损失已无法挽回。

所以,通过数据指标来衡量一个服务的稳定性和处理效率,是否正常运作,监控指标曲线的状态,指标出现异常时及时主动告警,这一套工具就十分重要。

常见的一些指标,包括但不限于:

  • QPS
  • 请求处理耗时
  • 进程占用内存
  • 进程占用CPU
  • golang 服务的 goroutine
  • nodejs 的 event loop lag
  • 前端应用的 Performance 耗时
  • ...

举个例子,假如一个服务:

  • 使用内存随着时间逐渐上涨
  • CPU 占用越来越高
  • 请求耗时越来越高,请求成功率下降
  • 磁盘空间频频被挤爆

又或者一个前端单页面应用:

  • 前端重定向到 /error 页,/excption 页的次数越来越多
  • 某个页面打开次数越来越少
  • 某个系统/某个版本的设备的激活率越来越低
  • ...

这到底是人性的扭曲还是道德的沦丧
一旦应用存在某些缺陷导致这些问题,通过服务日志,很难直观快速地察觉到这些指标的变化波动。

而对于前端,则很可能根本无法感知到用户的行为,只能通过埋点进行一定程度地监控。

通过监控和告警手段可以有效地覆盖了「发现」和「定位」问题,从而更有效率地排查和解决问题。

指标监控系统:Prometheus

Prometheus 是一个开源的服务监控系统和时间序列数据库。

工作流可以简化为:

  1. client 采集当前 机器/服务/进程 的状态等相关指标数据
  2. Prometheus server 按一定的时间周期主动拉取 client 的指标数据,并存储到时序数据库中
  3. 发现指标异常后,通过 alert manager 将告警通知给相关负责人

具体的架构设计如下:

为什么不用 mysql 存储?

Prometheus 用的是自己设计的时序数据库(TSDB),那么为什么不用我们更加熟悉,更加常用的 mysql, 或者其他关系型数据库呢?

假设需要监控 WebServerA 每个API的请求量为例,需要监控的维度包括:服务名(job)、实例IP(instance)、API名(handler)、方法(method)、返回码(code)、请求量(value)。

image

如果以SQL为例,演示常见的查询操作:

# 查询 method=put 且 code=200 的请求量
SELECT * from http_requests_total WHERE code=200AND 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.110AND 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: 值,数据的数值

每个指标,有多个时序图;多个时序数据点连接起来,构成一个时序图

image

假如用传统的关系型数据库来表示时序数据,就是以下结构:

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())
  }

image

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

image

并非我们预期中的:

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:
image

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 个时序点。

image

而 Prometheus 的默认查询 sample 上限是 5000w

image

所以,如果指标的时序图数量过大,允许查询的时间区间相对就会较小了

一个图表查询时序数量的影响因素有 3 个,分别是:

  1. 查询条件的时序数量(n)
  2. 查询的时间区间(time)
  3. 图表曲线每个时序点之间的间隔(step)

credit_insight_spl_id_all_pv 指标为例,该指标总共大约有 n = 163698 种时序,
image

假如 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
image

所以,目测可得一个图的查询时序点数量公式是: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
    image

  • 增大 step 到 30s, n 不变:
    image

当然,一般情况下,我们的 n 值只有几百,而 step 基本是大于 60s 的,所以一般情况下都能查询 2 个多月以上的数据图。

可视化平台: Grafana

grafana 是一个开源的,高度可配置的数据图表分析,监控,告警的平台,也是一款前端可视化的产品。

image

自定义图表

grafana 内置提供多种图表模板,具体是以下类型:

Prometheus 作为数据源的情况下,一般用的 graph 类型画时序图比较多。

对于一些基础的数据大盘监控,这些图表类型已经足够满足我们的需求。

但对于复杂的需求,这些类型无法满足我们的需要时,我们安装 pannel 插件,来更新可用的图表类型,也可以根据官方文档 build a panel plugin 开发自己的前端图表 panel。

图表配置

在时序图表配置场景下,我们需要核心关注配置的有:

  1. promQL: 查询语句
  2. Legend: 格式化图例文本
  3. step/interval: 采集点间隔,每隔一段时间,采集一次数据。
    一条曲线的数据点数量 = 图表时长 / 采样间隔。例如查看最近24小时的数据,采样 间隔5min,数据点数量=24*60/5=288。
    采集间隔时间越短,采样率越大,图表数据量越大,曲线越平滑。 采集间隔默认自动计算生成,也可以自定义配置。
  4. metric time range: 每个点的数据统计时间区间时长。
    以QPS为例,图表上每个时间点的数据的意义是:在这时间点上,过去n秒间的访问量。

从上图可以看到,

  • 如果采样间隔 > 统计区间时长: 数据采样率 < 100%。未能采集到的数据丢弃,不会再图表上展示。采样率过小可能会错误异常的数据指标。
  • 如果采样间隔 == 统计区间时长,采样率100%。
  • 如果采样间隔 < 统计区间时长,数据被重复统计,意义不大。

自定义变量

为了实现一些常用的筛选过滤场景,grafana 提供了变量功能

  • 变量配置:变量配置有多种方式(Type),可以自定义选项,也可以根据prometheus 指标的 label 动态拉取。
    image

  • 变量使用:变量通过 $xxx 形式去引用。
    image

告警

除了 Prometheus 本身可以配置告警表达式之外:

image

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 都有些进阶的玩法,大家有兴趣也可以去探索下。

参考文章

  1. Prometheus 的数据存储实现【理论篇】
  2. prometheus tsdb 的存储与索引
  3. query processing would load too many samples into memory in query execution
@SunshowerC SunshowerC changed the title NodeJS: 从 0 开始 Prometheus + Grafana 业务性能指标监控 前端运维: 从 0 开始 Prometheus + Grafana 业务性能指标监控 Nov 29, 2020
@SunshowerC SunshowerC changed the title 前端运维: 从 0 开始 Prometheus + Grafana 业务性能指标监控 NodeJS运维: 从 0 开始 Prometheus + Grafana 业务性能指标监控 Nov 30, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant