头图

prometheus-go-sdk不活跃指标清理问题

ning1875
English

k8s教程说明

prometheus全组件的教程

go语言课程

问题描述

  • 比如对于1个构建的流水线指标 pipeline_step_duration ,会设置1个标签是step
  • 每次流水线包含的step可能不相同

    # 比如 流水线a 第1次的step 包含clone 和build
    pipeline_step_duration{step="clone"}
    pipeline_step_duration{step="build"}
    # 第2次 的step 包含 build 和push
    pipeline_step_duration{step="build"}
    pipeline_step_duration{step="push"}
  • 那么问题来了:第2次的pipeline_step_duration{step="build"} 要不要删掉?
  • 其实在这个场景里面是要删掉的,因为已经不包含clone了

问题可以总结成:之前采集的标签已经不存在了,数据要及时清理掉 --问题是如何清理?

讨论这个问题前做个实验:对比两种常见的自打点方式对于不活跃指标的删除处理

实验手段:prometheus client-go sdk

  • 启动1个rand_metrics
  • 包含rand_key,每次key都不一样,测试请求metrics接口的结果

    var (
      T1 = prometheus.NewGaugeVec(prometheus.GaugeOpts{
          Name: "rand_metrics",
          Help: "rand_metrics",
      }, []string{"rand_key"})
    )

实现方式01 业务代码中直接实现打点:不实现Collector接口

  • 代码如下,模拟极端情况,每0.1秒生成随机key 和value设置metrics

    package main
    
    import (
      "fmt"
      "github.com/prometheus/client_golang/prometheus"
      "github.com/prometheus/client_golang/prometheus/promhttp"
      "math/rand"
      "net/http"
      "time"
    )
    
    var (
      T1 = prometheus.NewGaugeVec(prometheus.GaugeOpts{
          Name: "rand_metrics",
          Help: "rand_metrics",
      }, []string{"rand_key"})
    )
    
    func init() {
      prometheus.DefaultRegisterer.MustRegister(T1)
    }
    func RandStr(length int) string {
      str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
      bytes := []byte(str)
      result := []byte{}
      rand.Seed(time.Now().UnixNano() + int64(rand.Intn(100)))
      for i := 0; i < length; i++ {
          result = append(result, bytes[rand.Intn(len(bytes))])
      }
      return string(result)
    }
    
    func push() {
      for {
          randKey := RandStr(10)
          rand.Seed(time.Now().UnixNano() + int64(rand.Intn(100)))
          T1.With(prometheus.Labels{"rand_key": randKey}).Set(rand.Float64())
          time.Sleep(100 * time.Millisecond)
    
      }
    }
    
    func main() {
      go push()
      addr := ":8081"
      http.Handle("/metrics", promhttp.Handler())
      srv := http.Server{Addr: addr}
      err := srv.ListenAndServe()
      fmt.Println(err)
    }
    
  • 启动服务之后请求 :8081/metrics接口发现 过期的 rand_key还会保留,不会清理

    # HELP rand_metrics rand_metrics
    # TYPE rand_metrics gauge
    rand_metrics{rand_key="00DsYGkd6x"} 0.02229735291486387
    rand_metrics{rand_key="017UBn8S2T"} 0.7192676436571013
    rand_metrics{rand_key="01Ar4ca3i1"} 0.24131184816722678
    rand_metrics{rand_key="02Ay5kqsDH"} 0.11462075954697458
    rand_metrics{rand_key="02JZNZvMng"} 0.9874169937518104
    rand_metrics{rand_key="02arsU5qNT"} 0.8552103362564516
    rand_metrics{rand_key="02nMy3thfh"} 0.039571420204118024
    rand_metrics{rand_key="032cyHjRhP"} 0.14576779289125183
    rand_metrics{rand_key="03DPDckbfs"} 0.6106184905871918
    rand_metrics{rand_key="03lbtLwFUO"} 0.936911945555629
    rand_metrics{rand_key="03wqYiguP2"} 0.20167059771916385
    rand_metrics{rand_key="04uG2s3X0C"} 0.3324314184499403

