开源监控系统 Prometheus 的前世今生

Prometheus 是 SoundCloud 公司开源的监控系统,同时也是继 Kubernetes 之后,第二个加入 CNCF 的项目。Prometheus 是一个优秀的监控系统,沃趣围绕着 Prometheus 先后开发了多个组件,包括基础告警组件,服务发现组件、各种采集的 Exporters 等,这些组件结合 Prometheus 支撑了沃趣大部分的监控业务。本文主要介绍 Prometheus,从他的来源,架构以及一个具体的例子等方面来说明,以及沃趣围绕 Prometheus 做了哪些工作。

起源
SoundCloud 公司的之前的应用架构是巨石架构,也就是所有的功能放在一个大的模块里,各个功能之间没有明显的界线。巨石架构的应用主要存在两方面的问题,一方面在于很难对其进行水平扩展,只能垂直扩展,但是单台机器的能力毕竟是有限的;另外一方面在于各个功能耦合在一块,新增一个功能需要在已有的技术栈上进行开发,并且要确保不会对已有的功能造成影响。于是他们转向了微服务架构,将原有的功能拆分成了几百个独立的服务,整个系统运行上千个实例。迁移到微服务架构给监控带来一定的挑战,现在不仅需要知道某个组件的运行的情况,还要知道服务的整体运行情况。他们当时的监控方案是:StatsD + Graphite + Nagios,StatsD 结合 Graphite 构建监控图表,各个服务将样本数据推送给 StatsD,StatsD 将推送来的样本数据聚合在一起,定时地推送给 Graphite,Graphite 将样本数据保存在时序数据库中,用户根据 Graphite 提供的 API,结合自身监控的需求,构建监控图表,通过图表分析服务的指标(例如,延迟,每秒的请求数,每秒的错误数等)。

那么这样一种方案能满足微服务架构对监控的要求么?什么要求呢:既能知道服务整体的运行情况,也能够保持足够的粒度,知道某个组件的运行情况。答案是很难,为什么呢?例如,我们要统计 api-server 服务响应 POST /tracks 请求错误的数量,指标的名称为 api-server.tracks.post.500,这个指标可以通过 http 状态码来测量,服务响应的状态码为 500 就是错误的。Graphite 指标名称的结构是一种层次结构,api-server 指定服务的名称,tracks 指定服务的 handler,post 指定请求的方法,500 指定请求响应的状态码,api-server 服务实例将该指标推送给 StatsD,StatsD 聚合各个实例推送来的指标,然后定时推送给 Graphite。查询 api-server.tracks.post.500 指标,我们能获得服务错误的响应数,但是,如果我们的 api-server 服务跑了多个实例,想知道某个实例错误的响应数,该怎么查询呢?问题出在使用这样一种架构,往往会将各个服务实例发送来的指标聚合到一块,聚合到一起之后,实例维度的信息就丢失掉了,也就无法统计某个具体实例的指标信息。

StatsD 与 Graphite 的组合用来构建监控图表,告警是另外一个系统 -Nagios- 来做的,这个系统运行检测脚本,判断主机或服务运行的是否正常,如果不正常,发送告警。Nagios 最大的问题在于告警是面向主机的,每个告警的检查项都是围绕着主机的,在分布式系统的环境底下,主机 down 掉是正常的场景,服务本身的设计也是可以容忍节点 down 掉的,但是,这种场景下 Nagios 依然会触发告警。

Prometheus 的解决方案
那么,Prometheus 是如何解决上面这些问题的?之前的方案中,告警与图表的构建依赖于两个不同的系统,Prometheus 采取了一种新的模型,将采集时序数据作为整个系统的核心,无论是告警还是构建监控图表,都是通过操纵时序数据来实现的。Prometheus 通过指标的名称以及 label(key/value) 的组合来识别时序数据,每个 label 代表一个维度,可以增加或者减少 label 来控制所选择的时序数据,前面提到,微服务架构底下对监控的要求:既能知道服务整体的运行情况,也能够保持足够的粒度,知道某个组件的运行情况。借助于这种多维度的数据模型可以很轻松的实现这个目标,还是拿之前那个统计 http 错误响应的例子来说明,我们这里假设 api_server 服务有三个运行的实例,Prometheus 采集到如下格式的样本数据(其中 intance label 是 Prometheus 自动添加上去的):

api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample1"} -> 34
api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample2"} -> 28
api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample3"} -> 31

如果我们只关心特定实例的错误数,只需添加 instance label 即可,例如我们想要查看实例名称为 sample1 的错误的请求数,那么我就可以用
api_server_http_requests_total{method=”POST”,handler=”/tracks”,status=”500″,instance=”sample1″} 这个表达式来选择时序数据,选择的数据如下:

api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="sample1"} -> 34

如果我们关心整个服务的错误数,只需忽略 instance label 去除,然后将结果聚合到一块,即可,例如
sum without(instance) (api_server_http_requests_total{method=”POST”,handler=”/tracks”,status=”500″}) 计算得到的时序数据为:

api_server_http_requests_total{method="POST",handler="/tracks",status="500"} -> 93

告警是通过操纵时序数据而不是运行一个自定义的脚本来实现的,因此,只要能够采集到服务或主机暴露出的指标数据,那么就可以告警。

架构
我们再来简单的分析一下 Prometheus 的架构,看一下各个组件的功能,以及这些组件之间是如何交互的。

Prometheus Server 是整个系统的核心,它定时地从监控目标(Exporters)暴露的 API 中拉取指标,然后将这些数据保存到时序数据库中,如果是监控目标是动态的,可以借助服务发现的机制动态地添加这些监控目标,另外它还会暴露执行 PromQL(用来操纵时序数据的语言)的 API,其他组件,例如 Prometheus Web,Grafana 可以通过这个 API 查询对应的时序数据。Prometheus Server 会定时地执行告警规则,告警规则是 PromQL 表达式,表达式的值是 true 或 false,如果是 true,就将产生的告警数据推送给 alertmanger。告警通知的聚合、分组、发送、禁用、恢复等功能,并不是 Prometheus Server 来做的,而是 Alertmanager 来做的,Prometheus Server 只是将触发的告警数据推送给 Alertmanager,然后 Alertmanger 根据配置将告警聚合到一块,发送给对应的接收人。

如果我们想要监控定时任务,想要 instrument 任务的执行时间,任务执行成功还是失败,那么如何将这些指标暴露给 Prometheus Server?例如每隔一天做一次数据库备份,我们想要知道每次备份执行了多长时间,备份是否成功,我们备份任务只会执行一段时间,如果备份任务结束了,Prometheus Server 该如何拉取备份指标的数据呢?解决这种问题,可以通过 Prometheus 的 pushgateway 组件来做,每个备份任务将指标推送 pushgateway 组件,pushgateway 将推送来的指标缓存起来,Prometheus Server 从 Pushgateway 中拉取指标。

相关的工作
无论是监控图表相关的业务,还是告警相关的业务,都离不开相关指标的采集工作,沃趣是一家做数据库产品的公司,我们花费了很多的精力去采集数据库相关的指标,从 Oracle 到 MySQL,再到 SQL Server,主流的关系型数据库的指标都有采集。对于一些通用的指标,例如操作系统相关的指标,我们主要是借助开源的 Exporters 来采集的。

总结
Promtheus 将采集时序数据作为整个系统的核心,无论是构建监控图表还是告警,都是通过操纵时序数据来完成的。Prometheus 借助多维度的数据模型,以及强大的查询语言满足了微服务架构底下对监控的要求:既能知道服务整体的运行情况,也能够保持足够的粒度,知道某个组件的运行情况。