prometheus入门实例
什么是 PrometheusPrometheus是由 SoundCloud 开源监控告警解决方案,从 2012 年开始编写代码,再到 2015 年 github 上开源以来,已经吸引了 9k+ 关注,以及很多大公司的使用;2016 年 Prometheus 成为继 k8s 后,第二名 CNCF(Cloud Native Computing Foundation) 成员。作为新一代开源解决...
什么是 Prometheus
Prometheus 是由 SoundCloud 开源监控告警解决方案,从 2012 年开始编写代码,再到 2015 年 github 上开源以来,已经吸引了 9k+ 关注,以及很多大公司的使用;2016 年 Prometheus 成为继 k8s 后,第二名 CNCF(Cloud Native Computing Foundation) 成员。
作为新一代开源解决方案,很多理念与 Google SRE 运维之道不谋而合。
核心组件
- Prometheus Server, 主要用于抓取数据和存储时序数据,另外还提供查询和 Alert Rule 配置管理。
- client libraries,用于对接 Prometheus Server, 可以查询和上报数据。
- push gateway ,用于批量,短期的监控数据的汇总节点,主要用于业务数据汇报等。
- 各种汇报数据的 exporters ,例如汇报机器数据的 node_exporter, 汇报 MongoDB 信息的 MongoDB exporter 等等。
- 用于告警通知管理的 alertmanager 。
基础架构
从这个架构图,也可以看出 Prometheus 的主要模块包含, Server, Exporters, Pushgateway, PromQL, Alertmanager, WebUI 等。
它大致使用逻辑是这样:
- Prometheus server 定期从静态配置的 targets 或者服务发现的 targets 拉取数据。
- 当新拉取的数据大于配置内存缓存区的时候,Prometheus 会将数据持久化到磁盘(如果使用 remote storage 将持久化到云端)。
- Prometheus 可以配置 rules,然后定时查询数据,当条件触发的时候,会将 alert 推送到配置的 Alertmanager。
- Alertmanager 收到警告的时候,可以根据配置,聚合,去重,降噪,最后发送警告。
- 可以使用 API, Prometheus Console 或者 Grafana 查询和聚合数据。
基本概念
Prometheus 所有采集的监控数据均以指标(metric)的形式保存在内置的时间序列数据库当中(TSDB):属于同一指标名称,同一标签集合的、有时间戳标记的数据流。除了存储的时间序列,Prometheus 还可以根据查询请求产生临时的、衍生的时间序列作为返回结果。
样本在时间序列中的每一个点称为一个样本(sample),样本由以下三部分组成:
指标(metric):指标名称和描述当前样本特征的 labelsets;
时间戳(timestamp):一个精确到毫秒的时间戳;
样本值(value): 一个 folat64 的浮点型数据表示当前样本的值。
<metric name>{<label name>=<label value>, ...}
metric类型
一种累加的 metric,典型的应用如:请求的个数,结束的任务数,出现的错误数等。随着客户端不断请求,数值越来越大。例如api_http_requests_total{method="POST", handler="/messages"}
一种常规的 metric,典型的应用如:温度,运行的 goroutines 的个数。返回的数值会上下波动。
例如go_goroutines{instance="172.17.0.2", job="Prometheus"}
3.Histogram
可以理解为柱状图,典型的应用如:请求持续时间,响应大小。可以对观察结果采样,分组及统计。例如设置一个name为web_request_duration_seconds的Histogram 的metrics,并设置区间值为[0.1,0.5,1]
会对区间点生成一条统计数据。
响应时间小于0.1s的请求有3次
web_request_duration_seconds_bucket{endpoint="/query",method="GET",le="0.1"} 3
# 响应时间小于0.5s的请求有5次
web_request_duration_seconds_bucket{endpoint="/query",method="GET",le="0.5"} 5
# 响应时间小于1s的请求有7次
web_request_duration_seconds_bucket{endpoint="/query",method="GET",le="1"} 7
# 总共7次请求
web_request_duration_seconds_bucket{endpoint="/query",method="GET",le="+Inf"} 7
# 7次请求duration的总和
web_request_duration_seconds_sum{endpoint="/query",method="GET"} 2.7190880529999997
类似于 Histogram, 典型的应用如:请求持续时间,响应大小。
主要做统计用,设置分位数的值,会实时返回该分位数上的值。
案例一:golang中使用现有类型
例子地址:https://github.com/crockitwood/go-prometheus-example
下边是monitor
package monitor
import (
"github.com/prometheus/client_golang/prometheus"
"net/http"
"time"
)
// 初始化counter类型指标, 表示接收http请求总次数
var WebRequestTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "web_reqeust_total",
Help: "Number of hello requests in total",
},
// 设置两个标签 请求方法和 路径 对请求总次数在两个
[]string{"method", "endpoint"},
)
// bucket代表duration的分布区间
var WebRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "web_request_duration_seconds",
Help: "web request duration distribution",
Buckets: []float64{0.1, 0.3, 0.5, 0.7, 0.9, 1},
},
[]string{"method", "endpoint"},
)
func init() {
// 注册监控指标
prometheus.MustRegister(WebRequestTotal)
prometheus.MustRegister(WebRequestDuration)
}
// 包装 handler function,不侵入业务逻辑
func Monitor(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
h(w, r)
duration := time.Since(start)
WebRequestTotal.With(prometheus.Labels{"method": r.Method, "endpoint": r.URL.Path}).Inc()
WebRequestDuration.With(prometheus.Labels{"method": r.Method, "endpoint": r.URL.Path}).Observe(duration.Seconds())
}
}
package main
import (
"github.com/crockitwood/go-prometheus-example/monitor"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"io"
"log"
"math/rand"
"net/http"
"time"
)
func main() {
// expose prometheus metrics接口
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/hello", Hello)
http.HandleFunc("/query", monitor.Monitor(Query))
log.Fatal(http.ListenAndServe(":9090", nil))
}
// hello
func Hello(w http.ResponseWriter, r *http.Request) {
// 请求计数
monitor.WebRequestTotal.With(prometheus.Labels{"method": r.Method, "endpoint": r.URL.Path}).Inc()
_,_ = io.WriteString(w, "hello world!")
}
// query
func Query(w http.ResponseWriter, r *http.Request) {
//模拟业务查询耗时0~1s
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
_,_ = io.WriteString(w, "some results")
}
这里先访问
http://localhost:9090/query接口,并再次访问
http://localhost:9090/metrics
接口,返回的指标数据中就有了刚加上的metric,如下所示。
案例二:使用golang编写Prometheus Exporter
Exporter是基于Prometheus实施的监控系统中重要的组成部分,承担数据指标的采集工作,官方的exporter列表中已经包含了常见的绝大多数的系统指标监控,比如用于机器性能监控的node_exporter, 用于网络设备监控的snmp_exporter等等。这些已有的exporter对于监控来说,仅仅需要很少的配置工作就能提供完善的数据指标采集。
有时我们需要自己去写一些与业务逻辑比较相关的指标监控,这些指标无法通过常见的exporter获取到。比如我们需要提供对于DNS解析情况的整体监控,了解如何编写exporter对于业务监控很重要,也是完善监控系统需要经历的一个阶段。接下来我们就介绍如何编写exporter, 本篇内容编写的语言为golang, 官方也提供了python, java等其他的语言实现的库,采集方式其实大同小异。
指标类别
Prometheus中主要使用的四类指标类型,如下所示
- Counter (累加指标)
- Gauge (测量指标)
- Summary (概略图)
- Histogram (直方图)
Counter 一个累加指标数据,这个值随着时间只会逐渐的增加,比如程序完成的总任务数量,运行错误发生的总次数。常见的还有交换机中snmp采集的数据流量也属于该类型,代表了持续增加的数据包或者传输字节累加值。
Gauge代表了采集的一个单一数据,这个数据可以增加也可以减少,比如CPU使用情况,内存使用量,硬盘当前的空间容量等等
自定义指标
这里我们需要引入另一个依赖库
go get github.com/prometheus/client_golang/prometheus
下面先来定义了两个指标数据,一个是Guage类型, 一个是Counter类型。分别代表了CPU温度和磁盘失败次数统计,使用上面的定义进行分类。
下面先来定义了两个指标数据,一个是Guage类型, 一个是Counter类型。分别代表了CPU温度和磁盘失败次数统计,使用上面的定义进行分类。
cpuTemp = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "cpu_temperature_celsius",
Help: "Current temperature of the CPU.",
})
hdFailures = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "hd_errors_total",
Help: "Number of hard-disk errors.",
},
[]string{"device"},
)
这里还可以注册其他的参数,比如上面的磁盘失败次数统计上,我们可以同时传递一个device设备名称进去,这样我们采集的时候就可以获得多个不同的指标。每个指标对应了一个设备的磁盘失败次数统计。
注册指标
func init() {
// Metrics have to be registered to be exposed:
prometheus.MustRegister(cpuTemp)
prometheus.MustRegister(hdFailures)
}
使用prometheus.MustRegister是将数据直接注册到Default Registry,就像上面的运行的例子一样,这个Default Registry不需要额外的任何代码就可以将指标传递出去。注册后既可以在程序层面上去使用该指标了,这里我们使用之前定义的指标提供的API(Set和With().Inc)去改变指标的数据内容
func main() {
cpuTemp.Set(65.3)
hdFailures.With(prometheus.Labels{"device":"/dev/sda"}).Inc()
// The Handler function provides a default handler to expose metrics
// via an HTTP server. "/metrics" is the usual endpoint for that.
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":8080", nil))
}
其中With函数是传递到之前定义的label=”device”上的值,也就是生成指标类似于
cpu_temperature_celsius 65.3
hd_errors_total{"device"="/dev/sda"} 1
当然我们写在main函数中的方式是有问题的,这样这个指标仅仅改变了一次,不会随着我们下次采集数据的时候发生任何变化,我们希望的是每次执行采集的时候,程序都去自动的抓取指标并将数据通过http的方式传递给我们。
Counter数据采集实例
下面是一个采集Counter类型数据的实例,这个例子中实现了一个自定义的,满足采集器(Collector)接口的结构体,并手动注册该结构体后,使其每次查询的时候自动执行采集任务。
我们先来看下采集器Collector接口的实现
type Collector interface {
// 用于传递所有可能的指标的定义描述符
// 可以在程序运行期间添加新的描述,收集新的指标信息
// 重复的描述符将被忽略。两个不同的Collector不要设置相同的描述符
Describe(chan<- *Desc)
// Prometheus的注册器调用Collect执行实际的抓取参数的工作,
// 并将收集的数据传递到Channel中返回
// 收集的指标信息来自于Describe中传递,可以并发的执行抓取工作,但是必须要保证线程的安全。
Collect(chan<- Metric)
}
了解了接口的实现后,我们就可以写自己的实现了,先定义结构体,这是一个集群的指标采集器,每个集群都有自己的Zone,代表集群的名称。另外两个是保存的采集的指标。
type ClusterManager struct {
Zone string
OOMCountDesc *prometheus.Desc
RAMUsageDesc *prometheus.Desc
}
我们来实现一个采集工作,放到了ReallyExpensiveAssessmentOfTheSystemState函数中实现,每次执行的时候,返回一个按照主机名作为键采集到的数据,两个返回值分别代表了OOM错误计数,和RAM使用指标信息。
func (c *ClusterManager) ReallyExpensiveAssessmentOfTheSystemState() (
oomCountByHost map[string]int, ramUsageByHost map[string]float64,
) {
oomCountByHost = map[string]int{
"foo.example.org": int(rand.Int31n(1000)),
"bar.example.org": int(rand.Int31n(1000)),
}
ramUsageByHost = map[string]float64{
"foo.example.org": rand.Float64() * 100,
"bar.example.org": rand.Float64() * 100,
}
return
}
实现Describe接口,传递指标描述符到channel
// Describe simply sends the two Descs in the struct to the channel.
func (c *ClusterManager) Describe(ch chan<- *prometheus.Desc) {
ch <- c.OOMCountDesc
ch <- c.RAMUsageDesc
}
Collect函数将执行抓取函数并返回数据,返回的数据传递到channel中,并且传递的同时绑定原先的指标描述符。以及指标的类型(一个Counter和一个Guage)
func (c *ClusterManager) Collect(ch chan<- prometheus.Metric) {
oomCountByHost, ramUsageByHost := c.ReallyExpensiveAssessmentOfTheSystemState()
for host, oomCount := range oomCountByHost {
ch <- prometheus.MustNewConstMetric(
c.OOMCountDesc,
prometheus.CounterValue,
float64(oomCount),
host,
)
}
for host, ramUsage := range ramUsageByHost {
ch <- prometheus.MustNewConstMetric(
c.RAMUsageDesc,
prometheus.GaugeValue,
ramUsage,
host,
)
}
}
创建结构体及对应的指标信息,NewDesc参数第一个为指标的名称,第二个为帮助信息,显示在指标的上面作为注释,第三个是定义的label名称数组,第四个是定义的Labels
func NewClusterManager(zone string) *ClusterManager {
return &ClusterManager{
Zone: zone,
OOMCountDesc: prometheus.NewDesc(
"clustermanager_oom_crashes_total",
"Number of OOM crashes.",
[]string{"host"},
prometheus.Labels{"zone": zone},
),
RAMUsageDesc: prometheus.NewDesc(
"clustermanager_ram_usage_bytes",
"RAM usage as reported to the cluster manager.",
[]string{"host"},
prometheus.Labels{"zone": zone},
),
}
}
执行主程序
func main() {
workerDB := NewClusterManager("db")
workerCA := NewClusterManager("ca")
// Since we are dealing with custom Collector implementations, it might
// be a good idea to try it out with a pedantic registry.
reg := prometheus.NewPedanticRegistry()
reg.MustRegister(workerDB)
reg.MustRegister(workerCA)
}
如果直接执行上面的参数的话,不会获取任何的参数,因为程序将自动推出,我们并未定义http接口去暴露数据出来,因此数据在执行的时候还需要定义一个httphandler来处理http请求。
添加下面的代码到main函数后面,即可实现数据传递到http接口上:
gatherers := prometheus.Gatherers{
prometheus.DefaultGatherer,
reg,
}
h := promhttp.HandlerFor(gatherers,
promhttp.HandlerOpts{
ErrorLog: log.NewErrorLogger(),
ErrorHandling: promhttp.ContinueOnError,
})
http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r)
})
log.Infoln("Start server at :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Errorf("Error occur when start server %v", err)
os.Exit(1)
}
其中prometheus.Gatherers用来定义一个采集数据的收集器集合,可以merge多个不同的采集数据到一个结果集合,这里我们传递了缺省的DefaultGatherer,所以他在输出中也会包含go运行时指标信息。同时包含reg是我们之前生成的一个注册对象,用来自定义采集数据。
promhttp.HandlerFor()函数传递之前的Gatherers对象,并返回一个httpHandler对象,这个httpHandler对象可以调用其自身的ServHTTP函数来接手http请求,并返回响应。其中promhttp.HandlerOpts定义了采集过程中如果发生错误时,继续采集其他的数据。
代码粘贴出来。
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
"os"
"log"
"math/rand"
)
/*
type Collector interface {
Describe(chan <- *Desc)
Collect(chan <- Metric)
}
*/
type ClusterManager struct {
Zone string
OOMCountDesc *prometheus.Desc
RAMUsageDesc *prometheus.Desc
}
func NewClusterManager(zone string) *ClusterManager {
return &ClusterManager{
Zone: zone,
OOMCountDesc: prometheus.NewDesc(
"clustermanager_oom_crashes_total",
"Number of OOM crashes.",
[]string{"host"},
prometheus.Labels{"zone": zone},
),
RAMUsageDesc: prometheus.NewDesc(
"clustermanager_ram_usage_bytes",
"RAM usage as reported to the cluster manager.",
[]string{"host"},
prometheus.Labels{"zone": zone},
),
}
}
/* 我们来实现一个采集工作,放到了ReallyExpensiveAssessmentOfTheSystemState函数中实现,每次执行的时候,
返回一个按照主机名作为键采集到的数据,两个返回值分别代表了OOM错误计数,和RAM使用指标信息。*/
func (c *ClusterManager) ReallyExpensiveAssessmentOfTheSystemState() (
oomCountByHost map[string]int, ramUsageByHost map[string]float64,
) {
oomCountByHost = map[string]int{
"foo.example.org": int(rand.Int31n(1000)),
"bar.example.org": int(rand.Int31n(1000)),
}
ramUsageByHost = map[string]float64{
"foo.example.org": rand.Float64() * 100,
"bar.example.org": rand.Float64() * 100,
}
return
}
每次刷新的时候,我们都会获得不同的数据,类似于实现了一个数值不断改变的采集器。当然,具体的指标和采集函数还需要按照需求进行修改,满足实际的业务需求。
更多推荐
所有评论(0)