实现方式02 实现Collector接口

  • 实现prometheus sdk中的collect 接口 :也就是给1个结构体 绑定Collect和Describe方法
  • 在Collect中 实现设置标签和赋值方法
  • 在Describe中 传入desc

    package main
    
    import (
      "fmt"
      "github.com/prometheus/client_golang/prometheus"
      "github.com/prometheus/client_golang/prometheus/promhttp"
      "log"
      "math/rand"
      "net/http"
      "time"
    )
    
    var (
      T1 = prometheus.NewDesc(
          "rand_metrics",
          "rand_metrics",
          []string{"rand_key"},
          nil)
    )
    
    type MyCollector struct {
      Name string
    }
    
    func (mc *MyCollector) Collect(ch chan<- prometheus.Metric) {
      log.Printf("MyCollector.collect.called")
      ch <- prometheus.MustNewConstMetric(T1,
          prometheus.GaugeValue, rand.Float64(), RandStr(10))
    }
    func (mc *MyCollector) Describe(ch chan<- *prometheus.Desc) {
      log.Printf("MyCollector.Describe.called")
      ch <- T1
    }
    
    func RandStr(length int) string {
      str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
      bytes := []byte(str)
      result := []byte{}
      rand.Seed(time.Now().UnixNano() + int64(rand.Intn(100)))
      for i := 0; i < length; i++ {
          result = append(result, bytes[rand.Intn(len(bytes))])
      }
      return string(result)
    }
    
    func main() {
      //go push()
      mc := &MyCollector{Name: "abc"}
      prometheus.MustRegister(mc)
      addr := ":8082"
      http.Handle("/metrics", promhttp.Handler())
      srv := http.Server{Addr: addr}
      err := srv.ListenAndServe()
      fmt.Println(err)
    }
    
  • metrics效果测试 :请求:8082/metrics接口发现 rand_metrics总是只有1个值

    # HELP rand_metrics rand_metrics
    # TYPE rand_metrics gauge
    rand_metrics{rand_key="e1JU185kE4"} 0.12268247569586412
  • 并且查看日志发现,每次我们请求/metrics接口时 MyCollector.collect.called会调用

    2022/06/21 11:46:40 MyCollector.Describe.called
    2022/06/21 11:46:44 MyCollector.collect.called
    2022/06/21 11:46:47 MyCollector.collect.called
    2022/06/21 11:46:47 MyCollector.collect.called
    2022/06/21 11:46:47 MyCollector.collect.called
    2022/06/21 11:46:47 MyCollector.collect.called
    

现象总结

  • 实现Collector接口的方式 能满足过期指标清理的需求,并且打点函数是伴随/metrics接口请求触发的
  • 不实现Collector接口的方式 不能满足过期指标清理的需求,指标会随着业务打点堆积

源码解读相关原因

01 两种方式都是从web请求获取的指标,所以得先从 /metrics接口看

  • 入口就是 http.Handle("/metrics", promhttp.Handler())
  • 追踪后发现是 D:\go_path\pkg\mod\github.com\prometheus\client_golang@v1.12.2\prometheus\promhttp\http.go
  • 主要逻辑为:

    • 调用reg的Gather方法 获取 MetricFamily数组
    • 然后编码,写到http的resp中
  • 伪代码如下

    func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
      mfs, err := reg.Gather()
      for _, mf := range mfs {
          if handleError(enc.Encode(mf)) {
          return
      }
    }
    }

