代码植入

本页提供了一套关于如何为代码植入指标的指导性原则。

如何进行代码植入

简而言之,就是对所有东西都进行代码植入。每个库、子系统和服务都应该至少有一些指标,以便您大致了解其性能如何。

代码植入应该是您代码中不可或缺的一部分。在使用指标类的同一个文件中实例化它们。这样,在排查错误时,从告警到控制台再到代码的切换会变得很容易。

三种服务类型

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

在线服务系统

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

这类系统中的关键指标是执行的查询数量、错误和延迟。正在进行的请求数量也可能有用。

关于计算失败的查询,请参阅下面的失败部分。

在线服务系统应在客户端和服务端两侧进行监控。如果两边看到不同的行为,这对于调试来说是非常有用的信息。如果一个服务有许多客户端,服务方单独跟踪它们是不切实际的,因此它们必须依赖自己的统计数据。

在查询开始时或结束时进行计数,应保持一致。建议在查询结束时计数,因为它将与错误和延迟的统计数据保持一致,并且通常更容易编码。

离线处理

对于离线处理,没有人会主动等待响应,批量处理工作很常见。也可能存在多个处理阶段。

对于每个阶段,跟踪传入的项目、正在处理的项目数量、上次处理某项内容的时间以及发出的项目数量。如果采用批处理,还应跟踪进出的批次。

知道系统上次处理某项内容的时间对于检测其是否停滞很有用,但这是非常局部化的信息。更好的方法是通过系统发送心跳:一个虚拟项目,它会被一直传递下去,并包含其插入时的时间戳。每个阶段都可以导出它看到的最新心跳时间戳,让您知道项目在系统中传播需要多长时间。对于没有空闲处理期的系统,可能不需要明确的心跳。

批处理作业

离线处理和批处理作业之间的界线很模糊,因为离线处理可能是在批处理作业中完成的。批处理作业的特点是它们不是连续运行的,这使得抓取它们变得困难。

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

对于运行时间超过几分钟的批处理作业,使用基于拉取的监控来抓取它们也很有用。这使您可以像对待其他类型的作业一样,随时间跟踪相同的指标,例如与其他系统通信时的资源使用情况和延迟。这有助于在作业开始变慢时进行调试。

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

子系统

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

库应该提供代码植入,而无需用户进行额外配置。

如果它是一个用于访问进程外部资源的库(例如,网络、磁盘或 IPC),至少要跟踪总体查询计数、错误(如果可能出现错误)和延迟。

根据库的复杂程度,跟踪库本身的内部错误和延迟,以及您认为可能有用的任何常规统计信息。

一个库可能被应用程序的多个独立部分用于不同的资源,因此要注意在适当的地方使用标签来区分用途。例如,一个数据库连接池应该区分它正在通信的数据库,而一个 DNS 客户端库则无需区分其用户。

日志记录

一般来说,对于每一行日志代码,都应该有一个相应的计数器被递增。如果您发现一条有趣的日志消息,您会想知道它发生的频率和持续时间。

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

导出应用程序整体记录的 info/error/warning 日志总行数,并在发布过程中检查是否有显著差异,这通常也很有用。

失败

失败的处理方式应与日志记录类似。每次发生失败时,都应该增加一个计数器。与日志记录不同,根据代码结构,错误也可能会冒泡到一个更通用的错误计数器。

报告失败时,通常应该有另一个代表总尝试次数的指标。这使得失败率很容易计算。

线程池

对于任何类型的线程池,关键指标是排队请求的数量、正在使用的线程数量、线程总数、处理的任务数量以及它们花费的时间。跟踪事务在队列中等待的时间也很有用。

缓存

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

收集器

在实现一个非简单的自定义指标收集器时,建议导出一个 Gauge 指标来表示收集花费的时间(秒),以及另一个表示遇到的错误数量。

这是两种可以将持续时间导出为 Gauge 而不是 Summary 或 Histogram 的情况之一,另一种是批处理作业的持续时间。这是因为它们都代表了关于特定推送/抓取的信息,而不是随时间跟踪多个持续时间。

注意事项

在进行监控时,有一些普遍需要注意的事情,特别是针对 Prometheus 的情况。

使用标签

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

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

例如,与其使用 `http_responses_500_total` 和 `http_responses_403_total`,不如创建一个名为 `http_responses_total` 的单一指标,并带有表示 HTTP 响应代码的 `code` 标签。然后您可以在规则和图表中将整个指标作为一个整体进行处理。

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

另请参阅命名部分。

不要过度使用标签

每个标签集都是一个额外的时间序列,会产生 RAM、CPU、磁盘和网络成本。通常开销可以忽略不计,但在有大量指标和跨数百台服务器的数百个标签集的场景中,这可能会迅速累积。

作为一般指导原则,尽量将指标的基数保持在 10 以下,对于超过该值的指标,目标是将其限制在整个系统中的少数几个。绝大多数指标应该没有标签。

如果您的指标基数超过 100 或有可能增长到那么大,请研究替代解决方案,例如减少维度数量或将分析从监控转移到通用处理系统。

为了让您更好地了解底层数据,让我们看看 node_exporter。node_exporter 会为每个挂载的文件系统暴露指标。例如,每个节点都会有几十个关于 `node_filesystem_avail` 的时间序列。如果您有 10,000 个节点,最终将得到大约 100,000 个关于 `node_filesystem_avail` 的时间序列,这对于 Prometheus 来说是可处理的。

如果您现在要为每个用户添加配额,在 10,000 个节点上有 10,000 个用户的情况下,您很快就会达到千万级别。这对于 Prometheus 的当前实现来说太多了。即使数量较小,也会有机会成本,因为您不能再在这台机器上拥有其他可能更有用的指标。

如果不确定,可以先不使用标签,随着具体用例的出现再逐步添加更多标签。

计数器与仪表盘,摘要与直方图

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

在计数器和仪表盘之间选择,有一个简单的经验法则:如果值可以下降,它就是一个仪表盘。

计数器只能上升(以及重置,例如进程重启时)。它们对于累积事件数量或每个事件中某物的数量很有用。例如,HTTP 请求的总数,或 HTTP 请求中发送的总字节数。原始计数器很少有用。使用 `rate()` 函数来获取它们每秒的增长率。

仪表盘可以被设置、上升和下降。它们对于状态快照很有用,例如进行中的请求、可用/总内存或温度。您永远不应该对仪表盘使用 `rate()`。

摘要和直方图是更复杂的指标类型,在它们自己的部分中讨论。

时间戳,而不是经过的时间

如果您想跟踪自某事发生以来的时间,请导出事件发生的 Unix 时间戳,而不是自事件发生以来的时间。

导出时间戳后,您可以使用表达式 `time() - my_timestamp_metric` 来计算自事件发生以来的时间,从而无需更新逻辑,并保护您免受更新逻辑卡住的影响。

内部循环

总的来说,代码植入带来的额外资源成本远小于它为运营和开发带来的好处。

对于性能关键或在给定进程内每秒调用超过 10 万次的代码,您可能需要注意更新多少个指标。

一个 Java 计数器递增需要12-17纳秒,具体取决于争用情况。其他语言会有类似的性能。如果这段时间对您的内部循环很重要,请限制您在内部循环中递增的指标数量,并避免使用标签(或缓存标签查找的结果,例如,Go 中的 `With()` 或 Java 中的 `labels()` 的返回值)。

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

避免缺失指标

直到某事发生才出现的时间序列很难处理,因为通常的简单操作不足以正确处理它们。为避免这种情况,请为您预先知道可能存在的任何时间序列导出一个默认值,例如 `0`。

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

本页内容