Prometheus 教程

Prometheus 编写客户端库

本文档涵盖 Prometheus 客户端库应提供的功能和 API,目的是使各库保持一致,使简单的用例变得容易,并避免提供可能会将用户引入歧途的功能。

目前,Prometheus 已经支持 10 种语言,因此我们现在已经对如何编写客户端有了很好的了解。本指南旨在帮助新客户端库的作者编写出优秀的库。

约定

在许多标准和规范中,特别是在互联网工程任务组(IETF)的 RFC 文档和许多其他技术文档中常见的关键词,用于描述规范的要求和推荐做法。这些关键词具有特定的含义,用于帮助读者理解规范的严格性和灵活性。

  • MUST:这是一个绝对的要求。当规范中说“某个实现MUST做某件事”时,这意味着实现必须遵守这一要求,没有任何选择或例外。

  • MUST NOT:这也是一个绝对的要求,但它是禁止性的。当规范中说“某个实现MUST NOT做某件事”时,这意味着实现绝对不能做这件事,否则它将不符合规范。

  • SHOULD:这是一个建议或推荐。当规范中说“某个实现SHOULD做某件事”时,这意味着虽然这不是一个强制性的要求,但强烈建议实现这样做,因为它通常被认为是一个好的做法或可以提高系统的互操作性。

  • SHOULD NOT:与 SHOULD 相反,这是一个不推荐的做法。当规范中说“某个实现SHOULD NOT做某件事”时,这意味着虽然实现可以做这件事,但通常不建议这样做,因为它可能会导致问题或降低系统的互操作性。

  • MAY:这是一个选择性的要求。当规范中说“某个实现MAY做某件事”时,这意味着实现可以选择是否做这件事,没有任何强制性的要求。

这些关键词在编写和理解技术规范和标准时非常有用,因为它们提供了关于要求严格性和灵活性的明确指导。

更多信息参考 https://www.ietf.org/rfc/rfc2119.txt 文档。

此外,ENCOURAGED(鼓励)是指一个库最好具备某个功能,但不具备也没关系。换句话说,就是 "最好有"。

需要注意的事项:

  • 利用每种语言的特点。

  • 常见用例应简单易行。

  • 正确的方法应该是简单的方法。

  • 可以使用更复杂的用例。

常见的用例如下(按顺序排列):

  • 在库/应用程序中随意散布的无标签计数器。

  • 摘要/一览表中的计时函数/代码块。

  • 跟踪事物当前状态(及其限制)的仪表。

  • 监控批处理工作。

整体结构

客户端的编写必须(MUST)基于内部回调,客户端一般应遵循此处描述的结构。

关键类是 Collector,它有一个方法(通常称为 "collect"),可返回零个或多个度量指标及其样本。Collectors 在 CollectorRegistry 注册。通过将 CollectorRegistry 传递给类/方法/函数 "bridge" 来公开数据,"bridge" 会以 Prometheus 支持的格式返回度量值。每次抓取 CollectorRegistry 时,它必须回调到每个收集器的 collect 方法。

与大多数用户交互的界面是 Counter、Gauge、Summary 和 Histogram 收集器(Collectors)。这些收集器代表单一指标,可满足用户对自己的代码进行检测的绝大多数使用情况。

更高级的用例(如从其他监控/仪表系统代理)需要编写自定义 Collector。有些人可能还想编写一个 "bridge",利用 CollectorRegistry 生成不同监控/仪器系统可理解格式的数据,让用户只需考虑一个仪器系统。

CollectorRegistry 应该提供 register()/unregister() 函数,并允许一个 Collector 注册到多个 CollectorRegistry。

客户端库必须是线程安全的。

对于非 OO 语言(如 C 语言),客户端库应尽可能遵循本结构的精神。

命名

客户端库应遵循本文档中提到的函数/方法/类名,同时牢记所使用语言的命名约定。例如,在 Python 中,set_to_current_time() 是很好的方法名称,但在 Go 中,SetToCurrentTime() 更好,而在 Java 中,setToCurrentTime() 是约定俗成的名称。当名称因技术原因(如不允许函数重载)而不同时,文档/帮助字符串应将用户引向其他名称。

库不得提供与此处给出的名称相同或相似,但语义不同的函数/方法/类。

指标(Metrics)

计数器(Counter)、仪表(Gauge)、摘要(Summary)和直方图(Histogram)度量类型是用户的主要界面。

计数器和仪表必须是客户端库的一部分,必须提供摘要和直方图中的至少一种。

这些变量应主要作为文件静态变量使用,即在与所使用的代码相同的文件中定义的全局变量。客户端库应启用此功能。常见的用例是对一段代码进行整体检测,而不是在一个对象实例的上下文中对一段代码进行检测。用户不必担心在整个代码中使用度量指标,客户端库应该为他们做到这一点(如果客户端库做不到这一点,用户就会在客户端库周围编写一个包装器来使其 "更容易",这种情况很少发生)。

必须有一个默认的 CollectorRegistry,标准度量指标必须在默认情况下隐含地注册到其中,用户无需做任何特殊工作。必须有办法让度量指标不注册到默认的 CollectorRegistry,以便在批处理作业和单元测试中使用。自定义收集器也应遵循这一规定。