reg.Gather :遍历reg中已注册的collector 调用他们的collect方法

  • 先调用他们的collect方法获取metrics结果

      collectWorker := func() {
          for {
              select {
              case collector := <-checkedCollectors:
                  collector.Collect(checkedMetricChan)
              case collector := <-uncheckedCollectors:
                  collector.Collect(uncheckedMetricChan)
              default:
                  return
              }
              wg.Done()
          }
      }
    
  • 然后消费chan中的数据,处理metrics

      cmc := checkedMetricChan
      umc := uncheckedMetricChan
    
      for {
          select {
          case metric, ok := <-cmc:
              if !ok {
                  cmc = nil
                  break
              }
              errs.Append(processMetric(
                  metric, metricFamiliesByName,
                  metricHashes,
                  registeredDescIDs,
              ))
          case metric, ok := <-umc:
              if !ok {
                  umc = nil
                  break
              }
              errs.Append(processMetric(
                  metric, metricFamiliesByName,
                  metricHashes,
                  nil,
              ))

processMetric处理方法一致,所以方式12的不同就在 collect方法

02 不实现Collector接口的方式的collect方法追踪

  • 因为我们往reg中注册的是 prometheus.NewGaugeVec生成的*GaugeVec指针
  • 所以执行的是*GaugeVec的collect方法
  • 而GaugeVec 又继承了MetricVec

    type GaugeVec struct {
      *MetricVec
    }
  • 而MetricVec中有个metricMap对象, 所以最终是metricMap的collect方法

    type MetricVec struct {
      *metricMap
    
      curry []curriedLabelValue
    
      // hashAdd and hashAddByte can be replaced for testing collision handling.
      hashAdd     func(h uint64, s string) uint64
      hashAddByte func(h uint64, b byte) uint64
    }

    观察metricMap结构体和方法

  • metricMap有个metrics的map
  • 而它的Collect方法就是遍历这个map内层的所有metricWithLabelValues接口,塞入ch中处理

    // metricVecs.
    type metricMap struct {
      mtx       sync.RWMutex // Protects metrics.
      metrics   map[uint64][]metricWithLabelValues
      desc      *Desc
      newMetric func(labelValues ...string) Metric
    }
    
    // Describe implements Collector. It will send exactly one Desc to the provided
    // channel.
    func (m *metricMap) Describe(ch chan<- *Desc) {
      ch <- m.desc
    }
    
    // Collect implements Collector.
    func (m *metricMap) Collect(ch chan<- Metric) {
      m.mtx.RLock()
      defer m.mtx.RUnlock()
    
      for _, metrics := range m.metrics {
          for _, metric := range metrics {
              ch <- metric.metric
          }
      }
    }
    
  • 看到这里就很清晰了,只要metrics map中的元素不被显示的删除,那么数据就会一直存在
  • 有一些exporter是采用这种显式删除的流派的,比如event_expoter

03 实现Collector接口的方式的collect方法追踪

  • 因为我们的collector 实现了collect方法
  • 所以直接请求Gather会调用我们的collect方法 获取结果

    func (mc *MyCollector) Collect(ch chan<- prometheus.Metric) {
      log.Printf("MyCollector.collect.called")
      ch <- prometheus.MustNewConstMetric(T1,
          prometheus.GaugeValue, rand.Float64(), RandStr(10))
    }
  • 所以它不会往metricsMap中写入,所以只有1个值

总结

  • 两种打点方式的collect方法是不一样的
  • 其实主流的exporter的效果也是不活跃的指标会删掉:

    • 比如 process-exporter监控进程,进程不存在指标曲线就会消失:从grafana图上看就是断点:不然采集一次会一直存在
    • 比如 node-exporter 监控挂载点等,当挂载点消失相关曲线也会消失
  • 因为主流的exporter采用都是 实现collect方法的方式:
  • 还有k8s中kube-state-metrics采用的是 metrics-store作为informer的store 去watch etcd的delete 事件: pod删除的时候相关的曲线也会消失
  • 或者可以显示的调用delete 方法,将过期的series从map中删掉,不过需要hold中上一次的和这一次的diff
  • 总之两个流派:map显式删除VS实现collector接口
阅读 223

监控系统和运维开发
监控系统的源码解析,运维开发经验交流

运维开发工程师,腾讯课堂搜 燕小乙

158 声望
44 粉丝
0 条评论

运维开发工程师,腾讯课堂搜 燕小乙

158 声望
44 粉丝
文章目录
宣传栏