抓取循环 · 心跳所在

scrapeLoop 是 Prometheus 的心脏。这一章读 scrape/ 源码:用哈希把目标打散避免惊群、自动附加的 8 个元指标(排查采集问题的第一现场)、sample_limit 的自保、以及那个精妙的 staleness 标记。

作者 David更新于 第 12 篇(共 52 篇)

抓取循环 · 心跳所在

目标发现了、标签改好了,现在真正去抓。scrapeLoop 是 Prometheus 的心脏——每个目标一个,按周期跳动。这一章读它,并给出排查采集问题的实战套路,收尾第 1 部分。

结构:池 + 循环

源码里两个核心类型:

  • scrapePool(scrape/scrape.go:84)—— 管理一个 job 下所有目标的抓取。配置变化时,它负责增删 loop。
  • scrapeLoop(scrape/scrape.go:828)—— 一个目标一个,它的 run()(scrape.go:1263)就是那个永不停歇的循环:等到点 → 抓 → 解析 → relabel → 交给 storage.Appender。

用哈希把目标打散:避免惊群

一个精妙的细节。假设你有 5000 个目标、抓取间隔 15s——如果它们同时被抓,每 15 秒就会有一次 5000 并发的尖峰,把 Prometheus 和网络都打出锯齿。

看 Target.offset() 怎么解决(真实源码 scrape/target.go:170):

func (t *Target) offset(interval time.Duration, offsetSeed uint64) time.Duration {
    now := time.Now().UnixNano()
    var (
        base   = int64(interval) - now%int64(interval)
        offset = (t.hash() ^ offsetSeed) % uint64(interval)   // ← 关键
        next   = base + int64(offset)
    )
    if next > int64(interval) {
        next -= int64(interval)
    }
    return time.Duration(next)
}

每个目标的抓取时刻,由它自己标签的哈希决定一个在 [0, interval) 内的固定偏移——于是 5000 个目标被均匀摊开在 15 秒里,负载平滑,没有惊群。

为什么用哈希而不是随机数?因为哈希是确定性的:

  • 同一个目标,偏移永远相同 —— 于是它的采样点在时间轴上是等间距的(每次都是第 7.3 秒被抓),而不是忽早忽晚。这对 rate() 这类基于时间差的计算很重要——抖动会让速率算出来忽高忽低。
  • 重启后不变 —— Prometheus 重启,目标的偏移还是那个,时间轴上不会出现错位。
  • ^ offsetSeed —— 异或一个种子。这样多个 Prometheus 实例(比如两台做冗余)可以配不同 seed,错开抓取同一批目标,避免同时压向被抓服务。

确定性的哈希打散,是分布式系统里一个反复出现的技巧——上一章的 hashmod 分片也是同一个思路:用哈希取代随机,换来可预测和可复现。

自动附加的 8 个元指标:排查第一现场

每次抓取,Prometheus 会自动写入一批关于“这次抓取本身”的指标(源码 scrape/scrape.go:2044 起):

指标含义实战用途
up抓成功=1,失败=0最重要的指标:up == 0 就是目标挂了
scrape_duration_seconds这次抓取耗时接近 scrape_timeout 就危险了
scrape_samples_scraped抓回来多少个样本基数监控:突然暴涨=有人加了高基数标签
scrape_samples_post_metric_relabelingmetric relabel 之后剩多少和上一个对比,看 relabel 丢了多少
scrape_series_added本次新增多少条 series持续不为 0 = 基数在流失
scrape_timeout_seconds配置的超时和 duration 对比
scrape_sample_limit配置的样本上限见下
scrape_body_size_bytes响应体大小端点是不是太胖了

这 8 个指标是“关于监控的监控”——排查采集问题时,它们是你的第一现场。几条立刻能用的查询:

# 谁挂了?
up == 0

# 抓取要超时了?(耗时超过超时的 80%)
scrape_duration_seconds > 0.8 * scrape_timeout_seconds

# 谁在制造基数?(按 job 看每次抓回的样本数)
topk(10, scrape_samples_scraped)

# 基数在持续增长?(series 不断新增 = 有高基数标签在流失)
rate(scrape_series_added[10m]) > 0