创建度量的具体方式因语言而异。对某些语言(Java、Go)来说,最好采用生成器方法,而对其他语言(Python)来说,函数参数足够丰富,只需一次调用即可完成。

例如,在 Java Simpleclient 中,我们有:

class YourClass {
  static final Counter requests = Counter.build()
      .name("requests_total")
      .help("Requests.").register();
}

这将向默认的 CollectorRegistry 注册请求。通过调用 build() 而不是 register() 方法,度量指标将不会被注册(方便未测试),也可以向 register() 传递一个 CollectorRegistry(方便批处理工作)。

计数器(Counter)

计数器是一个单调递增的计数器。它不允许数值减少,但可以重置为 0(例如通过服务器重启)。

计数器必须具有以下方法:

  • inc(): 将计数器递增 1

  • inc(double v): 将计数器递增给定值。必须检查 v >= 0。

鼓励使用计数器:

一种对给定代码中抛出/引发的异常进行计数的方法,可以只对某些类型的异常进行计数。这就是 Python 中的 count_exceptions。

计数器必须从 0 开始。

仪表(Gauge)

仪表表示一个可以上下波动的数值。

仪表必须具有以下方法:

  • inc(): 将量规值递增 1

  • inc(double v): 按给定的数值递增仪表

  • dec(): 将仪表减小 1

  • dec(double v): 按给定值减小仪表

  • set(double v): 将仪表设置为给定值

仪表必须从 0 开始,您也可以为给定的仪表提供从不同数字开始的方法。

仪表应该具有以下方法:

  • set_to_current_time(): 将仪表设置为当前的 unixtime(以秒为单位)。

鼓励使用仪表盘:

在某些代码/函数中跟踪进行中请求的方法。这就是 Python 中的 track_inprogress。

为一段代码计时,并将仪表盘设置为以秒为单位的持续时间。这对批处理作业很有用。这就是 Java 中的 startTimer/setDuration 和 Python 中的 time() 装饰器/上下文管理器。这应该与 Summary/Histogram 中的模式相匹配(尽管是 set() 而不是 observe())。

摘要(Summary)

摘要对滑动时间窗口中的观察结果(通常是请求持续时间等)进行采样,并提供对其分布、频率和总和的即时了解。

摘要不得允许用户将 "量化值" 设置为标签名称,因为内部使用该名称来指定摘要的量化值。鼓励摘要提供量化值作为导出,尽管这些量化值无法聚合且速度较慢。摘要必须允许不使用量化值,因为 _count/_sum 非常有用,而且必须作为默认值。

摘要必须具有以下方法:

  • observe(double v): 观察给定的数量

摘要应具备以下方法:

以秒为单位为用户计时的方法。在 Python 中,这是 time() 装饰器/上下文管理器。在 Java 中是 startTimer/observeDuration。不得提供除秒以外的其他单位(如果用户需要其他单位,可以自己手动设置)。这应遵循与 Gauge/Histogram 相同的模式。

摘要 _count/_sum 必须从 0 开始。

直方图(Histogram)

直方图允许对事件(如请求延迟)进行汇总分布,其核心是每个桶的计数器。

直方图不允许将 le 作为用户设置的标签,因为 le 在内部用于指定桶。

直方图必须提供手动选择桶的方法。应提供 linear(start, width, count) 和 exponential(start, factor, count) 方式设置桶的方法,计数必须包括 +Inf 桶。

直方图应具有与其他客户端库相同的默认数据桶,一旦创建了度量值,就不能更改数据桶。

直方图必须具有以下方法:

  • observe(double v): 观察给定的数量

直方图应具有以下方法:

以秒为单位为用户计时的方法。在 Python 中,这是 time() 装饰器/上下文管理器。在 Java 中是 startTimer/observeDuration。不得提供除秒以外的其他单位(如果用户需要其他单位,可以自己手动设置)。这应遵循与 Gauge/Summary 相同的模式。

直方图 _count/_sum 和桶必须从 0 开始。

标签(Labels)

标签是 Prometheus 最强大的功能之一,但也很容易被滥用。因此,客户端库在向用户提供标签时必须非常谨慎。

客户端库不得允许用户为 Gauge/Counter/Summary/Histogram 或库提供的任何其他 Collector 的同一度量指标使用不同的标签名称。

来自自定义收集器的度量指标几乎都应具有一致的标签名称。在极少数有效用例中,情况并非如此,因此客户库不应验证这一点。

虽然标签功能强大,但大多数度量指标都没有标签。因此,应用程序接口应允许使用标签,但不应强制使用。

客户端库必须允许在创建 Gauge/Counter/Summary/Histogram 时选择性地指定标签名称列表。客户端库应支持任意数量的标签名称。客户机程序库必须验证标签名称是否符合文档要求。

访问度量标注维度的一般方法是通过 labels() 方法,该方法接收一个标注值列表或一个从标注名称到标注值的映射,并返回一个 "Child"。然后就可以在子方法上调用常用的 .inc()/.dec()/.observe() 等方法。

