Prometheus指标数据同步机制

源码地址:https://github.com/prometheus/prometheus

整体代码框架大致如下:

  • 由scrape.Manager管理所有的抓取对象;

  • 所有的抓取对象按group分组,每个group是一个job_name;

  • 每个group下含多个scrapeTarget,即具体的抓取目标endpoint;

  • 对每个目标endpoint,启动一个抓取goroutine,按照interval间隔循环的抓取对象的指标;

同步配置

假如prometheus.yaml中的抓取配置为:

scrape_configs:
  - job_name: "monitor"
    static_configs:
    - targets: ['192.168.101.9:11504']


  - job_name: 'node-exporter'
    static_configs:
    - targets: ['10.21.1.74:9100', '192.168.101.9:9100']

那么,抓取对象将按如下结构分组:

{
    "monitor": [
        {
            "Targets": [
                {
                    "__address__": "192.168.101.9:11504"
                }
            ],
            "Labels": null,
            "Source": "0"
        }
    ],
    "node-exporter": [
        {
            "Targets": [
                {
                    "__address__": "10.21.1.74:9100"
                },
                {
                    "__address__": "192.168.101.9:9100"
                }
            ],
            "Labels": null,
            "Source": "0"
        }
    ]
}

入口方法

下面来看下数据处理的入口方法:

func (m *Manager) reload() {
   m.mtxScrape.Lock()
   var wg sync.WaitGroup
   for setName, groups := range m.targetSets {
      if _, ok := m.scrapePools[setName]; !ok {
         scrapeConfig, ok := m.scrapeConfigs[setName]
         if !ok {
            level.Error(m.logger).Log("msg", "error reloading target set", "err", "invalid config id:"+setName)
            continue
         }
         sp, err := newScrapePool(scrapeConfig, m.append, m.jitterSeed, log.With(m.logger, "scrape_pool", setName), m.opts)
         if err != nil {
            level.Error(m.logger).Log("msg", "error creating new scrape pool", "err", err, "scrape_pool", setName)
            continue
         }
         m.scrapePools[setName] = sp
      }

      wg.Add(1)
      // Run the sync in parallel as these take a while and at high load can't catch up.
      go func(sp *scrapePool, groups []*targetgroup.Group) {
         sp.Sync(groups)
         wg.Done()
      }(m.scrapePools[setName], groups)

   }
   m.mtxScrape.Unlock()
   wg.Wait()
}

上面逻辑大致如下,对每个targetGroup:

  • 创建scrapePool;

  • 对scrapePool进行Sync:同步信息进行抓取;

数据同步

func (sp *scrapePool) Sync(tgs []*targetgroup.Group) {
   sp.mtx.Lock()
   defer sp.mtx.Unlock()
   start := time.Now()

   sp.targetMtx.Lock()
   var all []*Target
   sp.droppedTargets = []*Target{}
   for _, tg := range tgs {
      targets, failures := TargetsFromGroup(tg, sp.config, sp.noDefaultPort)
      for _, err := range failures {
         level.Error(sp.logger).Log("msg", "Creating target failed", "err", err)
      }
      targetSyncFailed.WithLabelValues(sp.config.JobName).Add(float64(len(failures)))
      for _, t := range targets {
         if !t.Labels().IsEmpty() {
            all = append(all, t)
         } else if !t.DiscoveredLabels().IsEmpty() {
            sp.droppedTargets = append(sp.droppedTargets, t)
         }
      }
   }
   sp.targetMtx.Unlock()
   sp.sync(all)

   targetSyncIntervalLength.WithLabelValues(sp.config.JobName).Observe(
      time.Since(start).Seconds(),
   )
   targetScrapePoolSyncsCounter.WithLabelValues(sp.config.JobName).Inc()
}

上面的逻辑大致如下:

  • 对于每个抓取的 job (servicemonitor)而言,TargetsFromGroup 会对 targetgroup 信息里面的 label 做排序,消耗了很多的 CPU。函数最终的目的,是过滤得到本 service monitor 所关注的一些 targets 信息。

  • 进行同步

下面来看下具体的同步方法:

