5-Prometheus Exporter 原理与实践

熊 0 阅读 监控平台

随着Prometheus的流行 , 很多系统都已经自带了用于Prometheus监控的接口 , 例如etcd、Kubernetes、coreDNS等 , 所以这些系统可以直接被Prometheus所监控。但是 , 有很多应用目前还没有提供用于Prometheus监控的接口 , 针对这这类应用 , Prometheus提出了Exporter的解决方案。

1. Exporter 简介

Exporter 是一个采集被监控系统的监控数据 , 通过Prometheus监控规范对外提供数据的组件。简单来说 , 为了采集被监控系统的监控样本数据 , 需要安装一个程序 , 该程序对外暴露了一个用于获取当前监控样本数据的HTTP访问地址 , 这样的一个程序称为Exporter。Prometheus通过轮询的方式定时从Target中获取监控数据样本 , 并且存储在数据库当中。

我们可以通过一个 metrics 接口为 Prometheus 提供监控指标 , 最好的方式就是直接在目标应用中集成该接口 ( 有些应用集成了metrics 接口) , 但是有的应用并没有内置支持 metrics 接口 , 比如 linux 系统、mysql、redis、kafka 等应用 , 这种情况下我们就可以单独开发一个应用来专门提供 metrics 服务 , 这就是我们这里说的 Exporter , 广义上讲所有可以向 Prometheus 提供监控样本数据的程序都可以被称为一个 Exporter , Exporter 的一个实例就是我们要监控的 target。

kubernetes-prometheus-exporter-introduce-1

Prometheus 社区提供了丰富的 Exporter 实现 , 涵盖了从基础设施、中间件以及网络等各个方面的监控实现 , 当然社区也出现了很多其他的 Exporter , 如果有必要 , 我们也可以完全根据自己的需求开发一个 Exporter , 但是最好以官方的 Exporter 开发的最佳实践文档作为参考实现方式。

由于 Exporter 是用于提供监控指标的独立服务 , 所以我们需要单独部署该服务来提供指标服务 , 比如 Node Exporter 就需要在操作系统上独立运行来收集系统的相关监控数据转换为 Prometheus 能够识别的 metrics 接口。

2. Exporter 运行方式

  • 独立运行

    由于操作系统本身并不直接支持Prometheus , 因此 , 只能通过一个独立运行的程序 , 从操作系统提供的相关接口将系统的状态参数转换为可供Prometheus读取的监控指标。

    除了操作系统外 , 如Mysql、kafka、Redis等介质 , 都是通过这种方式实现的。这类Exporter承担了一个中间代理的角色。

  • 应用集成

    由于Prometheus项目的火热 , 目前有部分开源产品直接在代码层面使用Prometheus的Client Library , 提供了在监控上的直接支持 , 如kubernetes、ETCD等产品。

    这类产品自身提供对应的metrics接口 , Prometheus可通过接口直接获取相关的系统指标数据。这种方式打破了监控的界限 , 应用程序本身做为一个Exporter提供功能。

3. 常用的Exporter

kubernetes-prometheus-exporter-introduce-2

这些Exporter主要通过被监控对象提供的监控相关的接口获取监控数据 , 通过例如以下几种方式获取到被监控对象的监控指标:

  • HTTP/HTTPs , 例如Rabbitmq Exporter通过HTTPs接口获取监控数据。

  • TCP , 例如Redis Exporter通过Redis提供的系统监控相关命令获取监控指标 , MySQL Server Exporter通过MySQL开放的监控相关的表获取监控指标。

  • 本地文件 , 例如Node Exporter通过读取整个proc文件系统下的文件 , 得到整个系统的当前状态。

  • 标准协议 , 例如IPMI Exporter通过IPMI协议获取硬件相关信息 , 并将这些信息的格式进行转化 , 输出为Prometheus能够识别的监控数据格式 , 从而扩大Prometheus的数据采集能力。

所有的Exporter程序都需要按照Prometheus的规范返回监控的样本数据。

