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

Prometheus是SoundCloud公司开源的监控系统,同时也是继Kubernetes之后,第二个加入CNCF的项目。Prometheus是一个优秀的监控系统,沃趣围绕着Prometheus先后开发了多个组件,包括基础告警组件,服务发现组件、各种采集的Exporters等,这些组件结合Prometheus支撑了沃趣大部分的监控业务。本文主要介绍Prometheus,从他的来源,架构以及一个具体的例子等方面来说明,以及沃趣围绕Prometheus做了哪些工作。 如果你想和更多Prometheus技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

起源

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依然会触发告警。

如果大家之前看过这篇 https://landing.google.com/sre … arker 介绍Google Borgmon的文章,对比Prometheus,你会发现这两个系统非常相似。实际上,Prometheus深受Borgmon系统的影响,并且当时参与构建Google监控系统的员工加入了SoundCloud公司。总之,种种因素的结合,促使了Prometheus系统的诞生。

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中拉取指标。

例子

前面都是从比较大的层面——背景、架构——来介绍Prometheus,现在,让我们从一个具体的例子出发,来看一下如何借助Prometheus来构建监控图表、分析系统性能以及告警。

我们有个服务,暴露出四个API,每个API只返回一些简单的文本数据,现在,我们要对这个服务进行监控,希望借助监控能够查看、分析服务的请求速率,请求的平均延迟以及请求的延迟分布,并且当应用的延迟过高或者不可访问时能够触发告警,代码示例如下:

package main



import (

"math/rand"

"net/http"

"time"



"github.com/prometheus/client_golang/prometheus"

"github.com/prometheus/client_golang/prometheus/promauto"

"github.com/prometheus/client_golang/prometheus/promhttp"

)



var (

Latency = promauto.NewHistogramVec(prometheus.HistogramOpts{

    Help: "latency of sample app",

    Name: "sample_app_latency_milliseconds",

    Buckets: prometheus.ExponentialBuckets(10, 2, 9),

}, []string{"handler", "method"})

)



func instrumentationFilter(f http.HandlerFunc) http.HandlerFunc {

return func(writer http.ResponseWriter, request *http.Request) {

    now := time.Now()

    f(writer, request)

    duration := time.Now().Sub(now)

    Latency.With(prometheus.Labels{"handler": request.URL.Path, "method": request.Method}).

        Observe(float64(duration.Nanoseconds()) / 1e6)

}

}



// jitterLatencyFilter make request latency between d and d*maxFactor

func jitterLatencyFilter(d time.Duration, maxFactor float64, f http.HandlerFunc) http.HandlerFunc {

return func(writer http.ResponseWriter, request *http.Request) {

    time.Sleep(d + time.Duration(rand.Float64()*maxFactor*float64(d)))

    f(writer, request)

}

}



func main() {

rand.Seed(time.Now().UnixNano())



http.Handle("/metrics", promhttp.Handler())

http.Handle("/a", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 256, func(w http.ResponseWriter, r *http.Request) {

    w.Write([]byte("success"))

})))

http.Handle("/b", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 128, func(w http.ResponseWriter, r *http.Request) {

    w.Write([]byte("success"))

})))

http.Handle("/c", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 64, func(w http.ResponseWriter, r *http.Request) {

    w.Write([]byte("success"))

})))

http.Handle("/d", instrumentationFilter(jitterLatencyFilter(10*time.Millisecond, 32, func(w http.ResponseWriter, r *http.Request) {

    w.Write([]byte("success"))

})))

http.ListenAndServe(":5001", nil)

} 

我们按照instrumentation、exposition、collection、query这样的流程构建监控系统,instrumentation关注的是如何测量应用的指标,有哪些指标需要测量;exposition关注的是如何通过http协议将指标暴露出来;collection关注的是如何采集指标;query关注的是如何构建查询时序数据的PromQL表达式。我们首先从instrumentation这里,有四个指标是我们关心的:

  • 请求速率
  • 请求的平均延迟
  • 请求的延迟分布
  • 访问状态
