Prometheus作为一套完整的开源监控接近方案,因为其诸多强大的特性以及生态的开放性,俨然已经成为了监控领域的事实标准并在全球范围内得到了广泛的部署应用。那么应该如何利用Prometheus对我们的应用形成有效的监控呢?事实上,作为应用我们仅仅需要以符合Prometheus标准的方式暴露监控数据即可,后续对于监控数据的采集,处理,保存都将由Prometheus自动完成。
一般来说,Prometheus监控对象有两种:如果用户对应用的代码有定制化能力,Prometheus提供了各种语言的SDK,用户能够方便地将其集成至应用中,从而对应用的内部状态进行有效监控并将数据以符合Prometheus标准的格式对外暴露;对于MySQL,Nginx等应用,一方面定制化代码难度颇大,另一方面它们已经以某种格式对外暴露了监控数据,对于此类应用,我们需要一个中间组件,利用此类应用的接口获取原始监控数据并转化成符合Prometheus标准的格式对外暴露,此类中间组件,我们称之为Exporter,社区中已经有大量现成的Exporter可以直接使用。
本文将以一个由Golang编写的HTTP Server为例,借此说明如何利用Prometheus的Golang SDK,逐步添加各种监控指标并以标准的方式暴露应用的内部状态。后续的内容将分为基础、进阶两个部分,通常第一部分的内容就足以满足大多数需求,但是若想要获得更多的定制化能力,那么第二部分的内容可以提供很好的参考。
1. 基础
client_golang
package main import ( "net/http" "github.com/prometheus/client_golang/prometheus/promhttp" ) func main() { http.Handle("/metrics", promhttp.Handler()) http.ListenAndServe(":8080", nil) }
client_golang/metrics
$ curl http://127.0.0.1:8080/metrics ... # HELP go_goroutines Number of goroutines that currently exist. # TYPE go_goroutines gauge go_goroutines 7 # HELP go_info Information about the Go environment. # TYPE go_info gauge go_info{version="go1.12.1"} 1 # HELP go_memstats_alloc_bytes Number of bytes allocated and still in use. # TYPE go_memstats_alloc_bytes gauge go_memstats_alloc_bytes 418912 ...
# HELP# TYPE/metrics
对于一个HTTP Server来说,了解当前请求的接收速率是非常重要的。Prometheus支持一种称为Counter的数据类型,这一类型本质上就是一个只能单调递增的计数器。如果我们定义一个Counter表示累积接收的HTTP请求的数目,那么最近一段时间内该Counter的增长量其实就是接收速率。另外,Prometheus中定义了一种自定义查询语句PromQL,能够方便地对样本的监控数据进行统计分析,包括对于Counter类型的数据求速率。因此,经过修改后的程序如下:
package main import ( "net/http" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( http_request_total = promauto.NewCounter( prometheus.CounterOpts{ Name: "http_request_total", Help: "The total number of processed http requests", }, ) ) func main() { http.HandleFunc("/", func(http.ResponseWriter, *http.Request){ http_request_total.Inc() }) http.Handle("/metrics", promhttp.Handler()) http.ListenAndServe(":8080", nil) }
promautoNewCounter_totalInc()Add()
/metrics
$ curl http://127.0.0.1:8080/metrics | grep http_request_total # HELP http_request_total The total number of processed http requests # TYPE http_request_total counter http_request_total 5
监控累积的请求处理显然还是不够的,通常我们还想知道当前正在处理的请求的数量。Prometheus中的Gauge类型数据,与Counter不同,它既能增大也能变小。将正在处理的请求数量定义为Gauge类型是合适的。因此,我们新增的代码块如下:
... var ( ... http_request_in_flight = promauto.NewGauge( prometheus.GaugeOpts{ Name: "http_request_in_flight", Help: "Current number of http requests in flight", }, ) ) ... http.HandleFunc("/", func(http.ResponseWriter, *http.Request){ http_request_in_flight.Inc() defer http_request_in_flight.Dec() http_request_total.Inc() }) ...
Dec()Sub()
对于一个网络服务来说,能够知道它的平均时延是重要的,不过很多时候我们更想知道响应时间的分布状况。Prometheus中的Histogram类型就对此类需求提供了很好的支持。具体到需要新增的代码如下:
... var ( ... http_request_duration_seconds = promauto.NewHistogram( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "Histogram of lantencies for HTTP requests", // Buckets: []float64{.1, .2, .4, 1, 3, 8, 20, 60, 120}, }, ) ) ... http.HandleFunc("/", func(http.ResponseWriter, *http.Request){ now := time.Now() http_request_in_flight.Inc() defer http_request_in_flight.Dec() http_request_total.Inc() time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) http_request_duration_seconds.Observe(time.Since(now).Seconds()) }) ...
/metrics
# HELP http_request_duration_seconds Histogram of lantencies for HTTP requests # TYPE http_request_duration_seconds histogram http_request_duration_seconds_bucket{le="0.005"} 0 http_request_duration_seconds_bucket{le="0.01"} 0 http_request_duration_seconds_bucket{le="0.025"} 0 http_request_duration_seconds_bucket{le="0.05"} 0 http_request_duration_seconds_bucket{le="0.1"} 3 http_request_duration_seconds_bucket{le="0.25"} 3 http_request_duration_seconds_bucket{le="0.5"} 5 http_request_duration_seconds_bucket{le="1"} 8 http_request_duration_seconds_bucket{le="2.5"} 8 http_request_duration_seconds_bucket{le="5"} 8 http_request_duration_seconds_bucket{le="10"} 8 http_request_duration_seconds_bucket{le="+Inf"} 8 http_request_duration_seconds_sum 3.238809838 http_request_duration_seconds_count 8
_sum_count+Inf_count+Inf
与Histogram类似,Prometheus中定义了一种类型Summary,从另一个角度描绘了数据的分布状况。对于响应时延,我们可能想知道它们的中位数是多少?九分位数又是多少?对于Summary类型数据的定义及使用如下:
... var ( ... http_request_summary_seconds = promauto.NewSummary( prometheus.SummaryOpts{ Name: "http_request_summary_seconds", Help: "Summary of lantencies for HTTP requests", // Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001, 0.999, 0.0001}, }, ) ) ... http.HandleFunc("/", func(http.ResponseWriter, *http.Request){ now := time.Now() http_request_in_flight.Inc() defer http_request_in_flight.Dec() http_request_total.Inc() time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) http_request_duration_seconds.Observe(time.Since(now).Seconds()) http_request_summary_seconds.Observe(time.Since(now).Seconds()) }) ...
Summary的定义和使用与Histogram是类似的,最终我们得到的结果如下:
$ curl http://127.0.0.1:8080/metrics | grep http_request_summary # HELP http_request_summary_seconds Summary of lantencies for HTTP requests # TYPE http_request_summary_seconds summary http_request_summary_seconds{quantile="0.5"} 0.31810446 http_request_summary_seconds{quantile="0.9"} 0.887116164 http_request_summary_seconds{quantile="0.99"} 0.887116164 http_request_summary_seconds_sum 3.2388269649999994 http_request_summary_seconds_count 8
_sum_count
事实上,上述的Counter,Gauge,Histogram,Summary就是Prometheus能够支持的全部监控数据类型了(其实还有一种类型Untyped,表示未知类型)。一般使用最多的是Counter和Gauge这两种基本类型,结合PromQL对基础监控数据强大的分析处理能力,我们就能获取极其丰富的监控信息。
//foo
Prometheus对于此类问题的方法是为指标的每个特征维度定义一个label,一个label本质上就是一组键值对。一个指标可以和多个label相关联,而一个指标和一组具体的label可以唯一确定一条时间序列。对于上述分别统计每条路径的请求数目的问题,标准的Prometheus的解决方法如下:
... var ( http_request_total = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "http_request_total", Help: "The total number of processed http requests", }, []string{"path"}, ) ... http.HandleFunc("/", func(http.ResponseWriter, *http.Request){ ... http_request_total.WithLabelValues("root").Inc() ... }) http.HandleFunc("/foo", func(http.ResponseWriter, *http.Request){ ... http_request_total.WithLabelValues("foo").Inc() ... }) )
NewCounterVecpath//fooWithLabelValuesrootfoo/metrics
$ curl http://127.0.0.1:8080/metrics | grep http_request_total # HELP http_request_total The total number of processed http requests # TYPE http_request_total counter http_request_total{path="foo"} 9 http_request_total{path="root"} 5
http_request_totalfooroottotal
sum(http_request_total)
label在Prometheus中是一个简单而强大的工具,理论上,Prometheus没有限制一个指标能够关联的label的数目。但是,label的数目也并不是越多越好,因为每增加一个label,用户在使用PromQL的时候就需要额外考虑一个label的配置。一般来说,我们要求添加了一个label之后,对于指标的求和以及求均值都是有意义的。
2. 进阶
基于上文所描述的内容,我们就能很好地在自己的应用程序里面定义各种监控指标并且保证它能被Prometheus接收处理了。但是有的时候我们可能需要更强的定制化能力,尽管使用高度封装的API确实很方便,不过它附加的一些东西可能不是我们想要的,比如默认的Handler提供的Golang运行时相关以及进程相关的一些监控指标。另外,当我们自己编写Exporter的时候,该如何利用已有的组件,将应用原生的监控指标转化为符合Prometheus标准的指标。为了解决上述问题,我们有必要对Prometheus SDK内部的实现机理了解地更为深刻一些。
在Prometheus SDK中,Register和Collector是两个核心对象。Collector里面可以包含一个或者多个Metric,它事实上是一个Golang中的interface,提供如下两个方法:
type Collector interface { Describe(chan<- *Desc) Collect(chan<- Metric) }
/metrics
promautopromauto.NewCounter
http_request_total = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "http_request_total", Help: "The total number of processed http requests", }, []string{"path"}, ) --- // client_golang/prometheus/promauto/auto.go func NewCounterVec(opts prometheus.CounterOpts, labelNames []string) *prometheus.CounterVec { c := prometheus.NewCounterVec(opts, labelNames) prometheus.MustRegister(c) return c } --- // client_golang/prometheus/counter.go func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec { desc := NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, labelNames, opts.ConstLabels, ) return &CounterVec{ metricVec: newMetricVec(desc, func(lvs ...string) Metric { if len(lvs) != len(desc.variableLabels) { panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs)) } result := &counter{desc: desc, labelPairs: makeLabelPairs(desc, lvs)} result.init(result) // Init self-collection. return result }), } }
promautoprometheus.MustRegister(c)prometheus.MustRegisterpromhttp.Handler()/metrics
当然,Registry和Collector也都是能自定义的,特别在编写Exporter的时候,我们往往会将所有的指标定义在一个Collector中,根据访问应用原生监控接口的结果对所需的指标进行填充并返回结果。基于上述对于Prometheus SDK的实现机制的理解,我们可以实现一个最简单的Exporter框架如下所示:
package main import ( "net/http" "math/rand" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) type Exporter struct { up *prometheus.Desc } func NewExporter() *Exporter { namespace := "exporter" up := prometheus.NewDesc(prometheus.BuildFQName(namespace, "", "up"), "If scrape target is healthy", nil, nil) return &Exporter{ up: up, } } func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- e.up } func (e *Exporter) Scrape() (up float64) { // Scrape raw monitoring data from target, may need to do some data format conversion here rand.Seed(time.Now().UnixNano()) return float64(rand.Intn(2)) } func (e *Exporter) Collect(ch chan<- prometheus.Metric) { up := e.Scrape() ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, up) } func main() { registry := prometheus.NewRegistry() exporter := NewExporter() registry.Register(exporter) http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) http.ListenAndServe(":8080", nil) }
/metricsNewDesc()/metricsScrape()MustNewConstMetric()/metrics
$ curl http://127.0.0.1:8080/metrics # HELP exporter_up If scrape target is healthy # TYPE exporter_up gauge exporter_up 1
3. 总结
经过本文的分析,可以发现,利用Prometheus SDK将应用程序进行简单的二次开发,它就能被Prometheus有效地监控,从而享受整个Prometheus监控生态带来的便利。同时,Prometheus SDK也提供了多层次的抽象,通常情况下,高度封装的API就能快速地满足我们的需求。至于更多的定制化需求,Prometheus SDK也有很多底层的,更为灵活的API可供使用。