仪表化

此页面提供了一组关于代码仪表化的主观看法指南。

如何进行仪表化

简而言之,就是对所有内容进行仪表化。每个库、子系统和服务都应该至少有一些指标,以便您大致了解其性能。

仪表化应该是您代码的组成部分。在您使用指标类的同一文件中实例化它们。这使得在您追查错误时,从告警到控制台再到代码变得容易。

三种类型的服务

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

在线服务系统

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

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

有关计数失败查询,请参阅下面的“失败”部分。

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

在计算查询时,请保持一致,无论是在查询开始时还是结束时。建议在结束时计算,因为它将与错误和延迟统计数据对齐,并且往往更容易编码。

离线处理

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

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

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

批处理作业

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

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

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

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

子系统

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

库应提供仪表化,而无需用户进行额外的配置。

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

根据库的重量,跟踪库内部的内部错误和延迟,以及您认为可能有用的任何一般统计数据。

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

日志记录

作为一般规则,对于每一行日志记录代码,您还应该有一个递增的计数器。如果您发现一条有趣的日志消息,您希望能够查看它发生的频率和持续时间。

如果在同一函数中有多个密切相关的日志消息(例如,if 或 switch 语句的不同分支),有时可以考虑为所有这些消息递增一个计数器。

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

失败

应该以类似于日志记录的方式处理失败。每次发生失败时,都应递增计数器。与日志记录不同,错误也可能根据您的代码结构向上冒泡到更一般的错误计数器。

在报告失败时,您通常应该有一些其他指标来表示尝试的总次数。这使得失败率易于计算。

线程池

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

缓存

缓存的关键指标是总查询数、命中数、总体延迟,然后是缓存前面的任何在线服务系统的查询计数、错误和延迟。

收集器

在实现非平凡的自定义指标收集器时,建议导出一个仪表盘,用于指示收集花费的时间(以秒为单位),另一个用于指示遇到的错误数。

这是可以将持续时间导出为仪表盘而不是摘要或直方图的两种情况之一,另一种情况是批处理作业持续时间。这是因为两者都表示有关特定推送/抓取的信息,而不是随时间跟踪多个持续时间。

注意事项

在进行监控时,有一些一般事项需要注意,还有一些是 Prometheus 特有的。

使用标签

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

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

例如,与其使用 http_responses_500_total 和 http_responses_403_total,不如创建一个名为 http_responses_total 的单个指标,并为 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 的实现来说太多了。即使数字较小,也会存在机会成本,因为您无法再在此机器上拥有其他可能更有用的指标。

如果您不确定,请从没有标签开始,并随着具体用例的出现而随着时间推移添加更多标签。

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

重要的是要知道对于给定的指标使用四种主要指标类型中的哪一种。

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

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

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

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

时间戳,而不是时间差

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

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

内部循环

总的来说,仪表化带来的额外资源成本远远超过了它给运营和开发带来的好处。

对于性能至关重要或在给定进程中每秒调用超过 10 万次的代码,您可能希望谨慎考虑要更新多少指标。

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

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

避免指标缺失

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

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

本文档是开源的。请通过提交问题或拉取请求来帮助改进它。