请参与 Prometheus 用户调研(2026 年 3 月版) ,帮助社区确定未来开发工作的优先级!

插桩

本页提供了一套关于为代码进行插桩的指导原则。

如何插桩

简而言之,要对所有内容进行插桩。每个库、子系统和服务至少都应该有几个指标,以便让你对其运行状态有个大致的了解。

插桩应成为代码不可或缺的一部分。在代码中使用指标类的地方实例化它们。这样当你追踪错误时,从告警跳转到控制台再到代码将变得非常容易。

三种类型的服务

出于监控目的,服务通常可以分为三种类型:在线服务、离线处理和批处理作业。它们之间存在重叠,但每种服务倾向于归入其中一个类别。

在线服务系统

在线服务系统是指人类或其他系统期望立即得到响应的系统。例如,大多数数据库和 HTTP 请求都属于这一类。

这类系统的关键指标是已执行查询的数量、错误数和延迟。正在处理的请求数也很有用。

关于统计失败查询,请参阅下文的故障部分。

应该在客户端和服务端监控在线服务系统。如果双方观察到的行为不同,这对调试非常有用。如果服务有很多客户端,服务逐一追踪它们是不现实的,因此必须依赖各自的统计信息。

在统计查询时要保持一致,是统计开始时还是结束时。建议在查询结束时统计,因为它会与错误和延迟统计保持一致,而且编码更容易。

离线处理

对于离线处理,没有人主动等待响应,且工作通常是分批进行的。处理过程可能包含多个阶段。

对于每个阶段,追踪进入的项目数、正在处理的数量、上次处理的时间以及发出的项目数。如果进行批处理,还应追踪进出的批次。

了解系统上次处理的时间有助于检测其是否停滞,但这是非常局部的。更好的方法是在系统中发送一个“心跳”:即一个贯穿整个流程并包含插入时间戳的伪项目。每个阶段都可以导出它所见过的最新心跳时间戳,让你知道项目在系统中传播需要多长时间。对于没有静默期(即一直有处理发生)的系统,可能不需要显式的心跳。

批处理作业

离线处理和批处理作业之间的界限模糊,因为离线处理可能是在批处理作业中完成的。批处理作业的区别在于它们不是连续运行的,这使得采集(Scraping)它们变得困难。

批处理作业的关键指标是它上次成功的时间。追踪每个主要阶段花费的时间、整体运行时长以及作业上次完成的时间(无论成功或失败)也很有用。这些都是 Gauge 指标,应该推送到 PushGateway。通常还有一些特定于作业的统计信息非常有用,例如处理的总记录数。

对于运行时间超过几分钟的批处理作业,建议同时使用基于拉取(Pull)的监控进行采集。这让你能够像对待其他类型的作业一样,随时间推移追踪相同的指标,例如资源使用情况和与其他系统通信时的延迟。如果作业开始变慢,这有助于调试。

对于运行频率极高的批处理作业(比如每 15 分钟运行一次以上),你应该考虑将其转换为守护进程,并按照离线处理作业来处理。

子系统

除了这三种主要的服务类型外,系统还有一些也应该被监控的子部件。

库应当提供插桩功能,且用户无需进行额外配置。

如果该库用于访问进程外部的某种资源(例如网络、磁盘或 IPC),至少要追踪总查询计数、错误数(如果可能出错)和延迟。

根据库的规模,追踪库内部的错误和延迟,以及你认为可能有用的任何通用统计信息。

库可能被应用程序中多个独立的部分针对不同资源所使用,因此在适当情况下,务必使用标签来区分不同的用途。例如,数据库连接池应该区分它所连接的数据库,而不需要区分 DNS 客户端库的不同用户。

日志

作为一般规则,每一行日志代码都应该配有一个递增的计数器。如果你发现了一条有用的日志信息,你应该能够查看它出现的频率以及持续了多久。

如果同一函数中有多个密切相关的日志信息(例如,if 或 switch 语句的不同分支),有时为它们全部增加同一个计数器是有意义的。

通常也有必要导出整个应用程序记录的信息/错误/警告行的总数,并在发布过程中检查是否存在显著差异。

故障

故障的处理方式应与日志类似。每次发生故障时,计数器都应递增。与日志不同的是,根据代码的结构,错误可能会冒泡到更通用的错误计数器。

在报告故障时,通常应有另一个代表尝试总次数的指标。这样可以轻松计算失败率。

线程池

对于任何类型的线程池,关键指标是排队请求数、正在使用的线程数、总线程数、已处理的任务数以及任务执行耗时。追踪任务在队列中的等待时间也很有用。

缓存

缓存的关键指标是总查询数、命中数、整体延迟,以及缓存后端所处的在线服务系统的查询计数、错误数和延迟。

收集器