4. 自定义export

尽管Promethesu社区提供了丰富多样的Exporter给用户使用 , 但业务特点 , 有时候可能无法在现有资源中找到合适的工具。对此 , 可以利用Prometheus的Clinet Libraries , 开发符合实际需要的自定义Exporter。

Clinet Libraries支持的语言版本非常丰富 , 除了官方提供了Go、Java or Scala、Python和Ruby几种外 , 还有很多第三方开发的其他语言版本。

本文以go为例 , 演示Exporter的开发。

kubernetes-prometheus-exporter-introduce-3

4.1 Gauge指标类型

  • 不带label的基本案例

    package main
     
    import (
        "net/http"
     
        "github.com/prometheus/client_golang/prometheus"
        "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
         // 定义指标
         cpuUsage := prometheus.NewGauge(prometheus.GaugeOpts{
          Name: "cpu_usage",                      // 指标名称
          Help: "this is test metrics cpu usage", // 帮助信息
         })
         // 给指标设置值
         cpuUsage.Set(89.56)
         // 注册指标
         prometheus.MustRegister(cpuUsage)
         // 暴露指标
         http.Handle("/metrics", promhttp.Handler())
         http.ListenAndServe(":9900", nil)
    }

  • 带有固定label指标的案例

    package main
     
    import (
        "net/http"
     
        "github.com/prometheus/client_golang/prometheus"
        "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
         // 定义指标
         cpuUsage := prometheus.NewGauge(prometheus.GaugeOpts{
          Name:        "cpu_usage",                      // 指标名称
          Help:        "this is test metrics cpu usage", // 帮助信息
          ConstLabels: prometheus.Labels{"MachineType": "host"}, // label
         })
         // 给指标设置值
         cpuUsage.Set(89.56)
         // 注册指标
         prometheus.MustRegister(cpuUsage)
         // 暴露指标
         http.Handle("/metrics", promhttp.Handler())
         http.ListenAndServe(":9900", nil)
    }

  • 带有非固定label指标的案例

    package main
     
    import (
        "net/http"
     
        "github.com/prometheus/client_golang/prometheus"
        "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
        //定义带有不固定label的指标
        mtu := prometheus.NewGaugeVec(prometheus.GaugeOpts{
             Name: "interface_mtu",
             Help: "网卡接口MTU",
        }, []string{"interface", "Machinetype"})
     
         // 给指标设置值
         mtu.WithLabelValues("lo", "host").Set(1500)
         mtu.WithLabelValues("ens32", "host").Set(1500)
         mtu.WithLabelValues("eth0", "host").Set(1500)
     
         // 注册指标
         prometheus.MustRegister(mtu)
     
         // 暴露指标
         http.Handle("/metrics", promhttp.Handler())
         http.ListenAndServe(":9900", nil)
    }

