编写客户端库
本文涵盖了 Prometheus 客户端库应提供的功能和 API,旨在实现不同库之间的一致性,使常见用例更易于使用,并避免提供可能导致用户误入歧途的功能。
到撰写本文时,已有 10 种语言具有客户端库,因此我们已经对如何编写客户端有了很好的了解。这些指南旨在帮助新客户端库的作者生成高质量的客户端库。
常规约定
“必须”、“必须不”、“应该”、“应该不”、“可以”分别具有在 https://www.ietf.org/rfc/rfc2119.txt 中给出的含义。
此外,“鼓励”意味着一个功能对于库来说是理想的,但如果不存在也没关系。换句话来说,这是一个可有可无的功能。
要记住的事情:
- 充分利用每种语言的特性。
- 常见用例应该易于实现。
- 正确执行某件事应该是容易做到的。
- 更复杂的用例也应该能够实现。
常见的用例按顺序如下:
- 不带标签的 Counter 广泛分布在库/应用程序中。
- 在 Summaries/Histograms 中对函数/代码块进行计时。
- 监控当前事物的状态及其限制的指标。
- 批处理作业的监控。
总体结构
客户端库在内部必须使用函数回调的方式编写。客户端库应遵循此处描述的大致结构。
关键类是 Collector(收集器)。它有一个方法(通常称为collect
),用于返回零个或多个指标及其样本。Collector 可以通过被注册到 CollectorRegistry 来获取。数据通过将 CollectorRegistry 传递给一个“桥接器”类/方法/函数暴露出来,该桥接器以 Prometheus 支持的格式返回指标。当每次收集 CollectorRegistry 时,都必须调用每个 Collector 的 collect 方法。
用户最常交互的接口是 Counter、Gauge、Summary 和 Histogram Collector。这些代表单个指标,并且应该覆盖大多数用户在其代码中进行注入的用例。
更高级的用例(例如从另一个监控/仪表化系统代理)需要编写自定义Collector。某些人可能还想编写一个“桥接器”,它接受 CollectorRegistry 并产生不同监控/仪表化系统理解的数据格式,从而使用户只需考虑一种监控系统。
CollectorRegistry 应该提供register()
/unregister()
函数,而 Collector 应该允许被多个 CollectorRegistry 注册。
客户端库必须线程安全。
对于无法使用面向对象(OO)语言如 C 的客户端库,应尽可能遵循此处结构。
命名约定
客户端库应遵循本文档中提到的函数/方法/类名称,同时考虑到所使用的语言的命名约定。例如,在 Python 中,set_to_current_time()
是一个良好的方法名称,但在 Go 中,SetToCurrentTime()
更好,而在Java中,setToCurrentTime()
则更符合惯例。如果由于技术原因导致名称不同(例如不允许函数重载),应该给出文档/帮助字符串(Help string)指向其他名称。
客户端库不得提供与这里给出的名称相同或相似但具有不同的语义的函数/方法/类。
指标
Counter、Gauge、Summary 和 Histogram 指标类型是用户依赖的主要接口。
Counter 和 Gauge 必须包含在客户端库中,而 Summary 和 Histogram 两个中至少一个必须被提供。
这些指标变量主要应用于作为静态文件变量使用,即定义在它们所监控的代码所在的同一文件中的全局变量。客户端库应该支持这种使用方式。最常见的用例是整体监控一段代码,而不是在单个对象实例的上下文中监控一段代码。用户不应该关心如何在代码中传输他们的指标,客户端库应该为处理好背后的细节(如果它没有,用户将需要编写一个围绕库的包装器使其“更容易”被传输)。
客户端库必须有一个默认的 CollectorRegistry,标准指标默认隐式地被注册到其中,无需用户进行特别的操作。必须有一种方式可以让指标不注册到默认的 CollectorRegistry ,以便用于批处理作业和单元测试。自定义 Collector 也应该遵循这种做法。
根据语言的不同,指标的创建方式会有所不同。对于一些(Java、Go)而言,Builder 模式是最好的,而对于其他(Python)而言,仅仅使用函数参数就足够了。
例如在 Java 的 Simpleclient 中:
这将使用默认的 CollectorRegistry 注册 requests。通过调用build()
而不是register()
,指标将不会被注册(适用于单元测试),也可以通过将 CollectorRegistry 传递给register()
(适用于批处理作业)。
Counter
Counter 是一个单调递增的计数器。它不能允许值减少,但是可以重置为 0(例如,服务器重启)。Counter 必须具备以下方法:
inc()
: 增加Counter 1inc(double v)
: 增加给定的数量。必须保证 v >= 0。
我们鼓励 Counter 具备:
一种方式来计算代码中抛出/引发的异常数量,以及可选地只计算特定类型的异常。例如 Python 中 的count_exceptions
。
Counter 必须从 0 开始。
Gauge
Gauge 表示可以上涨和下降的值。
Gauge 必须具备以下方法:
inc()
: 增加 Gauge 1inc(double v)
: 通过给定的数量增加 Gaugedec()
: 减少 Gauge 1dec(double v)
: 通过给定的数量减少 Gaugeset(double v)
: 将 Gauge 设置为给定的值
Gauge 必须从 0 开始,你可以提供一种方式让给定的 Gauge 从不同的数字开始。
Gauge 应该具备以下方法:
set_to_current_time()
: 将 Gauge 设置为当前 Unix 时间(秒)。
我们鼓励 Gauge 具备这些能力:
- 某些代码/函数中跟踪正在进行的请求。例如 Python 中的
track_inprogress
。 - 对代码进行计时并将其持续时间(秒)设置为 Gauge。这对于批处理作业来说很有用。比如 Java 中的
startTimer/setDuration
,以及 Python 中的time()
装饰器/上下文管理器。这应该遵循和 Summary/Histogram 相同的模式。
Summary
Summary 通过滑动时间窗口采样观察(通常是请求延迟之类的事物)并提供即时洞察其分布、频率和总和的方法。
Summary 不能允许用户设置“quantile”作为标签名称,因为这用于内部指定 Summary 量化值。Summary 鼓励提供量化值作为导出,尽管这些不能聚合且往往较慢。Summary 必须不强制包含量化值,因为即使只有_count
和_sum
,Summary 就非常有用,而这也是默认值。
Summary 必须具备以下方法:
observe(double v)
: 观测给定的数量
Summary 应该具备以下方法:
- 让用户以秒为单位的时间代码。在 Python 中这是
time()
装饰器/上下文管理器。在 Java 中这是startTimer/observeDuration
。除了秒之外不应提供单位(如果用户想要其他单位,他们可以通过手动操作)。这应该遵循 Gauge/Summary 的模式。
Summary 的_count
和_sum
必须从0开始。
Histogram
Histogram 可以进行聚合事件分布,如请求延迟。它本质上是一个有多个桶的 Counter。
Histogram 不能允许le
作为用户设置的标签,因为le
用于在其内部指定桶。
Histogram 必须提供手动选择桶的方式。以linear(start, width, count)
和exponential(start, factor, count)
形式设置桶的方式应该被支持。计数必须包括+Inf
桶。
Histogram 应该采用其他客户端库的默认桶。桶在创建后不得更改。
Histogram 必须具备以下方法:
observe(double v)
: 观测给定的量
Histogram 应该具备以下方法:
一种方式让用户对代码运行进行计时的代码。在 Python 中这是time()
装饰器/上下文管理器。在 Java 中这是startTimer
/observeDuration
。除了秒之外不应提供单位(如果用户想要其他单位,他们可以通过手动操作)。这应该遵循 Gauge/Summary 的模式。
Histogram 的_count
、_sum
和桶必须从 0 开始。
更进一步的思考
如果足够合理,则我们鼓励提供上述的指标类型之外的指标功能。
如果存在可以简化常见用例的做法,只要它不会鼓励不良行为(例如不理想的指标/标签布局,或者在客户端执行指标计算等),则值得一试。
标签
标签是 Prometheus 最强大的特性之一,但也很容易被滥用。因此,客户端库在向用户提供标签时必须非常小心。
客户端库应禁止允许用户为同一指标使用相同类型的 Counter /Gauge/Summary/Histogram 或其他由库提供的 Collector 的不同的标签名称。
来自自定义 Collector 的指标几乎总是应该具有一致的标签名称的。虽然会存在少数但有效的例外,但客户端库不需要考虑这一点。
虽然标签非常强大,但大多数指标通常不会带有标签。因此,即使 API 应允许用户使用标签,但不应鼓励标签的使用。
客户端库必须允许在创建 Gauge/Counter/Summary/Histogram 时可选地指定标签名称列表。客户端库应支持任意数量的标签名。客户端库必须保证标签名称符合要求。
提供标签访问的一般方式是提供labels()
方法,该方法接受标签值的列表或标签名到标签值的映射,并返回一个“子对象”。然后用户可以调用子对象上的.inc()
/.dec()
/.observe()
等常规方法。
通过labels()
返回的子对象应该由用户缓存,以避免再次查找 - 这在对延迟敏感的关键代码中很重要。
带有标签的指标应该支持与labels()
相同的签名的remove()
方法,该方法将从不再观测它的指标中移除子对象,并支持通过clear()
方法来移除指标的所有子对象。这些方法会使子对象的缓存失效。
客户端库应该提供一种初始化给定子对象的方式,通常是直接调用labels()
。没有标签的指标始终应该被初始化以避免缺失指标问题。
指标名称
指标名称必须遵循规范。如同标签名称一样,这必须适用于 Gauge/Counter/Summary/Histogram 以及库提供的任何其他 Collector 的使用。
许多客户端库提供了在三个部分设置名称的方式:namespace_subsystem_name
,其中只有name
是必需的。
动态/生成式(Generated)指标名称或名称的子部分必须被严格禁止,除非当自定义 Collector 代理来自其他监控系统的指标时。如果你想使用动态/生成式指标名称,那这表明你应该转而使用标签。
指标描述和帮助
对于 Gauge/Counter/Summary/Histogram,必须提供指标描述/帮助。
任何由客户端库提供的自定义 Collector 都必须为其指标提供描述/帮助。
我们建议将帮助信息作为必填参数,但不必检查长度是否符合一定的要求。因为如果有人真的不想写文档,我们也无法说服他们。
暴露指标
客户端必须实现文本形式的展示格式,如暴露格式文档中概述的那样。
如果可以实现而无需显著资源成本,则推荐有序暴露指标(特别是在可读格式中)。
标准和运行时 Collector
客户端库应该尽可能提供标准 Exporter,下文会有详细说明。
这些 Exporter 应该被视为自定义的 Collector 实现,并默认注册到默认 CollectorRegistry 上。库应该提供一种方式禁用这些 Exporter,因为有些非常特殊的用例可能会受到干扰。
进程指标
这些指标具有前缀process_
。如果获取必要的值存在问题甚至不可能时,或者在所使用的语言或运行时环境中是不可能的,客户端库应该优先于暴露错误、不准确或特殊值(如NaN
)而选择不暴露相应的指标。所有内存值单位都是字节,所有时间单位都是 unixtime/秒。
指标名称 | 帮助字符串 | 单位 |
---|---|---|
process_cpu_seconds_total | 总用户和系统 CPU 时间以秒为单位花费的时间。 | 秒 |
process_open_fds | 打开的文件描述符的数量。 | 文件描述符 |
process_max_fds | 最大打开的文件描述符数量。 | 文件描述符 |
process_virtual_memory_bytes | 虚拟内存大小,以字节为单位。 | 字节 |
process_virtual_memory_max_bytes | 可用的最大虚拟内存量,以字节为单位。 | 字节 |
process_resident_memory_bytes | 驻留内存大小,以字节为单位。 | 字节 |
process_heap_bytes | 进程堆大小,以字节为单位。 | 字节 |
process_start_time_seconds | 自 Unix 纪元以来进程开始时间,以秒为单位。 | 秒 |
process_threads | 进程中的 OS 线程数量。 | 线程 |
运行时指标
此外,客户端库鼓励为语言的运行时环境提供有意义的指标,例如垃圾回收统计信息,并附上适当的前缀,如go_
、hotspot_
等。
单元测试
客户端库应该包含覆盖核心监控库和指标暴露的单元测试。
我们鼓励客户端库为用户提供轻松地对监控代码进行单元测试的方法。例如,Python 中的CollectorRegistry.get_sample_value
。
打包与依赖关系
理想情况下,客户端库可以被包含在任何应用程序中,以添加一些指标的监控,同时不会破坏应用程序本身的依赖关系。
因此,在向客户端库添加依赖项时应谨慎行事。例如,如果在应用程序的其他部分使用了版本 x.z 的库的客户端,但添加了一个需要版本 x.y 的库的客户端,这是否会对应用程序产生负面影响呢?
我们建议在可能的情况下,将核心的指标监控与特定的指标的指标暴露分开。例如,Java 简单客户端simpleclient
模块没有任何依赖项,而simpleclient_servlet
则包含了 HTTP 相关的部分。
性能考虑
由于客户端库必须是线程安全的,所以需要某种并发控制,并且需要考虑到多核机器和应用上的性能。
根据我们的经验,最不具性能优势的是互斥锁(mutexes)。
处理器原子指令通常处于中间位置。
避免不同 CPU 修改相同的内存位的方法表现最佳,例如 Java 简单客户端中的 DoubleAdder。不过,这也额外引入了一定的内存成本。
如上所述,labels()
的结果应该是可缓存的。使用标签支持指标的并发哈希表往往相对较慢。库应该专门处理没有标签的指标,以避免类似于labels()
的查找,这可以帮助提高性能。
指标应当避免在进行递增/递减/设置等操作时阻塞整个应用程序,因为这不利于应用程序在采集过程中保持正常运行。
我们鼓励对主要的代码注入操作(包括标签操作)进行基准测试。
在进行指标暴露时,应考虑资源消耗,尤其是 RAM 的消耗。通常我们可以通过流式传输结果来减少内存占用,并可设定同时进行采集的最大数量限制。
该文档基于 Prometheus 官方文档翻译而成。