var (

Latency = promauto.NewHistogramVec(prometheus.HistogramOpts{

    Help: "latency of sample app",

    Name: "sample_app_latency_milliseconds",

    Buckets: prometheus.ExponentialBuckets(10, 2, 9),

}, []string{"handler", "method"})

)

首先将指标注册进来,然后追踪、记录指标的值。用Prometheus提供的golang客户端库可以方便的追踪、记录指标的值,我们将instrumentation code放到应用的代码里,每次请求,对应的指标状态的值就会被记录下来。

client golang提供了四种指标类型,分别为Counter, Gauge, Histogram, Summary,Counter类型的指标用来测量只会增加的值,例如服务的请求数;Gauge类型的指标用来测量状态值,即可以变大,也可以变小的值,例如请求的延迟时间;Histogram与Summary指标类似,这两个指标取样观察的值,记录值的分布,统计观察值的数量,累计观察到的值,可以用它来统计样本数据的分布。为了采集请求速率、平均延迟以及延迟分布指标,方便起见用Histogram类型的指标追踪、记录每次请求的情况,Histogram类型的指标与普通类型(Counter、Gauge)不同的地方在于会生成多条样本数据,一个是观察样本的总数,一个是观察样本值的累加值,另外是一系列的记录样本百分位数的样本数据。访问状态可以使用up指标来表示,每次采集时,Prometheus会将采集的健康状态记录到up指标中。

http.Handle("/metrics", promhttp.Handler())

instrumentation完成之后,下一步要做的就是exposition,只需将Prometheus http handler添加进来,指标就可以暴露出来。访问这个Handler返回的样本数据如下(省略了一些无关的样本数据):

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="10"} 0

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="20"} 0

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="40"} 0

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="80"} 0

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="160"} 0

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="320"} 0

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="640"} 1

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="1280"} 1

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="2560"} 1

sample_app_latency_milliseconds_bucket{handler="/d",method="GET",le="+Inf"} 1

sample_app_latency_milliseconds_sum{handler="/d",method="GET"} 326.308075

sample_app_latency_milliseconds_count{handler="/d",method="GET"} 1

仅仅将指标暴露出来,并不能让prometheus server来采集指标,我们需要进行第三步collection,配置prometheus server发现我们的服务,从而采集服务暴露出的样本数据。我们简单地看下prometheus server的配置,其中,global指定采集时全局配置, scrape_interval 指定采集的间隔, evaluation_interval 指定 alerting rule (alerting rule是PromQL表达式,值为布尔类型,如果为true就将相关的告警通知推送给Alertmanager)也就是告警规则的求值时间间隔,scrape_timeout指定采集时的超时时间;alerting指定Alertmanager服务的地址;scrape_configs指定如何发现监控对象,其中job_name指定发现的服务属于哪一类,static_configs指定服务静态的地址,前面我们也提到,Prometheus支持动态服务发现,例如文件、kubernetes服务发现机制,这里我们使用最简单的静态服务发现机制。

# my global config

global:

  scrape_interval:     2s # Set the scrape interval to every 15 seconds. Default is every 1 minute.

  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.

  # scrape_timeout is set to the global default (10s).

rule_files:

- rule.yaml



# Alertmanager configuration

alerting:

  alertmanagers:

  - static_configs:

    - targets:

      - localhost:9093



scrape_configs:

- job_name: sample-app

  scrape_interval: 3s

  static_configs:

  - targets:

    - sample:5001

采集完指标,就可以利用Prometheus提供的PromQL语言来操纵采集的时序数据,例如,我们想统计请求的平均速率,可以用这个表达式

irate(sample_app_latency_milliseconds_sum[1m]) / irate(sample_app_latency_milliseconds_count[1m])来计算。

有了时序数据之后,就可以借助Grafana来构建监控图表,具体怎么配置Grafana图表在这里就不展开了,核心点是利用PromQL表达式选择、计算时序数据。