在实现非平凡的自定义指标收集器时,建议导出一个表示收集时长(秒)的 Gauge,以及另一个表示所遇错误数量的 Gauge。

这是允许将持续时间导出为 Gauge(而不是 Summary 或 Histogram)的两种情况之一,另一种情况是批处理作业的持续时间。这是因为两者都代表关于特定推送/采集的信息,而不是随时间追踪多个持续时间。

注意事项

在进行监控时,有一些通用的注意事项,也有一些专门针对 Prometheus 的注意事项。

使用标签

很少有监控系统具备标签的概念和利用标签的表达式语言,因此需要一些时间来适应。

当你想要加、平均或求和多个指标时,它们通常应该是一个带标签的指标,而不是多个指标。

例如,不要使用 http_responses_500_totalhttp_responses_403_total,而应创建一个名为 http_responses_total 的单个指标,并使用一个 code 标签来表示 HTTP 响应码。然后,你就可以在规则和图表中将整个指标作为一个整体进行处理。

根据经验,指标名称的任何部分都不应以程序方式生成(应使用标签)。唯一的例外是从另一个监控/插桩系统代理指标时。

另请参阅命名部分。

不要滥用标签

每个标签集都是一个额外的时间序列,会占用 RAM、CPU、磁盘和网络资源。通常开销可以忽略不计,但在拥有大量指标和数百台服务器且分布有数百个标签集的情况下,这些开销会迅速累积。

一般指南是,尽量将指标的基数(Cardinality)保持在 10 以下。对于超过此基数的指标,尽量限制在整个系统中的少数几个。绝大多数指标应该没有标签。

如果某个指标的基数超过 100,或者有可能增长到那么大,请考虑替代方案,例如减少维度数量或将分析工作从监控系统迁移到通用处理系统。

为了让你更好地了解潜在数值,让我们看看 node_exporter。node_exporter 为每个挂载的文件系统暴露指标。对于 node_filesystem_avail,每个节点都会有几十个时间序列。如果你有 10,000 个节点,你最终会有大约 100,000 个 node_filesystem_avail 的时间序列,这对 Prometheus 来说是可以处理的。

如果你现在要添加每个用户的配额,拥有 10,000 个节点上的 10,000 个用户,很快就会达到千万级。这对当前的 Prometheus 实现来说负担太重了。即使数量较少,也会存在机会成本,因为你无法再在这台机器上拥有其他可能更有用的指标了。

如果你不确定,请从无标签开始,随着具体用例的出现再逐步添加标签。

计数器 vs. Gauge,摘要 vs. 直方图

了解为给定指标使用四种主要指标类型中的哪一种非常重要。

要在计数器(Counter)和 Gauge 之间进行选择,有一个简单的经验法则:如果数值可以减小,它就是一个 Gauge。

计数器只能增加(但在进程重启时会重置)。它们可用于累积事件数量或每次事件发生的量。例如,HTTP 请求总数或 HTTP 请求中发送的字节总数。原始计数器很少有用。使用 rate() 函数来获取其每秒增加的速率。

Gauge 可以被设置、增加和减少。它们适用于状态快照,例如正在进行的请求、空闲/总内存或温度。绝不应该对 Gauge 使用 rate()

摘要(Summary)和直方图(Histogram)是更复杂的指标类型,将在其专门部分中讨论。

时间戳,而不是距离当前的时间

如果你想追踪距离某事发生已经过去的时间,请导出它发生的 Unix 时间戳,而不是距离它发生的时间。

导出时间戳后,你可以使用表达式 time() - my_timestamp_metric 来计算距离该事件的时间,从而消除了更新逻辑的需要,并防止更新逻辑陷入停滞。

内层循环

总体而言,插桩带来的额外资源成本远小于它对运维和开发带来的好处。

对于性能关键或在特定进程内每秒被调用超过 10 万次的代码,你可能需要谨慎考虑更新指标的数量。

根据争用情况,Java 计数器的增量操作需要 12-17ns 。其他语言也有类似的性能。如果这段时间对你的内层循环来说很重要,请限制在该循环中更新的指标数量,并尽可能避免使用标签(或者缓存标签查找的结果,例如 Go 中的 With() 返回值或 Java 中的 labels())。

还要注意涉及时间或持续时间的指标更新,因为获取时间可能涉及系统调用(syscall)。正如所有涉及性能关键代码的事项一样,基准测试是确定任何给定更改影响的最佳方法。

避免丢失指标

在事件发生前不存在的时间序列很难处理,因为通常的简单操作不足以正确处理它们。为避免这种情况,请为你预知可能存在的时间序列导出默认值(例如 0)。

大多数 Prometheus 客户端库(包括 Go、Java 和 Python)会自动为你导出无标签指标的 0 值。

本页内容