4.2 Counter指标类型

  • 不带label的基本案例

    package main
     
    import (
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     // 定义指标
     reqTotal := prometheus.NewCounter(prometheus.CounterOpts{
      Name: "current_request_total",
      Help: "当前请求总数",
     })
     // 注册指标
     prometheus.MustRegister(reqTotal)
     
     // 设置值
     reqTotal.Add(10)
     
     // 暴露指标
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

  • 带有固定label指标的案例

    package main
     
    import (
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     // 定义指标
     suceReqTotal := prometheus.NewCounter(prometheus.CounterOpts{
      Name:        "sucess_request_total",
      Help:        "请求成功的总数",
      ConstLabels: prometheus.Labels{"StatusCode": "200"},
     })
     // 注册指标
     prometheus.MustRegister(suceReqTotal)
     
     // 设置值
     suceReqTotal.Add(5675)
     
     // 暴露指标
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

  • 带有非固定label指标的案例

    package main
     
    import (
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     // 定义指标
     pathReqTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
      Name: "path_request_total",
      Help: "path请求总数",
     }, []string{"path"})
     // 注册指标
     prometheus.MustRegister(pathReqTotal)
     
     // 设置值
     pathReqTotal.WithLabelValues("/token").Add(37)
     pathReqTotal.WithLabelValues("/auth").Add(23)
     pathReqTotal.WithLabelValues("/user").Add(90)
     pathReqTotal.WithLabelValues("/api").Add(67)
     
     // 暴露指标
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

4.3 Counter指标类型

  • 不带label的案例

    package main
     
    import (
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     // 定义指标
     reqDelay := prometheus.NewHistogram(prometheus.HistogramOpts{
      Name:    "request_delay",
      Help:    "请求延迟 , 单位秒",
      Buckets: prometheus.LinearBuckets(0, 3, 6), // 调用LinearBuckets生成区间 , 从0开始 , 宽度3 , 共6个Bucket
     })
     
     // 注册指标
     prometheus.MustRegister(reqDelay)
     
     // 设置值
     reqDelay.Observe(6)
     
     // 暴露指标
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

  • 带固定label的案例

    package main
     
    import (
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     // 定义指标
     reqDelay := prometheus.NewHistogram(prometheus.HistogramOpts{
      Name:        "request_delay",
      Help:        "请求延迟 , 单位秒",
      Buckets:     prometheus.LinearBuckets(0, 3, 6), // 调用LinearBuckets生成区间 , 从0开始 , 宽度3 , 共6个Bucket
      ConstLabels: prometheus.Labels{"path": "/api"},
     })
     
     // 注册指标
     prometheus.MustRegister(reqDelay)
     
     // 设置值
     reqDelay.Observe(6)
     
     // 暴露指标
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

  • 带有非固定label的案例

    package main
     
    import (
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     // 定义指标
     reqDelay := prometheus.NewHistogramVec(prometheus.HistogramOpts{
      Name:    "request_delay",
      Help:    "请求延迟 , 单位秒",
      Buckets: prometheus.LinearBuckets(0, 3, 6), // 调用LinearBuckets生成区间 , 从0开始 , 宽度3 , 共6个Bucket
     }, []string{"path"})
     
     // 注册指标
     prometheus.MustRegister(reqDelay)
     
     // 设置值
     reqDelay.WithLabelValues("/api").Observe(6)
     reqDelay.WithLabelValues("/user").Observe(3)
     reqDelay.WithLabelValues("/delete").Observe(2)
     reqDelay.WithLabelValues("/get_token").Observe(13)
     
     // 暴露指标
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

4.4 Summary指标类型

  • 不带label的案例

    package main
     
    import (
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     // 定义指标
     reqDelay := prometheus.NewSummary(prometheus.SummaryOpts{
      Name:       "request_delay",
      Help:       "请求延迟",
      Objectives: map[float64]float64{0.5: 0.05, 0.90: 0.01, 0.99: 0.001}, // 百分比:精度
     })
     
     // 注册指标
     prometheus.MustRegister(reqDelay)
     
     // 设置值
     reqDelay.Observe(4)
     
     // 暴露指标
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

  • 带有固定label的案例

    package main
     
    import (
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     // 定义指标
     reqDelay := prometheus.NewSummary(prometheus.SummaryOpts{
      Name:        "request_delay",
      Help:        "请求延迟",
      Objectives:  map[float64]float64{0.5: 0.05, 0.90: 0.01, 0.99: 0.001}, // 百分比:精度
      ConstLabels: prometheus.Labels{"path": "/api"},
     })
     
     // 注册指标
     prometheus.MustRegister(reqDelay)
     
     // 设置值
     reqDelay.Observe(4)
     
     // 暴露指标
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }
  • 带有非固定label的案例

    package main
     
    import (
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     // 定义指标
     reqDelay := prometheus.NewSummaryVec(prometheus.SummaryOpts{
      Name:       "request_delay",
      Help:       "请求延迟",
      Objectives: map[float64]float64{0.5: 0.05, 0.90: 0.01, 0.99: 0.001}, // 百分比:精度
     }, []string{"path"})
     
     // 注册指标
     prometheus.MustRegister(reqDelay)
     
     // 设置值
     reqDelay.WithLabelValues("/api").Observe(4)
     reqDelay.WithLabelValues("/token").Observe(2)
     reqDelay.WithLabelValues("/user").Observe(3)
     
     // 暴露指标
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

4.5. 值的修改

  • 基于事件的触发来修改值 , 比如每访问1次/api就增1

    基于事件的触发对指标的值进行修改 , 通常大多数是来自业务方面的指标需求 , 如自研的应用需要暴露相关指标给promethus进行监控、展示 , 那么指标采集的代码 ( 指标定义、设置值) 就要嵌入到自研应用代码里了。

    package main
     
    import (
     "fmt"
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
    )
     
    func main() {
     urlRequestTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
      Name: "urlRequestTotal",
      Help: "PATH请求累计 单位 次",
     }, []string{"path"})
     
     http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
      urlRequestTotal.WithLabelValues(r.URL.Path).Inc() // 使用Inc函数进行增1
      fmt.Fprintf(w, "Welcome to the api")
     })
     
     prometheus.MustRegister(urlRequestTotal)
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }
  • 基于时间周期的触发来修改值 , 比如下面的示例中 , 是每间隔1秒获取cpu负载指标 。

    基于时间周期的触发 , 可以是多少秒、分、时、日、月、周。

    package main
     
    import (
     "net/http"
     "time"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
     "github.com/shirou/gopsutil/load"
    )
     
    func main() {
     
     cpuUsage := prometheus.NewGaugeVec(prometheus.GaugeOpts{
      Name: "CpuLoad",
      Help: "CPU负载",
     }, []string{"time"})
     
     // 开启一个子协程执行该匿名函数内的逻辑来给指标设置值 , 且每秒获取一次
     go func() {
      for range time.Tick(time.Second) {
       info, _ := load.Avg()
       cpuUsage.WithLabelValues("Load1").Set(float64(info.Load1))
       cpuUsage.WithLabelValues("Load5").Set(float64(info.Load5))
       cpuUsage.WithLabelValues("Load15").Set(float64(info.Load15))
      }
     }()
     
     prometheus.MustRegister(cpuUsage)
     
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

  • 基于每访问一次获取指标的URI才修改值 , 比如每次访问/metrics才去修改某些指标的值

    案例是每访问一次/metrics就获取一次内存总容量。

    package main
     
    import (
     "fmt"
     "net/http"
     
     "github.com/prometheus/client_golang/prometheus"
     "github.com/prometheus/client_golang/prometheus/promhttp"
     "github.com/shirou/gopsutil/mem"
    )
     
    func main() {
     MemTotal := prometheus.NewGaugeFunc(prometheus.GaugeOpts{
      Name: "MemTotal",
      Help: "内存总容量 单位 GB",
     }, func() float64 {
      fmt.Println("call MemTotal ...")
      info, _ := mem.VirtualMemory()
      return float64(info.Total / 1024 / 1024 / 1024)
     })
     prometheus.MustRegister(MemTotal)
     http.Handle("/metrics", promhttp.Handler())
     http.ListenAndServe(":9900", nil)
    }

目前只有Gauge和Counter的指标类型有对应的函数 , 分别是NewGaugeFunc和NewCounterFunc , 而且是固定的label。

5. Exporter规范

所有的Exporter程序都需要按照Prometheus的规范 , 返回监控的样本数据。以Node Exporter为例 , 当访问/metrics地址时会返回以下内容:

# HELP node_cpu Seconds the cpus spent in each mode.
# TYPE node_cpu counter
node_cpu{cpu="cpu0",mode="idle"} 362812.7890625
# HELP node_load1 1m load average.
# TYPE node_load1 gauge
node_load1 3.0703125

这是一种基于文本的格式规范 , 在Prometheus 2.0之前的版本还支持Protocol buffer规范。相比于Protocol buffer文本具有更好的可读性 , 以及跨平台性。Prometheus 2.0的版本也已经不再支持Protocol buffer , 这里就不对Protocol buffer规范做详细的阐述。

Exporter返回的样本数据 , 主要由三个部分组成:样本的一般注释信息 ( HELP) , 样本的类型注释信息 ( TYPE) 和样本。Prometheus会对Exporter响应的内容逐行解析:

如果当前行以# HELP开始 , Prometheus将会按照以下规则对内容进行解析 , 得到当前的指标名称以及相应的说明信息:

# HELP <metrics_name> <doc_string>

如果当前行以# TYPE开始 , Prometheus会按照以下规则对内容进行解析 , 得到当前的指标名称以及指标类型:

# TYPE <metrics_name> <metrics_type>

TYPE注释行必须出现在指标的第一个样本之前。如果没有明确的指标类型需要返回为untyped。 除了# 开头的所有行都会被视为是监控样本数据。 每一行样本需要满足以下格式规范:

metric_name [
  "{" label_name "=" `"` label_value `"` { "," label_name "=" `"` label_value `"` } [ "," ] "}"
] value [ timestamp ]

其中metric_name和label_name必须遵循PromQL的格式规范要求。value是一个float格式的数据 , timestamp的类型为int64 ( 从1970-01-01 00:00:00以来的毫秒数) , timestamp为可选默认为当前时间。具有相同metric_name的样本必须按照一个组的形式排列 , 并且每一行必须是唯一的指标名称和标签键值对组合。

需要特别注意的是对于histogram和summary类型的样本。需要按照以下约定返回样本数据:

  • 类型为summary或者histogram的指标x , 该指标所有样本的值的总和需要使用一个单独的x_sum指标表示。

  • 类型为summary或者histogram的指标x , 该指标所有样本的总数需要使用一个单独的x_count指标表示。

  • 对于类型为summary的指标x , 其不同分位数quantile所代表的样本 , 需要使用单独的x{quantile=“y”}表示。

  • 对于类型histogram的指标x为了表示其样本的分布情况 , 每一个分布需要使用x_bucket{le=“y”}表示 , 其中y为当前分布的上位数。同时必须包含一个样本x_bucket{le="+Inf"} , 并且其样本值必须和x_count相同。

  • 对于histogram和summary的样本 , 必须按照分位数quantile和分布le的值的递增顺序排序。

以下是类型为histogram和summary的样本输出示例:

# A histogram, which has a pretty complex representation in the text format:
# HELP http_request_duration_seconds A histogram of the request duration.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 24054
http_request_duration_seconds_bucket{le="0.1"} 33444
http_request_duration_seconds_bucket{le="0.2"} 100392
http_request_duration_seconds_bucket{le="+Inf"} 144320
http_request_duration_seconds_sum 53423
http_request_duration_seconds_count 144320
 
# Finally a summary, which has a complex representation, too:
# HELP rpc_duration_seconds A summary of the RPC duration in seconds.
# TYPE rpc_duration_seconds summary
rpc_duration_seconds{quantile="0.01"} 3102
rpc_duration_seconds{quantile="0.05"} 3272
rpc_duration_seconds{quantile="0.5"} 4773
rpc_duration_seconds_sum 1.7560473e+07
rpc_duration_seconds_count 2693

对于某些Prometheus还没有提供支持的编程语言 , 用户只需要按照以上规范返回响应的文本数据即可。

指定样本格式的版本

在Exporter响应的HTTP头信息中 , 可以通过Content-Type指定特定的规范版本 , 例如:

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 2906
Content-Type: text/plain; version=0.0.4
Date: Sat, 17 Mar 2018 08:47:06 GMT

其中version用于指定Text-based的格式版本 , 当没有指定版本的时候 , 默认使用最新格式规范的版本。同时HTTP响应头还需要指定压缩格式为gzip。


参考链接

构建企业级监控平台系列 ( 十五) :Prometheus Exporter 原理与实践 (qq.com)