scrape_series_added 持续大于 0 是个强烈的警报信号:正常情况下,一个稳定的服务应该反复吐出同一批 series,新增应该趋近于 0。如果它一直在涨,说明每次抓取都在产生新的标签组合——典型元凶是标签里混进了时间戳、UUID、递增 ID、错误消息。这是 TSDB 内存爆炸的前兆,能在事故前抓到它。

sample_limit:防止一个目标拖垮全局

sample_limit 是一道自保闸门**:如果一次抓取返回的样本数超过限制,Prometheus 会丢弃整次抓取并把 up 记为 0。**

这个设计初看很粗暴(为什么不留一部分?),但它是对的:一个失控的服务不应该有能力打垮整个监控系统。如果某个应用因为 bug 吐出 100 万条 series,没有闸门的话 Prometheus 会照单全收,然后 TSDB 内存爆掉、整个集群的监控全瞎——为了一个坏服务牺牲所有服务的可观测性,显然不划算。

丢弃整次而非部分,是为了避免“部分数据”带来的误导——半截的指标可能让你算出完全错误的结论,还不如明确地“这个目标现在没数据(up=0)”。宁可明确地失败,也不要含糊地成功,这是监控系统该有的态度。

实战建议:给 sample_limit 设一个值(比如 10000),别裸奔。配合上面的 scrape_samples_scraped 告警,你能在撞上限之前就发现苗头。

staleness:怎么知道一条 series “没了”

一个容易忽略但很精妙的机制。目标消失了(pod 被删),它的 series 该怎么办?

Prometheus 会给消失的 series 写入一个特殊的 staleness marker(陈旧标记)——一个特殊的 NaN 值,表示“这条 series 到此为止了”。

为什么需要它?如果只是“不再写新数据”,那么查询时 Prometheus 会因为 lookback(默认往回找 5 分钟)而继续返回最后那个值——于是一个已经死掉的 pod,在图上还会有一条“平的线”持续 5 分钟。更糟的是,如果这条 series 参与了告警计算,你会基于幽灵数据做决策。

staleness marker 让查询立刻知道“这条 series 结束了”,不再返回它。这解决了:

  • 目标消失(pod 删除、SD 里没了)。
  • 某条指标不再被暴露(应用不再吐这个 series 了)。

这是“数据缺失”和“数据为 0”之间的区别——两者语义完全不同,而 staleness marker 让 Prometheus 能表达前者。实战意义:写告警时要分清“值是 0”(服务活着但没流量)和“没有数据”(服务没了)——后者常常需要用 absent() 或 up == 0 来单独处理。

实战:采集问题排查顺序

把这一章的东西串成一个套路。当“我的指标没了”时,按这个顺序查:

图表加载中…

这个顺序的逻辑是“从外到内、从便宜到贵”:先看 up(一个查询搞定)→ 再 curl 端点(确认是采集问题还是埋点问题)→ 最后才查 relabel 规则(最烧脑)。

小结与下一部分

scrapeLoop(scrape/scrape.go:828,run():1263)是 Prometheus 的心脏,一个目标一个,由 scrapePool(:84)管理。Target.offset()(target.go:170)用标签哈希给每个目标算一个固定偏移,把目标均匀摊开避免惊群——用哈希而非随机是为了采样等间距(rate() 才准)、重启后不变、多实例可用 seed 错开。每次抓取自动附加 8 个元指标(scrape.go:2044 起),它们是“关于监控的监控”、排查第一现场——尤其 up == 0(挂了)和 scrape_series_added 持续 > 0(基数流失的前兆)。sample_limit 是自保闸门:超限则丢弃整次抓取并 up=0——宁可明确失败,不要含糊成功。staleness marker(特殊 NaN)让查询知道“这条 series 到此为止”,避免死掉的目标因 lookback 留下 5 分钟的幽灵数据。

第 1 部分讲完了“指标怎么被拿到”。但一个更前置、也更重要的问题是:你到底该定义哪些指标? 下一部分是本专栏最实战的部分之一。👉 第 2 部分 · 指标体系:RED、USE 与黄金信号。

参考资料

  • [1] Prometheus 源码 scrape/scrape.go(scrapePool:84、scrapeLoop:828、run:1263、元指标 :2044 起)、scrape/target.go:170(offset)。
  • [2] Prometheus 官方文档:Configuration — sample_limit;Querying basics — staleness。
  • [3] 关联服务发现、relabeling、基数控制。

本页目录