func (sp *scrapePool) sync(targets []*Target) {
   var (
      uniqueLoops = make(map[uint64]loop)
      interval = time.Duration(sp.config.ScrapeInterval)
      timeout = time.Duration(sp.config.ScrapeTimeout)
      bodySizeLimit = int64(sp.config.BodySizeLimit)
      sampleLimit = int(sp.config.SampleLimit)
      labelLimits = &labelLimits{
         labelLimit: int(sp.config.LabelLimit),
         labelNameLengthLimit: int(sp.config.LabelNameLengthLimit),
         labelValueLengthLimit: int(sp.config.LabelValueLengthLimit),
      }
      honorLabels = sp.config.HonorLabels
      honorTimestamps = sp.config.HonorTimestamps
      mrc = sp.config.MetricRelabelConfigs
   )

   sp.targetMtx.Lock()
   for _, t := range targets {
      hash := t.hash()

      if _, ok := sp.activeTargets[hash]; !ok {
         // The scrape interval and timeout labels are set to the config's values initially,
         // so whether changed via relabeling or not, they'll exist and hold the correct values
         // for every target.
         var err error
         interval, timeout, err = t.intervalAndTimeout(interval, timeout)
         acceptHeader := scrapeAcceptHeader
         if sp.enableProtobufNegotiation {
            acceptHeader = scrapeAcceptHeaderWithProtobuf
         }
         s := &targetScraper{Target: t, client: sp.client, timeout: timeout, bodySizeLimit: bodySizeLimit, acceptHeader: acceptHeader}
         l := sp.newLoop(scrapeLoopOptions{
            target: t,
            scraper: s,
            sampleLimit: sampleLimit,
            labelLimits: labelLimits,
            honorLabels: honorLabels,
            honorTimestamps: honorTimestamps,
            mrc: mrc,
            interval: interval,
            timeout: timeout,
         })
         if err != nil {
            l.setForcedError(err)
         }

         sp.activeTargets[hash] = t
         sp.loops[hash] = l

         uniqueLoops[hash] = l
      } else {
         // This might be a duplicated target.
         if _, ok := uniqueLoops[hash]; !ok {
            uniqueLoops[hash] = nil
         }
         // Need to keep the most updated labels information
         // for displaying it in the Service Discovery web page.
         sp.activeTargets[hash].SetDiscoveredLabels(t.DiscoveredLabels())
      }
   }

   var wg sync.WaitGroup

   // Stop and remove old targets and scraper loops.
   for hash := range sp.activeTargets {
      if _, ok := uniqueLoops[hash]; !ok {
         wg.Add(1)
         go func(l loop) {
            l.stop()
            wg.Done()
         }(sp.loops[hash])

         delete(sp.loops, hash)
         delete(sp.activeTargets, hash)
      }
   }

   sp.targetMtx.Unlock()

   targetScrapePoolTargetsAdded.WithLabelValues(sp.config.JobName).Set(float64(len(uniqueLoops)))
   forcedErr := sp.refreshTargetLimitErr()
   for _, l := range sp.loops {
      l.setForcedError(forcedErr)
   }
   for _, l := range uniqueLoops {
      if l != nil {
         go l.run(nil)
      }
   }
   // Wait for all potentially stopped scrapers to terminate.
   // This covers the case of flapping targets. If the server is under high load, a new scraper
   // may be active and tries to insert. The old scraper that didn't terminate yet could still
   // be inserting a previous sample set.
   wg.Wait()
}

上面代码逻辑大致如下:

  • scrapePool为targetGroup下的每个targets,创建1个scrapeLoop:对每个target,使用newLoop()创建targetLoop

  • 然后对每个targetLoop启动1个goroutine,让targetLoop.run()循环拉取:

下面来看下loop方法:

func (sl *scrapeLoop) run(errc chan<- error) {
   select {
   case <-time.After(sl.scraper.offset(sl.interval, sl.jitterSeed)):
      // Continue after a scraping offset.
   case <-sl.ctx.Done():
      close(sl.stopped)
      return
   }

   var last time.Time

   alignedScrapeTime := time.Now().Round(0)
   ticker := time.NewTicker(sl.interval)
   defer ticker.Stop()

mainLoop:
   for {
      select {
      case <-sl.parentCtx.Done():
         close(sl.stopped)
         return
      case <-sl.ctx.Done():
         break mainLoop
      default:
      }

      // Temporary workaround for a jitter in go timers that causes disk space
      // increase in TSDB.
      // See https://github.com/prometheus/prometheus/issues/7846
      // Calling Round ensures the time used is the wall clock, as otherwise .Sub
      // and .Add on time.Time behave differently (see time package docs).
      scrapeTime := time.Now().Round(0)
      if AlignScrapeTimestamps &amp;&amp; sl.interval > 100*ScrapeTimestampTolerance {
         // For some reason, a tick might have been skipped, in which case we
         // would call alignedScrapeTime.Add(interval) multiple times.
         for scrapeTime.Sub(alignedScrapeTime) >= sl.interval {
            alignedScrapeTime = alignedScrapeTime.Add(sl.interval)
         }
         // Align the scrape time if we are in the tolerance boundaries.
         if scrapeTime.Sub(alignedScrapeTime) <= ScrapeTimestampTolerance {
            scrapeTime = alignedScrapeTime
         }
      }

      last = sl.scrapeAndReport(last, scrapeTime, errc)

      select {
      case <-sl.parentCtx.Done():
         close(sl.stopped)
         return
      case <-sl.ctx.Done():
         break mainLoop
      case <-ticker.C:
      }
   }

   close(sl.stopped)

   if !sl.disabledEndOfRunStalenessMarkers {
      sl.endOfRunStaleness(last, ticker, sl.interval)
   }
}

