编写 Exporter
如果你正在自定义 Exporter 代码,请遵循使用 Prometheus 客户端库进行仪表化的一般规则。当从另一个监控或指标系统获取指标时,事情往往不是那么黑白分明。
本文档包含在编写 Exporter 或自定义 Collector 时需要考虑的事情,其中所涵盖的理论对直接仪表化(direct instrumentation)也很有帮助。
如果你在编写 Exporter 时对这里的内容有任何疑问,请通过 IRC(libera 上的 #prometheus)或邮件列表联系我们。
可维护性和纯度
编写 Exporter 时的主要考量是你愿意投入多少工作以获得完美的指标。
如果目标系统只有少数很少变化的指标,那么获得完美指标是一个容易的选择,haproxy_exporter 是一个不错的例子。
另一方面,如果尝试在系统中获得完美指标,该系统具有数百个频繁更改的指标,新版本带来了新的指标,那就代表你作出了持续提供服务的保证。这也就是 MySQL_exporter 所做的。
node_exporter 是一个混合体,复杂性因模块而异。例如,mdadm
Collector 手动解析文件并暴露专门用于该 Collector 的指标,因此我们可以确保指标正确无误。对于 meminfo
Collector,它的结果取决于内核版本,所以我们需要进行转换以创建广泛有效的指标。
配置
与应用程序交互时,应力求 Exporter 无需应用程序路径之外的其他自定义配置即可正常运行。你可能还需要提供过滤某些指标的能力,这些指标在大型基础设施上可能粒度太细或过于昂贵,例如 haproxy_exporter 允许过滤服务器统计信息。同样,你也可以使将昂贵指标默认禁用。
与其他监控系统、框架和协议交互时,你通常需要提供额外的配置或自定义以生成适用于 Prometheus 的指标。在最好的情况下,监控系统具有与 Prometheus 相似的数据模型,你可以自动确定如何转换指标。cloudwatch_exporter 和 collectd_exporter 就是类似的工具,它们只需要用户自定义他们想要抓取的指标即可。
在其他情况下,系统中的指标并非完全符合标准,具体取决于系统的用途及其底层应用程序。在这种情况下,用户需要告诉 Prometheus 如何转换指标。jmx_exporter 算是最坏的一种情况,graphite_exporter 和 statsd_exporter 也需要用户进行转换来提取标签。
我们建议你确保 Exporter 在无需配置的情况下也能正常工作,并在需要时提供转换示例配置。
YAML 是 Prometheus 配置格式的标准,所有配置都应该使用 YAML。
指标
命名
遵循指标命名的最佳实践。
通常,指标名称应该让熟悉 Prometheus 但不熟悉特定系统的人员能够大致猜出指标的意义。名为http_requests_total
的指标用处并不大 - 这些指标是在它们进入用户代码时测量的吗?还是在某种过滤器中?而requests_total
就更糟了,这究竟是什么类型的请求?
直接进行仪表化时,给定的指标应该只存在于一个文件中。因此,在 Exporter 和 Collector 中,一个指标应该恰好应用于一个子系统,并且名称应该与此对应。
指标名称绝不能程序生成,除非你在编写自定义 Collector 或 Exporter。
应用程序的指标通常应该以 Exporter 名称前缀的形式出现,例如haproxy_up
。
指标必须使用基本单位(例如秒、字节),并将可读单位的转换工作留给绘图工具。无论最终使用什么单位,指标名称中的单位都必须匹配实际使用的单位。类似地,应该暴露比率(ratio)而不是百分比(percentage)。或者为每个组成比率的两个组件分别提供 Counter,这样更好
指标名称不应包含它们随附的标签,例如by_type
,因为这在聚合时将不再具有意义。一种例外的情况是当你通过多个指标以不同标签导出相同的数据时,此时这通常是区分它们最理智的方式来。对于直接仪表化,这仅在导出单个带有所有标签的指标会具有过高的基数时才会出现。
Prometheus 指标和标签名称使用 snake_case。将 camelCase 转换为 snake_case 是可取的,但有时对于像myTCPExample
或isNaN
这样的东西,自动执行并不能总是产生良好的结果,还是最好保持原样。
暴露的指标不应包含冒号,这被保留用于用户自定义的记录规则进行聚合得到的结果。
指标名称只允许包含[a-zA-Z0-9:_]
。
_sum
、_count
、_bucket
和_total
后缀用于 Summary、Histogram 和 Counter。除非你正在生成这些指标之一,否则避免这些后缀。
_total
是 Counter 的约定,如果你使用 Counter 指标类型,请使用这个后缀。
process_
和scrape_
前缀被保留。如果它们遵循匹配语义,可以在这些前缀上添加自己的前缀。例如,Prometheus 有一个scrape_duration_seconds
来表示抓取花了多长时间,也有一个以 Exporter 为中心的指标jmx_scrape_duration_seconds
,表示特定 Exporter 完成其任务所需的时间。对于需要访问 PID 的进程指标,Go 和 Python 提供了处理此类问题的 Collector。一个很好的例子是 haproxy_exporter。
当你需要成功请求计数和失败请求计数指标时,最明智的方式是将总请求作为单独的指标导出,并将另一个指标用于失败请求计数。这样我们就能够很容易地计算出失败率。不要使用带有失败或成功的标签的单个指标!同样,对于缓存的击中或未命中,最好有一个总计指标和一个击中指标。
考虑到有人会在监控时搜索指标名,如果名称非常标准化且不太可能在那些习惯使用这些名称的人之外使用(例如 SNMP 和网络工程师),则保留原样可能也是一个明智之举。然而,这个逻辑并不适用于所有 Exporter,例如 MySQL_exporter 的指标可能会被各种人使用,而不仅仅是数据库管理员。提供原始名称的帮助字符串(Help String)可以获得与使用原始名称相同的大部分优势。
标签
请阅读关于标签的一般建议。
避免使用type
作为标签名称,它过于通用且往往没有意义。在可能的情况下,应尽量避免使用可能会与 Target 标签冲突的名称,例如region
、zone
、cluster
、availability_zone
、az
、datacenter
、dc
、owner
、customer
、stage
、service
、environment
和env
。然而,如果这是应用程序调用资源的方式,最好避免混淆并重命名它们。
避免仅仅因为某些事物共享前缀就将其放入一个指标中,除非你确定作为单个指标有意义,否则使用多个指标通常更为安全。
标签le
对 Histogram 具有特殊含义,quantile
对 Summary 具有特殊含义。所以,在一般情况下,请避免这些标签。
读写和发送/接收最好作为分开的指标,而不是仅仅使用标签区分。这是因为有些时候你只关心其中的一个,而且,将他们作为独立的指标使用起来更方便。
经验规则表明,一个指标应该在求和或平均时有意义。但是,还有一种另一种常见情况。如果对于 Exporter 而言,直接对该指标进行求和或平均没有意义,那么这就要求用户通过正则表达式对指标名称进行操作才能继续使用。考虑一下主板上的电压传感器(主板上有多个电压传感器),在它们之间做数学运算毫无意义,所以把不同传感器的电压值放到同一个指标比分开它们更有意义。指标内的所有值(几乎)总是应该具有相同的单位,例如,如果你混合了风扇速度与电压,还没有自动分离它们的方法。
不要这样做:
或者不要这样做:
前者对于执行你的指标的sum()
操作的人而言是不可行的,后者破坏了求和功能,并且很难处理。一些客户端库,例如 Go,会主动尝试阻止你在自定义 Collector 中执行后者。实际上,所有客户端库都应该阻止你通过直接仪表化执行后者。永远不要这样做,你应该依赖 Prometheus 的聚合方法。
如果你的监控程序暴露了一个总和指标,那么请舍弃它。如果必须保留它,例如总和包括未单独计数的事物,请使用不同的指标名称。
仪表化标签应尽可能简洁,每多一个标签,用户在编写 PromQL 时就需要考虑更多因素。因此,避免使用有可能被删除而不会影响时间序列唯一性的标签。指标的额外信息可以通过信息指标来添加,下面是一个如何处理版本号的示例。
然而,存在某些场景,几乎所有使用该指标的用户都希望获取额外的信息。在这种情况下,添加一个非唯一的标签,而不是信息指标,才是正确的解决方案。例如,mysqld_exporter 的mysqld_perf_schema_events_statements_total
的 digest
标签是对完整查询模式的哈希,满足唯一性。然而,如果没有可读的digest_text
标签(不具有唯一性),mysqld_perf_schema_events_statements_total
仅包含查询模式的开始部分,对于长查询几乎毫无作用。因此,我们最终同时保留了可读的digest_text
标签和用于唯一性的digest
标签。
Target 标签,而非静态抓取的标签
如果你发现自己想要将相同的标签应用于所有指标,请停止这么做。
通常有两种情况会出现这种需求。
第一种情况是某些标签在指标上比较有用,例如软件的版本号。相反,我们可以采用 https://www.robustperception.io/how-to-have-labels-for-machine-roles/ 中描述的方法。
第二种情况是一个标签实际上是一个 Target 标签。这些信息是来自基础设施而不是应用程序本身,如区域、集群名称等。应用程序不应决定其在这些基础设施相关的标签,这应该是由 Prometheus 服务器的维护人员配置的,并且监控同一应用程序的不同人员可能会给它不同的名称。
因此,这些标签应位于 Prometheus 的抓取配置中,通过你正在使用的服务发现机制。在这里也可以应用机器角色的概念,因为它对于至少某些抓取它的人员来说可能是有用的信息。
类型
应尝试将你的指标类型匹配到 Prometheus 类型。这通常意味着 Counter 和 Gauges。Summary 的count
和sum
也相当常见,<font style="color:rgb(51, 51, 51);">quantiles</font>
较为少见。Histograms 很少会被用到,如果你遇到了,请记住按照格式来暴露累积值。
不清楚指标类型的情况经常发生,尤其是在你自动处理一组指标时。一般来说,UNTYPE
是一个安全的默认值。
Counter 不能减少,所以如果你从其他监控系统获取 Counter 类型的指标的时候,得到了可以递减的 Counter,则这说明它不是一个 Counter,而是一个 Gauge。在这种情况下,UNTYPED
可能是更好的类型选择,因为如果被用作Counter
或者GAUGE
会造成误导。
帮助字符串(Help strings)
在转换指标时,如果能够让用户回溯原始状态以及找到导致转换的规则,这将对用户而言很有帮助。将 Collector 或 Exporter 的名称、应用于任何规则的 ID、原始指标的名称和详细信息放入帮助字符串将会极大地帮助用户。
Prometheus 不喜欢一个指标有不同帮助字符串。如果你将一个指标从许多其他指标中创建出来,请选择其中一个放在帮助字符串中。
此示例中的帮助字符串包括 SNMP Exporter 使用 OID、JMX Exporter 插入样本 mBean 名称、HAProxy Exporter 的手动编写字符串、节点 Exporter 的广泛示例。
剔除无用统计数据
一些监控系统会额外提供1分钟、5分钟、15分钟的速率数据,以及从应用启动以来的平均速率( Dropwizard 指标中被称为mean
)。这些数据应该被剔除,因为它们用处不大,还会增加复杂度。Prometheus 可以自行计算速率,而且通常更准确,因为暴露出的平均值通常是指数衰减的。我们不知道最小值或最大值是在什么时间段内计算的,所以它的标准偏差在统计学上是没有意义的。如果你需要计算它,你可以暴露平方和,_sum
和_count
。
分位数(Quantiles)也存在类似问题,你可能选择直接剔除它们或者将它们放入 Summary 中。
点字符串(Dotted strings)
许多监控系统没有标签,而是采用像my.class.path.mymetric.labelvalue1.labelvalue2.labelvalue3
这样的点字符串形式。
Graphite 和 StatsD Exporter 通过共享一种小型配置语言来转换这些字符串。其他 Exporter 应该实现同样的方法。当前只有 Go 实现了这一转换,如果可以提取出来成为一个独立的库,其用处将会更大。
Collector
在实现你的 Exporter 的 Collector 时,你永远都应该避免使用通常的在每次抓取时更新指标的直接监控(direct instrumentation)方法。
相反,你应该每次都创建新的指标。在 Go 中,这可以通过在Collect()
方法中使用MustNewConstMetric
来完成。对于 Python,请参阅 https://github.com/prometheus/client_python#custom-collectors,对于 Java,在收集方法中生成List<MetricFamilySamples>
,StandardExports.java 中有一个示例。
之所以这样做,原因有两方面。首先,两次抓取可能会同时发生,直接监控使用的是实际上全局变量级别的变量,所以你可能会陷入竞争条件之中;其次,如果一个标签值消失了,它仍然会被导出。
直接监控你的 Exporter 自身是被允许的,例如总传输字节数或 Exporter 在所有抓取期间执行的操作次数。 对于如 blackbox exporter 和 SNMPexporter 这样的 Exporter,这些指标不应该在对特定 Target 的抓取中暴露出来,而只应在纯/metrics
调用中导出。
关于抓取本身的指标
有时你希望导出关于抓取动作的指标,比如它花费了多长时间或者处理了多少记录。
这些应该作为 Gauges 暴露,因为它们与抓取事件有关。这些指标名称前缀应该以 Exporter 名称为特征,例如jmx_scrape_duration_seconds
。通常情况下,不需要_exporter
后缀,如果 Exporter 也可以用作仅作为 Collector,则百分之一百应该排除掉_exporter
后缀。
主机和进程指标
许多系统(例如 Elasticsearch)都会暴露主机指标,如 CPU、内存和文件系统信息。由于 node_exporter 已经在 Prometheus 生态系统中提供了这些指标,因此这一类指标应该被剔除。
在 Java 的世界中,许多监控框架会暴露进程级和 JVM 级别的统计信息,如 CPU 和 GC。Java 客户端和 JMXexporter 已经通过 DefaultExports.java 以首选项包含这些指标,因此在使用此类语言的情况下应该提出此类指标。这里的规则同样适用于其他语言和框架。
部署
每个 Exporter 都应该监控单个实例应用程序,而且最好是与应用程序在同一台机器上运行。举例来说,这意味着对于你运行的每个 HAProxy,你都需要运行一个haproxy_exporter
进程。对于每台有 Mesos 工作负载的机器,你需要在它上面运行 Mesos exporter,并且如果有机器同时也是主控节点的话,还需要再运行一个 Exporter 进程。
这一原则背后的理论是,对于直接监控,这就是你希望做的事情,我们在其他的设计布局中也尽可能接近地这一点。这意味着所有服务发现都在 Prometheus 中进行,而不是在 Exporter 中。这也使 Prometheus 能够通过 blackbox exporter 让用户探测你的服务。
有两个例外:
第一个是在监控的目标应用附近运行完全不合理的场景。这主要包括 SNMP、blackbox 和 IPMI exporters。IPMI 和 SNMP exporter 中的设备往往是无法运行代码的黑盒(尽管如果你能在它们上运行一个 node_exporter 会更好),以及在监控 DNS 名称之类的情况时,也没有任何东西可以运行的 blackbox。在这种情况下,Prometheus 仍然应该进行服务发现,并将 Target 传递给要抓取的。具体请参阅 blackbox 和SNMP exporters 以获取示例。
请注意,目前只能使用 Go、Python 和 Java 客户端库编写这种类型的 Exporter。
第二个例外是你从系统中的任意实例拉取一些统计数据的同时,并不关心你正在与哪个实例通信。考虑这样一组 MySQL 副本,你想要执行一些针对数据的业务查询,然后导出。使用通常的负载均衡方法与一个副本通信的 Exporter 是最合理的方法。
这不适用于监控带有选举机制的系统的情况,在这种情况下,你应该分别监控每个实例,并在 Prometheus 中处理“主”节点(master)。这是因为并不总是恰好有一个主节点,并且在 Prometheus 实例前改变 Target 会导致奇怪的问题。
定时
指标只有在 Prometheus 抓取它们时才应该从应用程序中获取,Exporter 不应基于自己的定时器执行指标抓取。也就是说,所有抓取都应该是同步的(synchronous)。
因此,你不应在你暴露的指标上设置时间戳,让 Prometheus 来处理即可。如果你认为你需要时间戳,则很可能你需要使用 Pushgateway。
如果一个指标需要消耗特别昂贵的资源才能完成检索(例如超过一分钟的时间),那么可以考虑将它缓存起来。另外,应该在HELP
字符串中注明经过了缓存。
Prometheus 默认的抓取超时时间为10秒。如果你的 Exporter 预期可能超出这个时间,你应该在用户文档中明确指出。
推送
一些应用程序和监控系统只推送指标,例如 StatsD、Graphite 和 collectd。
这里有两个需要考虑的问题。
首先,什么时候让指标值过期?Collectd 和与 Graphite 通信的系统会定期导出数据,当它们停止运行时,我们希望停止暴露这些指标值。Collectd 支持设置过期时间。Graphite 则没有过期时间这一特性,所以过期时间是一个在 Exporter 上设置的标志位(flag)。
StatsD 处理的是事件而不是指标。对于 StatsD,最好的方式是为每个应用运行一个 Exporter,并在应用重启时重新启动它们,以便清除以往的状态。
其次,这些类型的系统倾向于允许用户发送差分数据或原始 Counter。你应该尽可能地依赖原始 Counter,因为这是 Prometheus 的一般模型。
对于服务级别指标,例如服务级别的批处理作业,你应该让 Exporter 将数据推送到 Pushgateway 并在事件发生后退出,而不是自己去处理事件状态。对于实例级别的批处理指标,目前还没有清晰的模式。可能的选项之一是“滥用” node_exporter 的文本文件 Collector,这依赖其内存状态(如果不需要重启后的持久化,这可能是最佳选择),或者实现类似文本文件 Collector 的功能。
失败的抓取
目前有两种失败抓取的模式:应用无法响应或出现其他问题。
第一种是返回5xx
错误。
第二种是在myexporter_up
(例如haproxy_up
)变量中有一个值,该值取决于抓取是否成功,0表示失败,1表示成功。
后者在期望抓取失败但仍能获取有用指标的情况下更好,例如 HAProxyexporter 提供进程统计信息的方式。前者对用户来说稍微更容易处理一点,因为up
通常按常规方式工作,但无法区分 Exporter 关闭和应用程序关闭的情况。
Landing page
如果用户访问http://yourexporter/
,有一个简单的 HTML 页面显示 Exporter 名称,并且提供到/metrics
页面的链接,这会让用户的体验更好。
端口号
用户可能在同一台机器上拥有多个 Exporter 和 Prometheus 组件,因此为了简化操作,每个 Exporter 都有唯一的端口号。
https://github.com/prometheus/prometheus/wiki/Default-port-allocations 是我们跟踪端口号变化的地方,这个列表公开可编辑。
在开发你的 Exporter 时,你可以从列表中自由选择地下一个可用端口号,但最好是在公开发布之前。如果你尚未准备好发布,将用户名和 WIP 标记放在注册列表里面也是可以接受的。
这只是为了使我们的用户更方便的一个注册列表,而不是开发特定的 Exporter 的承诺。对于内部应用程序的 Exporter,我们推荐使用默认端口范围之外的端口号。
发布
一旦你准备好向公开发布 Exporter,就可以通过邮件列表发送邮件,并提交 PR 以将其添加到可用 Exporter 列表中。
该文档基于 Prometheus 官方文档翻译而成。