代码植入
本页面提供了一套关于如何为您的代码植入监控指标的指导原则。
如何植入
简而言之,就是为所有东西植入监控。每个库、子系统和服务都应该至少有一些指标,以便让您大致了解其性能如何。
代码植入应该是代码中不可或缺的一部分。在您使用指标类的同一个文件中实例化它们。这使得在您追踪错误时,从告警到控制台再到代码变得容易。
三种类型的服务
出于监控目的,服务通常可以分为三种类型:在线服务、离线处理和批处理作业。它们之间有重叠,但每个服务都倾向于很好地归入其中一个类别。
在线服务系统
在线服务系统是指人类或其他系统期望立即得到响应的系统。例如,大多数数据库和 HTTP 请求都属于这一类。
在这种系统中,关键指标是执行的查询数量、错误数和延迟。正在进行的请求数量也可能有用。
关于计算失败的查询,请参阅下面的失败部分。
在线服务系统应在客户端和服务器端进行监控。如果两端看到不同的行为,这对调试来说是非常有用的信息。如果一个服务有许多客户端,服务跟踪它们每一个是不切实际的,所以它们必须依赖自己的统计数据。
在是在查询开始时还是在结束时进行计数,应保持一致。建议在结束时计数,因为它将与错误和延迟统计数据保持一致,并且通常更容易编码。
离线处理
对于离线处理,没有人会主动等待响应,并且通常会进行批量工作。可能还会有多个处理阶段。
对于每个阶段,跟踪进入的项目、正在处理的数量、上次处理某物的时间以及发送出去的项目数量。如果进行批处理,您还应该跟踪进入和出去的批次。
知道系统上次处理某物的时间对于检测它是否已停滞很有用,但这是非常局部化的信息。更好的方法是向系统发送心跳:一个虚拟项目,它会被一直传递,并包含插入时的时间戳。每个阶段都可以导出它所看到的最新心跳时间戳,让您知道项目在系统中传播需要多长时间。对于没有空闲处理时段的系统,可能不需要明确的心跳。
批处理作业
离线处理和批处理作业之间有一条模糊的界线,因为离线处理可能在批处理作业中完成。批处理作业的特点是它们不是连续运行的,这使得采集它们变得困难。
批处理作业的关键指标是它上次成功的时间。跟踪作业的每个主要阶段花费了多长时间、总体运行时间以及作业上次完成的时间(成功或失败)也很有用。这些都是仪表盘(gauge),应该推送到 PushGateway。通常还有一些与作业相关的总体统计数据,跟踪这些数据也很有用,例如处理的记录总数。
对于运行超过几分钟的批处理作业,使用基于拉取的监控来采集它们也很有用。这使您可以像对其他类型的作业一样,随时间跟踪相同的指标,例如与其他系统通信时的资源使用情况和延迟。如果作业开始变慢,这可以帮助调试。
对于运行非常频繁的批处理作业(例如,比每 15 分钟一次更频繁),您应该考虑将它们转换为守护进程,并将其作为离线处理作业来处理。
子系统
除了三种主要类型的服务之外,系统还有一些子部分也应该进行监控。
库
库应提供无需用户额外配置的监控植入。
如果它是一个用于访问进程外部资源的库(例如,网络、磁盘或 IPC),则至少应跟踪总体查询计数、错误(如果可能出现错误)和延迟。
根据库的复杂程度,跟踪库本身的内部错误和延迟,以及您认为可能有用的任何常规统计信息。
一个库可能被应用程序的多个独立部分用于不同的资源,因此请注意在适当的地方使用标签来区分用途。例如,数据库连接池应区分它正在通信的数据库,而没有必要区分 DNS 客户端库的用户。
日志记录
作为一般规则,对于每一行日志代码,您都应该有一个递增的计数器。如果您发现一条有趣的日志消息,您希望能够看到它发生的频率和持续时间。
如果同一函数中有多个密切相关的日志消息(例如,if 或 switch 语句的不同分支),有时为所有这些消息递增一个计数器是有意义的。
通常,导出应用程序整体记录的信息/错误/警告行的总数也很有用,并在发布过程中检查是否有显著差异。
失败
失败应与日志记录类似处理。每次发生失败时,都应递增一个计数器。与日志记录不同,错误也可能会冒泡到一个更通用的错误计数器,具体取决于您的代码结构。
报告失败时,通常应该有另一个表示总尝试次数的指标。这使得失败率很容易计算。
线程池
对于任何类型的线程池,关键指标是排队请求的数量、正在使用的线程数、线程总数、处理的任务数以及它们花费了多长时间。跟踪事情在队列中等待了多长时间也很有用。
缓存
缓存的关键指标是总查询数、命中数、总体延迟,然后是缓存前面的任何在线服务系统的查询计数、错误和延迟。
收集器
在实现非平凡的自定义指标收集器时,建议导出一个仪表盘(gauge)来表示收集花费的时间(以秒为单位),并导出另一个仪表盘来表示遇到的错误数。
这是两种可以将持续时间导出为仪表盘而不是摘要或直方图的情况之一,另一种是批处理作业的持续时间。这是因为两者都表示有关该特定推送/采集的信息,而不是随时间跟踪多个持续时间。
注意事项
在进行监控时,需要注意一些一般性事项,还有一些是 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 个节点,您最终将得到大约 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` 值。