func (sl *scrapeLoop) scrapeAndReport(last, appendTime time.Time, errc chan<- error) time.Time {
  start := time.Now()

  // Only record after the first scrape.
  if !last.IsZero() {
    targetIntervalLength.WithLabelValues(sl.interval.String()).Observe(
      time.Since(last).Seconds(),
    )
  }

  b := sl.buffers.Get(sl.lastScrapeSize).([]byte)
  defer sl.buffers.Put(b)
  buf := bytes.NewBuffer(b)

  var total, added, seriesAdded, bytes int
  var err, appErr, scrapeErr error

  app := sl.appender(sl.appenderCtx)
  defer func() {
    if err != nil {
      app.Rollback()
      return
    }
    err = app.Commit()
    if err != nil {
      level.Error(sl.l).Log("msg", "Scrape commit failed", "err", err)
    }
  }()

  defer func() {
    if err = sl.report(app, appendTime, time.Since(start), total, added, seriesAdded, bytes, scrapeErr); err != nil {
      level.Warn(sl.l).Log("msg", "Appending scrape report failed", "err", err)
    }
  }()

  if forcedErr := sl.getForcedError(); forcedErr != nil {
    scrapeErr = forcedErr
    // Add stale markers.
    if _, _, _, err := sl.append(app, []byte{}, "", appendTime); err != nil {
      app.Rollback()
      app = sl.appender(sl.appenderCtx)
      level.Warn(sl.l).Log("msg", "Append failed", "err", err)
    }
    if errc != nil {
      errc <- forcedErr
    }

    return start
  }

  var contentType string
  scrapeCtx, cancel := context.WithTimeout(sl.parentCtx, sl.timeout)
  contentType, scrapeErr = sl.scraper.scrape(scrapeCtx, buf)
  cancel()

  if scrapeErr == nil {
    b = buf.Bytes()
    // NOTE: There were issues with misbehaving clients in the past
    // that occasionally returned empty results. We don't want those
    // to falsely reset our buffer size.
    if len(b) > 0 {
      sl.lastScrapeSize = len(b)
    }
    bytes = len(b)
  } else {
    level.Debug(sl.l).Log("msg", "Scrape failed", "err", scrapeErr)
    if errc != nil {
      errc <- scrapeErr
    }
    if errors.Is(scrapeErr, errBodySizeLimit) {
      bytes = -1
    }
  }

  // A failed scrape is the same as an empty scrape,
  // we still call sl.append to trigger stale markers.
  total, added, seriesAdded, appErr = sl.append(app, b, contentType, appendTime)
  if appErr != nil {
    app.Rollback()
    app = sl.appender(sl.appenderCtx)
    level.Debug(sl.l).Log("msg", "Append failed", "err", appErr)
    // The append failed, probably due to a parse error or sample limit.
    // Call sl.append again with an empty scrape to trigger stale markers.
    if _, _, _, err := sl.append(app, []byte{}, "", appendTime); err != nil {
      app.Rollback()
      app = sl.appender(sl.appenderCtx)
      level.Warn(sl.l).Log("msg", "Append failed", "err", err)
    }
  }

  if scrapeErr == nil {
    scrapeErr = appErr
  }

  return start
}

上面逻辑大致如下,每个scrapeLoop按抓取周期循环执行

  • scrape抓取指标数据;

  • append写入底层存储;

  • 最后更新scrapeLoop的状态(主要是指标值);

HTTP抓取的逻辑:

  • 首先,向target的url发送HTTP Get;

  • 然后,将写入io.writer(即上文中的buffers)中,待后面解析出指标:

posted on 2023-05-24 19:41  萌兰三太子  阅读(296)  评论(0)    收藏  举报  来源

导航