Instrumentation
This page provides an opinionated set of guidelines for instrumenting your code.
How to instrument
The short answer is to instrument everything. Every library, subsystem and service should have at least a few metrics to give you a rough idea of how it is performing.
Instrumentation should be an integral part of your code. Instantiate the metric classes in the same file you use them. This makes going from alert to console to code easy when you are chasing an error.
The three types of services
For monitoring purposes, services can generally be broken down into three types: online-serving, offline-processing, and batch jobs. There is overlap between them, but every service tends to fit well into one of these categories.
Online-serving systems
An online-serving system is one where a human or another system is expecting an immediate response. For example, most database and HTTP requests fall into this category.
The key metrics in such a system are the number of performed queries, errors, and latency. The number of in-progress requests can also be useful.
有关计算失败查询,请参阅下方 失败 部分。
在线服务系统应在客户端和服务器端进行监控。如果两端看到的行为不同,这对调试来说是非常有用的信息。如果一个服务有很多客户端,那么让服务单独跟踪它们是不现实的,因此它们必须依赖于自己的统计数据。
在计算查询时,请始终如一地选择查询开始时或结束时进行计数。建议在查询结束时计数,因为它会与错误和延迟统计数据保持一致,并且通常更容易编写代码。
离线处理
对于离线处理,没有人主动等待响应,并且通常会进行批量处理。也可能存在多个处理阶段。
对于每个阶段,跟踪传入的条目数、正在处理的条目数、上一次处理操作的时间以及发出的条目数。如果进行批量处理,您还应该跟踪进出批次。
了解系统上一次处理某项内容的时间有助于检测其是否已停滞,但这仅是局部信息。更好的方法是通过系统发送心跳:一个虚拟的条目,它会一直传递到最后,并包含其插入的时间戳。每个阶段都可以导出它看到的最新心跳时间戳,让您知道条目在系统中传播需要多长时间。对于没有处理期间的系统(即没有静默期),可能不需要显式心跳。
批处理作业
离线处理和批处理作业之间有一条模糊的界线,因为离线处理可能以批处理作业的形式进行。批处理作业的特点是它们不连续运行,这使得抓取它们变得困难。
批处理作业的关键指标是其最后一次成功运行的时间。跟踪作业的每个主要阶段花费的时间、整体运行时间和作业完成(成功或失败)的最后一次时间也很有用。这些都是仪表盘指标,应该推送到 PushGateway。通常,还会有一些整体的、特定于作业的统计数据,例如处理的记录总数,这些也很有用。
对于运行时间超过几分钟的批处理作业,使用拉取式监控来抓取它们也很有用。这允许您跟踪与其他类型作业相同的指标,例如资源使用情况以及与其他系统通信时的延迟。如果作业开始变慢,这有助于调试。
对于运行非常频繁的批处理作业(例如,每 15 分钟一次以上),您应该考虑将其转换为守护进程,并将其作为离线处理作业来处理。
子系统
除了三种主要的服务类型外,系统还包含应该进行监控的子部分。
库
库应提供无需用户额外配置即可使用的检测功能。
如果这是一个用于访问进程外资源的库(例如,网络、磁盘或 IPC),则至少应跟踪总查询数、错误(如果可能发生错误)和延迟。
根据库的复杂程度,跟踪库内部的内部错误和延迟,以及您认为有用的任何通用统计信息。
一个库可能被应用程序的多个独立部分用于不同的资源,因此请注意在适当的地方使用标签来区分不同的用途。例如,数据库连接池应区分其正在通信的数据库,而无需区分 DNS 客户端库的用户。
日志记录
通常来说,每行日志代码都应该有一个递增的计数器。如果您发现有趣的日志消息,您会希望能够看到它发生了多少次以及持续了多久。
如果同一函数中有多个密切相关的日志消息(例如,if 或 switch 语句的不同分支),有时将所有这些消息递增到一个计数器中是合理的。
将应用程序整体记录的信息/错误/警告行的总数导出,并在发布过程中检查是否有显著差异,通常也很有用。
故障
故障的处理方式应与日志记录类似。每次发生故障时,都应递增一个计数器。与日志记录不同,根据代码的结构,错误也可能冒泡到一个更通用的错误计数器。
在报告故障时,通常应该有另一个表示尝试总数的指标。这使得故障率易于计算。
线程池
对于任何类型的线程池,关键指标是排队请求的数量、正在使用的线程数、总线程数、已处理的任务数以及它们花费的时间。跟踪队列中事物等待的时间也很有用。
缓存
缓存的关键指标是总查询数、命中数、总体延迟,以及缓存前面的在线服务系统的查询数、错误和延迟。
收集器
在实现一个非平凡的自定义指标收集器时,建议导出一个表示收集耗时的秒数以及遇到的错误数的仪表盘。
这是导出持续时间作为仪表盘的两个情况之一,另一个是批处理作业的持续时间。这是因为两者都代表了该特定推送/抓取的信息,而不是跟踪一段时间内的多个持续时间。
注意事项
在进行监控时,有一些一般性注意事项,特别是针对 Prometheus 的。
使用标签
很少有监控系统具备标签的概念和利用这些标签的表达式语言,所以需要一些时间来适应。
当您有多个要相加/平均/求和的指标时,它们通常应该是一个带有标签的指标,而不是多个指标。
例如,不要使用 http_responses_500_total 和 http_responses_403_total,而是创建一个名为 http_responses_total 的单一指标,并为其添加一个 code 标签来表示 HTTP 响应代码。然后,您可以在规则和图中将整个指标作为一个整体进行处理。
经验法则:指标名称的任何部分都不应通过程序生成(使用标签代替)。唯一例外是在代理来自另一个监控/检测系统的指标时。
另请参阅 命名 部分。
不要过度使用标签
每个标签集都是一个额外的时间序列,具有 RAM、CPU、磁盘和网络成本。通常开销可以忽略不计,但在存在大量指标和数百个标签集跨越数百台服务器的情况下,这会迅速累加。
作为一般指导,尽量将指标的基数保持在 10 以下,对于超过这个值的指标,争取在整个系统中将其限制为少数几个。绝大多数指标应该没有标签。
如果您有一个基数超过 100 或有潜力增长到那么大的指标,请研究替代方案,例如减少维数或将分析从监控转移到通用处理系统。
为了让您对底层数字有更好的了解,让我们看看 node_exporter。node_exporter 会为每个挂载的文件系统暴露指标。例如,每个节点会有几十个时间序列,比如 node_filesystem_avail。如果您有 10,000 个节点,那么 node_filesystem_avail 将会有大约 100,000 个时间序列,这对于 Prometheus 来说是可以处理的。
如果您现在要为每个用户添加配额,那么在 10,000 个用户和 10,000 个节点的情况下,您将很快达到数千万的级别。这对 Prometheus 的当前实现来说太多了。即使数字较小,也存在机会成本,因为您无法在该机器上拥有其他可能更有用的指标了。
如果您不确定,请从没有标签开始,随着具体用例的出现,再逐渐添加更多标签。
计数器与仪表盘,摘要与直方图
了解为特定指标选择哪种主要指标类型(四种)非常重要。
在计数器和仪表盘之间选择,有一个简单的经验法则:如果值可以下降,那么它就是仪表盘。
计数器只能增加(并且可以重置,例如进程重启时)。它们可用于累积事件数量或每次事件的数量。例如,HTTP 请求的总数,或 HTTP 请求中发送的字节总数。原始计数器很少有用。使用 rate() 函数来获取它们增加的每秒速率。
仪表盘可以被设置,可以增加,也可以下降。它们适用于状态快照,例如进行中的请求、可用/总内存或温度。您永远不应该对仪表盘使用 rate()。
摘要和直方图是更复杂的指标类型,将在它们自己的部分中讨论。
时间戳,而非已过时间
如果您想跟踪自某事件发生以来经过的时间,请导出事件发生的 Unix 时间戳,而不是自事件发生以来的时间。
导出时间戳后,您可以使用表达式 time() - my_timestamp_metric 来计算自事件以来的时间,无需更新逻辑,并防止更新逻辑卡死。
内部循环
总的来说,检测的额外资源成本远远小于它为运维和开发带来的好处。
对于性能关键的代码,或在单个进程中每秒调用超过 10 万次的函数,您可能需要小心您更新多少指标。
Java 计数器每次递增需要 12-17ns ,具体取决于争用情况。其他语言的性能也类似。如果这点时间对您的内部循环很重要,请限制内部循环中更新的指标数量,并在可能的情况下避免使用标签(或缓存标签查找的结果,例如 Go 中的 With() 或 Java 中的 labels() 的返回值)。
还要注意涉及时间或持续时间的指标更新,因为获取时间可能需要系统调用。与所有涉及性能关键代码的事务一样,基准测试是确定任何给定更改影响的最佳方法。
避免丢失指标
直到某件事情发生后才存在的时序很难处理,因为通常的简单操作不足以正确处理它们。为了避免这种情况,请为任何您预知可能存在的时序导出默认值,例如 0。
大多数 Prometheus 客户端库(包括 Go、Java 和 Python)都会为您自动导出一个 0 值,用于没有标签的指标。