Prometheus的告警是通过对Alerting Rule求值来实现的,alerting rule是一系列的PromQL表达式,alerting rule保存在配置文件中。我们想要对应用的延迟以及可用状态进行告警,当应用过高或者不可访问时就触发告警,规则可以如下这样定义:

- name: sample-up

  rules:

  - alert: UP

    expr: up{instance="sample:5001"} == 0

    for: 1m

    labels:

      severity: page

    annotations:

      summary: Service health

  - alert: 95th-latency

    expr: histogram_quantile(0.95, rate(sample_app_latency_milliseconds_bucket[1m])) > 1000

    for: 1m

    labels:

      severity: page

    annotations:

      summary: 95th service latency

其中UP指定服务的可用状态,95th-latency指定95%的请求大于1000毫秒就触发告警。Prometheus定时的对这些规则进行求值,如果条件满足,就将告警通知发送给Alertmanger,Alertmanger会根据自身路由配置,对告警进行聚合,分发到指定的接收人,我们想通过邮箱接收到告警,可以如下进行配置:

global:

  smtp_smarthost: 

  smtp_auth_username: 

  smtp_from: 

  smtp_auth_password: 

  smtp_require_tls: false

  resolve_timeout: 5m

route:

  receiver: me

receivers:

- name: me

  email_configs:

  - to: example@domain.com

templates:

- '*.tmpl'

这样,我们就可以通过邮箱收到告警邮件了。

相关的工作

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

沃趣大部分场景中,要监控的服务都是动态的。比如,用户从平台上申请了一个数据库,需要增加相关的监控服务,用户删除数据库资源,需要移除相关的监控服务,要监控的数据库服务处于动态的变化之中。沃趣每个产品线的基础架构都不相同,数据库服务有跑在Oracle RAC上的,有跑在ZStack的,有跑在Kubernetes上的。对于跑在Kubernetes上的应用来说,并需要担心Prometheus怎么发现要监控的服务,只需要配置相关的服务发现的机制就可以了。对于其他类型的,我们主要借助Prometheus的file_sd服务发现机制来实现,基于文件的服务发现机制是一种最通用的机制,我们将要监控的对象写到一个文件中,Prometheus监听这个文件的变动,动态的维护要监控的对象,我们在file_sd基础上构建了专门的组件去负责服务的动态更新,其他应用调用这个组件暴露的API来维护自身想要监控的对象。

Prometheus本身的机制的并不能满足我们业务上对告警的要求,一方面我们需要对告警通知进行统计,但是Alertmanager本身并没有对告警通知做持久化,服务重启之后告警通知就丢失掉了;另外一方面用户通过Web页面来配置相关的告警,告警规则以及告警通知的路由需要根据用户的配置动态的生成。为了解决这两方面的问题,我们将相关的业务功能做成基础的告警组件,供各个产品线去使用。针对Alertmanager不能持久化告警通知的问题,基础告警组件利用Alertmanager webhook的机制来接收告警通知,然后将通知保存到数据库中;另外用户的告警配置需要动态的生成,我们定义了一种新的模型来描述我们业务上的告警模型。

总结

Promtheus将采集时序数据作为整个系统的核心,无论是构建监控图表还是告警,都是通过操纵时序数据来完成的。Prometheus借助多维度的数据模型,以及强大的查询语言满足了微服务架构底下对监控的要求:既能知道服务整体的运行情况,也能够保持足够的粒度,知道某个组件的运行情况。沃趣站在巨人的肩旁上,围绕Prometheus构建了自己的监控系统,从满足不同采集要求的Exporters到服务发现,最后到基础告警组件,这些组件结合Prometheus,构成了沃趣监控系统的核心。

作者:郭振,沃趣科技开发工程师,多年的Python、Golang等语言的开发经验,熟悉Kubernetes、Prometheus等云原生应用,负责QFusion RDS平台以及基础告警平台的研发工作。