仪表化
本页提供了一套关于代码仪表化的主观指南。
如何进行仪表化
简而言之,就是对所有事物进行仪表化。每个库、子系统和服务都应该至少有一些指标,以便您大致了解其性能。
仪表化应该是您代码不可或缺的一部分。在您使用指标类的同一文件中实例化它们。这使得在追踪错误时,从告警到控制台再到代码的流程变得简单。
三种服务类型
出于监控目的,服务通常可以分为三种类型:在线服务、离线处理和批处理作业。它们之间存在重叠,但每种服务都倾向于很好地归入其中一个类别。
在线服务系统
在线服务系统是指人类或其他系统期望即时响应的系统。例如,大多数数据库和 HTTP 请求都属于此类别。
此类系统中的关键指标是执行的查询数量、错误和延迟。正在进行的请求数量也可能很有用。
要计算失败的查询,请参阅下面的“失败”部分。
在线服务系统应在客户端和服务器端同时进行监控。如果两端表现出不同的行为,这对调试来说是非常有用的信息。如果一个服务有许多客户端,服务单独跟踪它们是不切实际的,因此它们必须依赖自己的统计数据。
无论您是在查询开始时还是结束时进行计数,请保持一致。建议在查询结束时计数,因为它将与错误和延迟统计数据保持一致,并且通常更容易编码。
离线处理
对于离线处理,没有人会主动等待响应,并且批量处理工作很常见。处理过程也可能存在多个阶段。
对于每个阶段,跟踪进入的项、正在进行的数量、上次处理的时间以及发送出去的项数量。如果是批量处理,您还应该跟踪进出的批次。
了解系统上次处理某事物的时间对于检测其是否停滞很有用,但这是一种非常局部化的信息。更好的方法是通过系统发送心跳:一些虚拟项,它们被一路传递并包含插入时的时间戳。每个阶段都可以导出它看到的最新心跳时间戳,让您知道项在系统中传播所需的时间。对于没有空闲期(即没有处理发生)的系统,可能不需要明确的心跳。
批处理作业
离线处理和批处理作业之间存在模糊界限,因为离线处理可能在批处理作业中完成。批处理作业的特点是它们不持续运行,这使得抓取它们变得困难。
批处理作业的关键指标是它上次成功的完成时间。跟踪作业每个主要阶段所花费的时间、总运行时间以及作业上次完成(成功或失败)的时间也很有用。这些都是 Gauge 类型指标,应推送到 PushGateway。通常还有一些有用的整体作业特定统计数据需要跟踪,例如处理的总记录数。
对于运行时间超过几分钟的批处理作业,使用拉取式监控抓取它们也很有用。这使您可以像监控其他类型的作业一样,随时间跟踪相同的指标,例如资源使用情况以及与其他系统通信时的延迟。如果作业开始变慢,这有助于调试。
对于运行非常频繁(例如,每 15 分钟一次以上)的批处理作业,您应该考虑将其转换为守护进程并将其作为离线处理作业进行处理。
子系统
除了三种主要服务类型之外,系统还包含应同样受到监控的子部分。
库
库应提供仪表化功能,无需用户进行额外配置。
如果这是一个用于访问进程外部资源(例如,网络、磁盘或 IPC)的库,则至少应跟踪总查询计数、错误(如果可能发生错误)和延迟。
根据库的复杂程度,跟踪库内部的错误和延迟,以及您认为可能有用的任何通用统计数据。
一个库可能被应用程序的多个独立部分用于不同的资源,因此在适当的情况下,请注意使用标签区分其用途。例如,数据库连接池应区分其连接的数据库,而 DNS 客户端库的用户则无需区分。
日志
通常,对于每一行日志代码,您都应该有一个递增的计数器。如果您发现一条有趣的日志消息,您会希望能够查看它发生的频率和持续时间。
如果在同一函数中有多个密切相关的日志消息(例如,if 或 switch 语句的不同分支),有时可以对它们全部递增一个单独的计数器。
导出应用程序整体记录的信息/错误/警告日志总行数,并将其作为发布过程的一部分检查是否存在显著差异,这通常也很有用。
失败
失败应与日志记录类似处理。每次发生失败时,计数器都应递增。与日志记录不同,错误也可能根据您的代码结构冒泡到更通用的错误计数器。
报告失败时,您通常应该有另一个指标来表示总尝试次数。这使得失败率易于计算。
线程池
对于任何类型的线程池,关键指标是排队请求的数量、正在使用的线程数量、线程总数、处理的任务数量以及它们花费的时间。跟踪事物在队列中等待了多长时间也很有用。
缓存
缓存的关键指标是总查询量、命中量、整体延迟,以及缓存前方任何在线服务系统的查询计数、错误和延迟。
收集器
在实现一个非简单的自定义指标收集器时,建议导出一个 Gauge 类型指标表示收集耗时(秒),另一个表示遇到的错误数量。
这是将持续时间导出为 Gauge 而不是 Summary 或 Histogram 的两种情况之一,另一种是批处理作业持续时间。这是因为它们都代表了特定推送/抓取的信息,而不是随时间跟踪多个持续时间。
注意事项
在进行监控时有一些通用注意事项,特别是与 Prometheus 相关的。
使用标签
很少有监控系统具有标签概念和利用标签的表达式语言,因此需要一些时间来适应。
当您有多个需要添加/平均/求和的指标时,它们通常应该是一个带有标签的指标,而不是多个指标。
例如,与其使用 http_responses_500_total
和 http_responses_403_total
,不如创建一个名为 http_responses_total
的单一指标,并使用 code
标签来表示 HTTP 响应代码。然后您可以在规则和图表中将整个指标作为一个整体进行处理。
根据经验法则,指标名称的任何部分都不应通过程序生成(而是使用标签)。唯一的例外是从另一个监控/仪表化系统代理指标时。
另请参阅命名部分。
不要过度使用标签
每个标签集都是一个额外的时间序列,会产生内存、CPU、磁盘和网络开销。通常开销可以忽略不计,但在拥有大量指标和数百台服务器上数百个标签集的情况下,这会迅速累积。
作为一般指导原则,尝试将指标的基数保持在 10 以下,对于超出此范围的指标,目标是在整个系统中将其数量限制在少数。您的绝大多数指标应该没有标签。
如果您的指标基数超过 100 或有可能增长到那么大,请研究替代解决方案,例如减少维度数量或将分析从监控转移到通用处理系统。
为了让您更好地了解底层数字,我们来看看 node_exporter。node_exporter 会暴露每个已挂载文件系统的指标。每个节点都会有几十个时间序列,例如 node_filesystem_avail
。如果您有 10,000 个节点,那么 node_filesystem_avail
将会有大约 100,000 个时间序列,这对于 Prometheus 来说是可以处理的。
如果您现在为每个用户添加配额,那么在 10,000 个节点上拥有 10,000 个用户,您将很快达到数千万的数量。这对于 Prometheus 当前的实现来说太多了。即使数量较小,也存在机会成本,因为您无法在该机器上拥有其他可能更有用的指标。
如果您不确定,可以从不使用标签开始,然后随着具体用例的出现,逐渐添加更多标签。
计数器与 Gauge,摘要与直方图
了解对于给定指标应该使用四种主要指标类型中的哪一种非常重要。
在计数器和 Gauge 之间进行选择时,有一个简单的经验法则:如果值可以下降,那么它就是 Gauge。
计数器只能向上增加(并在进程重启时重置)。它们可用于累积事件数量或每个事件的某个数量。例如,HTTP 请求的总数,或 HTTP 请求中发送的总字节数。原始计数器很少有用。使用 rate()
函数获取它们每秒的增长速率。
Gauge 可以被设置、向上增加或向下减少。它们对于状态快照很有用,例如正在进行的请求、空闲/总内存或温度。您绝不应该对 Gauge 使用 rate()
函数。
摘要和直方图是更复杂的指标类型,在各自的部分中进行了讨论。
时间戳,而非已过时间
如果您想跟踪某事发生以来的时间,请导出它发生的 Unix 时间戳 — 而不是它发生以来已过的时间。
导出时间戳后,您可以使用表达式 time() - my_timestamp_metric
来计算事件发生以来的时间,从而消除更新逻辑的需要,并保护您免受更新逻辑卡死的风险。
内部循环
通常,仪表化带来的额外资源开销远低于其为运维和开发带来的益处。
对于性能关键型代码或在给定进程中每秒调用超过 10 万次的代码,您可能需要注意更新的指标数量。
一个 Java 计数器需要12-17 纳秒来递增,具体取决于争用情况。其他语言也会有类似的性能。如果这段时间对您的内部循环很重要,请限制在内部循环中递增的指标数量,并尽可能避免使用标签(或缓存标签查找的结果,例如 Go 中的 With()
或 Java 中的 labels()
的返回值)。
还要注意涉及时间或持续时间的指标更新,因为获取时间可能涉及系统调用。与所有涉及性能关键型代码的事项一样,基准测试是确定任何给定更改影响的最佳方法。
避免指标缺失
直到发生某事才出现的时间序列很难处理,因为通常的简单操作不足以正确处理它们。为了避免这种情况,对于您已知可能提前存在的任何时间序列,请导出默认值(例如 0
)。
大多数 Prometheus 客户端库(包括 Go、Java 和 Python)将自动为您导出没有标签的指标的 0
。