由 labels() 返回的 "子节点 "应该可以被用户缓存,以避免再次查找--这在对延迟要求很高的代码中很重要。

带有标签的度量标准应当支持与 labels() 具有相同签名的 remove() 方法,该方法将从不再输出该标签的度量标准中删除子节点,而 clear() 方法则会从度量标准中删除所有子节点。这些方法会使子节点的缓存失效。

应该有办法用默认值初始化给定的子节点,通常只需调用 labels()。没有标签的度量值必须始终初始化,以避免出现度量值丢失的问题。

度量名称

度量名称必须符合规范。与标签名称一样,在使用 auge/Counter/Summary/Histogram 以及库中提供的任何其他收集器时,也必须遵守这一规定。

许多客户端库提供三部分名称设置:namespace_subsystem_name,其中只有 name  是必须的。

必须避免使用动态/生成的度量名称或度量名称的子部分,除非自定义收集器是从其他仪器/监控系统代理而来。生成/动态度量名称表明您应该使用标签。

度量说明和帮助

Gauge/Counter/Summary/Histogram 必须要求提供度量描述/帮助。

客户端库中提供的任何自定义 Collectors 都必须在其度量指标上提供说明/帮助。

建议将其作为一个强制参数,但不要检查其长度,因为如果有人真的不想写文档,我们也不会说服他们。库中提供的 Collectors(以及生态系统中我们可以提供的任何地方)都应具有良好的度量描述,以起到表率作用。

说明

客户必须实现 exposition formats 文档中概述的基于文本的 exposition 格式。

如果可以在不耗费大量资源的情况下实现,则鼓励对暴露的指标进行可复制的排序(特别是对于人类可读的格式)。

标准和运行时收集器

客户库应尽可能提供标准导出(Exporter),如下文所述。

这些应该被实现为自定义 Collectors,并在默认情况下在默认的 CollectorRegistry 中注册。应该有一种方法来禁用它们,因为有一些非常小众的用例,它们会妨碍使用。

进程度量

这些指标的前缀是 process_。如果在所使用的语言或运行时中获取必要的值存在问题或甚至不可能,客户端库应优先选择不输出相应的度量值,而不是输出虚假、不准确或特殊的值(如 NaN)。所有内存值单位均为字节,所有时间单位均为 unixtime/秒。

指标名称

帮助信息

单位

process_cpu_seconds_total

用户和系统CPU总时间(以秒为单位)。

seconds

process_open_fds

打开的文件描述符数。

file descriptors

process_max_fds

打开的文件描述符的最大数量。

file descriptors

process_virtual_memory_bytes

以字节为单位的虚拟内存大小。

bytes

process_virtual_memory_max_bytes

可用的最大虚拟内存量(以字节为单位)。

bytes

process_resident_memory_bytes

驻留内存大小 (以字节为单位)。

bytes

process_heap_bytes

处理堆大小(以字节为单位)。

bytes

process_start_time_seconds

自 unix 纪元以来进程的开始时间(以秒为单位)。

seconds

process_threads

进程中的操作系统线程数。

threads

运行时指标

此外,我们鼓励客户库也为其语言的运行时提供任何有意义的指标(如垃圾回收统计),并使用适当的前缀,如 go_、hotspot_ 等。

单元测试

客户端库应具有涵盖核心仪器库和exposition的单元测试。

鼓励客户端库提供各种方法,使用户能够轻松地对使用仪器代码的情况进行单元测试。例如,Python 中的 CollectorRegistry.get_sample_value。

打包和依赖关系

理想情况下,客户端库可以包含在任何应用程序中,在不破坏应用程序的情况下添加一些仪器。

因此,在客户端库中添加依赖关系时应谨慎。例如,如果添加的库使用了需要 x.y 版本库的 Prometheus 客户端,但应用程序在其他地方使用了 x.z,这会对应用程序产生不利影响吗?

建议在可能出现这种情况时,将核心仪器与桥接/以给定格式显示度量值分开。例如,Java simpleclient 的 simpleclient 模块没有依赖关系,而 simpleclient_servlet 具有 HTTP 位。

性能考虑因素

由于客户端库必须是线程安全的,因此需要某种形式的并发控制,而且必须考虑多核机器和应用程序的性能。

根据我们的经验,性能最低的是互斥。

处理器原子指令往往处于中间位置,一般可以接受。

避免不同 CPU 更改相同 RAM 位的方法效果最好,例如 Java simpleclient 中的 DoubleAdder。不过,这需要付出内存代价。

如上所述,labels() 的结果应该是可缓存的。支持带标签度量的并发映射往往相对较慢。对不带标签的度量进行特殊处理,以避免类似 labels() 的查找,会有很大帮助。

度量值在递增/递减/设置等过程中应避免阻塞,因为当抓取正在进行时,整个应用程序被阻塞是不可取的。

鼓励对包括标签在内的主要仪器操作进行基准测试。

资源消耗,尤其是 RAM,在进行阐述时应牢记。考虑通过流式处理结果来减少内存占用,并对并发抓取的数量进行限制。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号