本页提供了一套关于对代码进行仪器仪表的指导意见。
简而言之,就是要对所有内容进行仪器仪表。每个库、子系统和服务都应该至少有一些指标,以便大致了解其性能。
仪器仪表应该是代码的一个组成部分。在使用指标类的同一文件中实例化它们。这使得在追踪错误时,从告警、到控制台、再到代码的流程变得简单。
出于监控目的,服务通常可以分为三种类型:在线服务、离线处理和批处理作业。它们之间存在重叠,但每种服务通常都很好地适合其中一种类别。
在线服务系统是指人类或另一个系统期望立即响应的系统。例如,大多数数据库和 HTTP 请求属于这一类别。
这种系统中的关键指标是已执行查询的数量、错误数和延迟。正在进行的请求数也可能有用。
有关计数失败查询的信息,请参阅下方的失败章节。
在线服务系统应在客户端和服务器端都进行监控。如果两边看到不同的行为,这对调试来说是非常有用的信息。如果一个服务有许多客户端,服务单独跟踪它们是不实际的,因此它们必须依靠自己的统计信息。
在计算查询时,无论是从开始时计数还是从结束时计数,都要保持一致。建议从结束时计数,因为它将与错误和延迟统计数据保持一致,并且通常更容易编写代码。
对于离线处理,没有人积极等待响应,并且工作批处理很常见。还可能存在多个处理阶段。
对于每个阶段,跟踪进入的项、正在进行的项数、上次处理某物的时间以及发送出去的项数。如果进行批处理,您还应该跟踪进入和离开的批次。
了解系统上次处理某物的时间对于检测其是否停滞很有用,但这是一种非常局限的信息。更好的方法是通过系统发送心跳:一个会一直传递下去的虚拟项,包含插入时的时间戳。每个阶段都可以导出它看到的最新的心跳时间戳,让您了解项通过系统传播需要多长时间。对于没有安静期(没有处理发生)的系统,可能不需要明确的心跳。
离线处理和批处理作业之间有一条模糊界线,因为离线处理可以在批处理作业中完成。批处理作业的特点是它们不持续运行,这使得抓取它们变得困难。
批处理作业的关键指标是其上次成功的时间。跟踪作业每个主要阶段花费的时间、总运行时间以及作业上次完成的时间(成功或失败)也很有用。这些都是计量器(gauge),应推送到 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 个节点,您最终将拥有大约 100,000 个 node_filesystem_avail
的时间序列,这对于 Prometheus 处理来说没问题。
如果您现在添加每个用户的配额,那么在 10,000 个节点上拥有 10,000 个用户时,您将很快达到数千万的时间序列。这对于 Prometheus 当前的实现来说太多了。即使数量较少,也存在机会成本,因为您无法再在该机器上拥有其他可能更有用的指标。
如果您不确定,请从不使用标签开始,并随着具体用例的出现逐渐添加更多标签。
了解给定指标应使用四种主要指标类型中的哪一种非常重要。
在计数器(counter)和计量器(gauge)之间进行选择时,有一个简单的经验法则:如果值可以下降,它就是计量器。
计数器只能增加(并在进程重启等情况下重置)。它们对于累积事件数量或每个事件中某个事物的数量很有用。例如,HTTP 请求总数或 HTTP 请求中发送的总字节数。原始计数器很少有用。使用 rate()
函数获取它们每秒增加的速率。
计量器可以设置、增加和减少。它们对于状态的快照很有用,例如正在进行的请求、可用/总内存或温度。您绝不应该对计量器使用 rate()
函数。
摘要(summary)和直方图(histogram)是更复杂的指标类型,在它们自己的章节中讨论。
如果您想跟踪某事发生以来经过的时间,请导出其发生的 Unix 时间戳,而不是自发生以来的时间长度。
导出时间戳后,您可以使用表达式 time() - my_timestamp_metric
来计算自事件发生以来经过的时间,从而无需更新逻辑,并保护您免受更新逻辑卡住的影响。
总的来说,仪器仪表带来的额外资源成本远不及它为运维和开发带来的好处。
对于性能关键型代码或在给定进程内部每秒被调用超过 10 万次的代码,您可能需要注意更新多少指标。
Java 计数器的递增时间为12-17纳秒,具体取决于争用情况。其他语言也将具有类似的性能。如果这段时间对于您的内部循环很重要,请限制您在内部循环中递增的指标数量,并尽可能避免使用标签(或缓存标签查找的结果,例如,Go 中的 With()
或 Java 中的 labels()
的返回值)。
还要注意涉及时间或持续时间的指标更新,因为获取时间可能涉及系统调用。与所有涉及性能关键代码的事项一样,基准测试是确定任何给定更改影响的最佳方法。
只有在发生某些事情后才会出现的时间序列很难处理,因为通常简单的操作已不足以正确处理它们。为避免这种情况,对于您提前知道可能存在的任何时间序列,请导出一个默认值,例如 0
。
大多数 Prometheus 客户端库(包括 Go、Java 和 Python)会自动为您没有标签的指标导出 0
。
本文档是开源的。请通过提交问题或拉取请求帮助改进它。