编写 Exporter

如果您正在为自己的代码插桩,应遵循使用 Prometheus 客户端库进行代码插桩的一般规则。从其他监控或插桩系统获取指标时,情况往往不那么非黑即白。

本文档包含了编写 Exporter 或自定义采集器时应考虑的事项。其中涵盖的理论对于直接插桩的人员也会有所帮助。

如果您正在编写 Exporter 但对本文档中的任何内容不清楚,请通过 IRC(libera 上的 #prometheus 频道)或邮件列表与我们联系。

可维护性与纯粹性

编写 Exporter 时需要做出的主要决定是,您愿意投入多少工作来获得完美的指标。

如果所涉及的系统只有少数不常变化的指标,那么追求完美是一件很容易的选择,一个很好的例子是 HAProxy exporter

另一方面,如果当系统有数百个指标且随新版本频繁变化时,您仍试图使其完美,那么您将面临大量持续的工作。 MySQL exporter 就属于这种情况。

node exporter 是两者的结合,复杂性因模块而异。例如,mdadm 采集器手动解析文件并暴露专门为此采集器创建的指标,因此我们不妨把指标做好。对于 meminfo 采集器,结果因内核版本而异,因此我们只需进行足够的转换来创建有效的指标。

配置

处理应用程序时,您应该力求编写一个除了告知应用程序位置外,无需用户进行任何自定义配置的 Exporter。如果某些指标在大型设置中过于细粒度且开销昂贵,您可能还需要提供过滤这些指标的能力,例如 HAProxy exporter 允许过滤每个服务器的统计数据。类似地,可能有一些默认禁用的开销较大的指标。

处理其他监控系统、框架和协议时,您通常需要提供额外的配置或自定义项,以生成适合 Prometheus 的指标。在最好的情况下,监控系统的数据模型与 Prometheus 足够相似,您可以自动确定如何转换指标。对于 CloudwatchSNMPcollectd 来说就是如此。最多,我们需要让用户选择他们想要拉取哪些指标的能力。

在其他情况下,系统中的指标完全是非标准的,取决于系统的使用方式和底层应用程序。在这种情况下,用户必须告诉我们如何转换指标。JMX exporter 是这方面的典型案例,GraphiteStatsD exporters 也需要配置来提取标签。

建议确保 Exporter 开箱即用无需配置,并在需要时提供一些示例配置用于转换。

YAML 是标准的 Prometheus 配置格式,所有配置默认应使用 YAML。

指标

命名

遵循指标命名的最佳实践

一般来说,指标名称应该让熟悉 Prometheus 但不熟悉特定系统的人能够很好地猜测指标的含义。名为 http_requests_total 的指标并不是非常有用——这些指标是在进入、在某个过滤器中还是在到达用户代码时测量的?而 requests_total 更糟糕,是什么类型的请求?

直接插桩时,给定的指标应该恰好存在于一个文件中。因此,在 Exporter 和采集器中,一个指标应该恰好应用于一个子系统,并据此命名。

除了编写自定义采集器或 Exporter 时,指标名称不应通过程序生成。

应用程序的指标名称通常应以 Exporter 名称为前缀,例如 haproxy_up

指标必须使用基本单位(例如秒、字节),并将其转换为更易读的形式留给绘图工具。无论您最终使用什么单位,指标名称中的单位必须与正在使用的单位匹配。同样,暴露比率而不是百分比。更好的是,为比率的两个组成部分分别指定一个计数器。

指标名称不应包含它们导出的标签,例如 by_type,因为如果标签被聚合掉,这就没有意义了。

唯一的例外是当您通过多个指标导出具有不同标签的相同数据时,在这种情况下,这通常是区分它们的最好方法。对于直接插桩,这只会在导出带有所有标签的单个指标会导致基数过高时才会出现。

Prometheus 指标和标签名称采用 snake_case 命名法。将 camelCase 转换为 snake_case 是可取的,尽管自动转换对于 myTCPExampleisNaN 等情况并不总是能产生好的结果,所以有时最好保留原样。

暴露的指标不应包含冒号,这些保留供用户定义的记录规则在聚合时使用。

指标名称中仅 [a-zA-Z0-9:_] 是有效的字符。

_sum_count_bucket_total 后缀由 Summaries、Histograms 和 Counters 使用。除非您正在生成这些类型之一,否则请避免使用这些后缀。

_total 是计数器(COUNTER)的约定后缀,如果您使用 COUNTER 类型,则应该使用它。

process_scrape_ 前缀是保留的。如果您的前缀遵循匹配的语义,则可以在它们之上添加自己的前缀。例如,Prometheus 有一个指标 scrape_duration_seconds 表示一次抓取花费的时间,一个好的实践是也拥有一个以 Exporter 为中心的指标,例如 jmx_scrape_duration_seconds,表示特定 Exporter 完成其工作所花费的时间。对于可以访问 PID 的进程统计信息,Go 和 Python 都提供了可以为您处理此事的采集器。一个很好的例子是 HAProxy exporter

当您拥有成功请求计数和失败请求计数时,最好的暴露方式是将总请求数作为一个指标,失败请求数作为另一个指标。这样可以轻松计算失败率。不要使用一个带有失败或成功标签的指标。类似地,对于缓存的命中或未命中,最好有一个表示总数的指标和另一个表示命中数的指标。

考虑一下使用监控的人搜索指标名称的可能性(通过代码或网络搜索)。如果名称非常常用,并且不太可能在习惯这些名称的人(例如 SNMP 和网络工程师)之外使用,那么保留原样可能是个好主意。这个逻辑并非适用于所有 Exporter,例如 MySQL Exporter 指标可能会被各种人员使用,而不仅仅是 DBA。使用包含原始名称的 HELP 字符串可以提供与使用原始名称相同的大部分好处。

标签

阅读关于标签的一般建议

避免使用 type 作为标签名称,它过于通用且通常没有意义。在可能的情况下,您还应该尝试避免与目标标签可能冲突的名称,例如 regionzoneclusteravailability_zoneazdatacenterdcownercustomerstageserviceenvironmentenv。但是,如果应用程序就是这样称呼某个资源的,最好不要通过重命名来引起混淆。

避免仅因为共享前缀就将事物放入一个指标的诱惑。除非您确定某个事物作为一个指标有意义,否则使用多个指标更安全。

标签 le 对于 Histograms 具有特殊意义,标签 quantile 对于 Summaries 具有特殊意义。通常避免使用这些标签。

读/写(read/write)和发送/接收(send/receive)最好作为独立的指标,而不是作为标签。这通常是因为您一次只关心其中一个,并且这样使用起来更容易。

经验法则是,一个指标在求和或平均时应该有意义。对于 Exporter 来说,还有一种情况出现,那就是数据从根本上是表格形式的,否则用户将需要对指标名称进行正则表达式匹配才能使用。考虑一下您主板上的电压传感器,虽然对它们进行数学运算没有意义,但将它们放在一个指标中比每个传感器有一个指标更有意义。一个指标内的所有值(几乎)总是有相同的单位,例如,考虑一下风扇速度是否与电压混在一起,而您没有办法自动将它们分开。

不要这样做

my_metric{label="a"} 1
my_metric{label="b"} 6
my_metric{label="total"} 7

或这样

my_metric{label="a"} 1
my_metric{label="b"} 6
my_metric{} 7

前者会破坏对您的指标进行 sum() 的人的计算,后者会破坏求和并且使用起来相当困难。一些客户端库,例如 Go,会主动阻止您在自定义采集器中做后者,所有客户端库都应该阻止您在直接插桩时做后者。永远不要做这两种情况中的任何一种,而是依赖 Prometheus 的聚合功能。

如果您的监控暴露了这样的总数,请丢弃该总数。如果出于某种原因必须保留它,例如总数包含了未单独计数的项目,请使用不同的指标名称。

插桩标签应该尽量少,每个额外的标签都是用户在编写 PromQL 时需要考虑的一个因素。因此,避免使用那些即使移除也不会影响时间序列唯一性的插桩标签。可以通过 info 指标来添加关于指标的附加信息,例如,参见下文如何处理版本号。

但是,在某些情况下,几乎所有指标用户都期望获得附加信息。如果情况是这样,添加非唯一的标签,而不是 info 指标,是正确的解决方案。例如,mysqld_exportermysqld_perf_schema_events_statements_totaldigest 标签是完整查询模式的哈希值,足以保证唯一性。然而,如果没有人类可读的 digest_text 标签,它的用处不大,对于长查询,该标签只会包含查询模式的开头,因此不唯一。因此,我们最终得到了用于人类阅读的 digest_text 标签和用于唯一性的 digest 标签。

目标标签,而非静态抓取标签

如果您发现自己想要将相同的标签应用于所有指标,请停下。

通常有两种情况会出现这种情况。

第一种情况是某些标签可能对指标有用,例如软件的版本号。相反,请使用 https://www.robustperception.io/how-to-have-labels-for-machine-roles/ 中描述的方法。

第二种情况是当某个标签实际上是目标标签时。这些是区域、集群名称等来自您的基础设施设置而不是应用程序本身的标签。应用程序不应说明它在您的标签分类中属于哪个位置,这应该由运行 Prometheus 服务器的人来配置,并且监控同一应用程序的不同人员可能会给它不同的名称。

因此,这些标签应该通过您正在使用的任何服务发现机制添加到 Prometheus 的抓取配置中。在这里应用机器角色的概念也是可以的,因为它对于至少一些抓取它的人来说可能是非常有用的信息。

类型

您应该尝试将您的指标类型与 Prometheus 类型匹配。这通常意味着 counters 和 gauges。summaries 的 _count_sum 也相对常见,偶尔您会看到 quantiles。Histograms 很少见,如果您遇到,请记住暴露格式暴露的是累积值。

通常情况下,指标的类型并不明显,特别是如果您正在自动处理一组指标。一般来说,UNTYPED 是一个安全的默认值。

Counters 不能下降,因此如果您从另一个插桩系统获取的计数器类型可以递减(例如 Dropwizard metrics),那么它不是一个计数器,而是一个 gauge。UNTYPED 可能是最好的类型,因为如果将其用作计数器,GAUGE 将会产生误导。

帮助字符串

当您转换指标时,用户能够追溯原始指标是什么以及导致转换的规则是什么,这一点非常有用。将采集器或 Exporter 的名称、应用的任何规则的 ID 以及原始指标的名称和详细信息放入帮助字符串中,将极大地帮助用户。

Prometheus 不喜欢一个指标有不同的帮助字符串。如果您正在从许多其他指标创建一个指标,请选择其中一个放入帮助字符串中。

关于此示例,SNMP Exporter 使用 OID,JMX Exporter 放入一个示例 mBean 名称。HAProxy exporter 有手动编写的字符串。node exporter 也有各种各样的示例。

丢弃不太有用的统计数据

一些插桩系统除了最小值、最大值和标准差之外,还会暴露 1 分钟、5 分钟、15 分钟的速率以及自应用程序启动以来的平均速率(例如,在 Dropwizard metrics 中称为 mean)。

这些都应该被丢弃,因为它们不是很有用并且会增加混乱。Prometheus 可以自己计算速率,并且通常更准确,因为暴露的平均值通常是指数衰减的。您不知道最小值或最大值是根据哪个时间段计算的,并且标准差在统计上没有用,如果您需要计算它,总是可以暴露平方和、_sum_count

Quantiles 有相关的问题,您可以选择丢弃它们或将它们放入 Summary 中。

点分隔字符串

许多监控系统没有标签,而是采用 my.class.path.mymetric.labelvalue1.labelvalue2.labelvalue3 之类的方式。

GraphiteStatsD exporters 共享一种使用小型配置语言转换这些字符串的方式。其他 Exporter 也应该实现相同的功能。该转换目前仅在 Go 中实现,如果能将其重构为单独的库将会受益。

采集器

在实现 Exporter 的采集器时,您绝不应使用通常的直接插桩方法,然后在每次抓取时更新指标。

相反,每次都创建新的指标。在 Go 中,这可以通过在 Collect() 方法中使用 MustNewConstMetric 来完成。对于 Python,请参阅 https://github.com/prometheus/client_python#custom-collectors;对于 Java,请在您的 collect 方法中生成一个 List<MetricFamilySamples>,示例请参阅 StandardExports.java

原因有两点。首先,两次抓取可能同时发生,而直接插桩使用的是实际上是文件级别的全局变量,因此您会遇到竞态条件。其次,如果一个标签值消失了,它仍然会被导出。

通过直接插桩来插桩您的 Exporter 本身是可以的,例如 Exporter 在所有抓取中传输的总字节数或执行的调用次数。对于未绑定到单个目标的 Exporter(例如 blackbox exporterSNMP exporter),这些指标只应在常规的 /metrics 调用中暴露,而不是在抓取特定目标时暴露。

关于抓取本身的指标

有时您希望导出与抓取相关的指标,例如抓取花费了多长时间或处理了多少条记录。

这些应该作为 gauges 暴露,因为它们与一个事件(抓取)相关,并且指标名称应以 Exporter 名称为前缀,例如 jmx_scrape_duration_seconds。通常会省略 _exporter 后缀,并且如果该 Exporter 仅作为采集器使用也有意义,则肯定要省略它。

应避免其他抓取的“元”指标。例如,抓取次数的计数器,或抓取持续时间的直方图。让 Exporter 跟踪这些指标会重复 Prometheus 本身的自动生成的指标。这增加了每个 Exporter 实例的存储成本。

机器与进程指标

许多系统(例如 Elasticsearch)会暴露机器指标,例如 CPU、内存和文件系统信息。由于 node exporter 在 Prometheus 生态系统中提供了这些信息,因此应丢弃此类指标。

在 Java 世界中,许多插桩框架暴露进程级别和 JVM 级别的统计信息,例如 CPU 和 GC。Java 客户端和 JMX exporter 已经通过 DefaultExports.java 以首选形式包含了这些信息,因此这些信息也应丢弃。

其他语言和框架也是如此。

部署

每个 Exporter 应该恰好监控一个应用程序实例,最好与其部署在同一台机器上。这意味着对于您运行的每个 HAProxy,您都运行一个 haproxy_exporter 进程。对于每台带有 Mesos worker 的机器,您都在其上运行 Mesos exporter,如果一台机器同时有 worker 和 master,则为 master 运行另一个 Exporter。

这样做的理论基础是,对于直接插桩,您会这样做,并且我们正努力在其他布局中尽可能地接近这种模式。这意味着所有服务发现都在 Prometheus 中完成,而不是在 Exporter 中。这还有一个好处,即 Prometheus 拥有它需要的目标信息,允许用户使用 blackbox exporter 来探测您的服务。

有两个例外

第一种情况是,将 Exporter 与您要监控的应用程序部署在一起完全没有意义。SNMP、blackbox 和 IPMI exporters 是主要的例子。IPMI 和 SNMP exporters 的设备通常是无法在其上运行代码的黑盒(尽管如果您能在其上运行 node exporter 会更好),而 blackbox exporter 用于监控像 DNS 名称这样的目标,这些目标上也没有任何可运行的代码。在这种情况下,Prometheus 仍然应该进行服务发现,并将目标传递给 Exporter 进行抓取。请参阅 blackbox 和 SNMP exporters 的示例。

请注意,目前只能使用 Go、Python 和 Java 客户端库编写这种类型的 Exporter。

第二个例外是,您从系统的随机实例中提取一些统计信息,并且不关心您正在与哪个实例通信。考虑一组 MySQL 副本,您想对其数据运行一些业务查询,然后导出。在这种情况下,使用您常用的负载均衡方法与其中一个副本通信的 Exporter 是最合理的方法。

当您监控具有主节点选举(master-election)的系统时,此规则不适用。在这种情况下,您应该单独监控每个实例,并在 Prometheus 中处理“主节点”状态。这是因为不总是恰好有一个主节点,并且在 Prometheus 不知情的情况下更改目标会导致异常。

调度

只有当 Prometheus 抓取时,才应从应用程序中拉取指标,Exporter 不应根据自己的计时器执行抓取。也就是说,所有抓取都应该是同步的。

因此,您不应该在暴露的指标上设置时间戳,让 Prometheus 来处理。如果您认为需要时间戳,那么您可能需要使用 Pushgateway

如果一个指标的检索成本特别高,即耗时超过一分钟,可以接受对其进行缓存。这应在 HELP 字符串中注明。

Prometheus 的默认抓取超时时间是 10 秒。如果您的 Exporter 预计会超过此时间,您应在用户文档中明确指出这一点。

推送

一些应用程序和监控系统只推送指标,例如 StatsD、Graphite 和 collectd。

这里有两个需要考虑的事项。

首先,您何时使指标过期?Collectd 和与 Graphite 通信的事物都会定期导出指标,当它们停止时,我们希望停止暴露这些指标。Collectd 包含一个过期时间,因此我们使用它;Graphite 没有,因此 Exporter 上有一个标志来控制。

StatsD 有点不同,它处理的是事件而不是指标。最好的模式是在每个应用程序旁边运行一个 Exporter,并在应用程序重启时重启 Exporter,以便清除状态。

其次,这类系统倾向于允许用户发送增量或原始计数器。您应该尽可能依赖原始计数器,因为这是 Prometheus 的一般模式。

对于服务级别的指标,例如服务级别的批处理作业,您的 Exporter 应该将数据推送到 Pushgateway 并在事件结束后退出,而不是自己处理状态。对于实例级别的批处理指标,目前还没有明确的模式。可选项包括:滥用 node exporter 的 textfile 采集器,依赖内存状态(如果不需要在重启后持久化,这可能是最好的方式),或者实现类似于 textfile 采集器的功能。

失败的抓取

目前有两种处理抓取失败的模式,当您正在通信的应用程序没有响应或存在其他问题时会发生这种情况。

第一种是返回一个 5xx 错误。

第二种是有一个 myexporter_up(例如 haproxy_up)变量,其值为 0 或 1,取决于抓取是否成功。

当即使抓取失败也能获得一些有用的指标时,后者更好,例如 HAProxy exporter 提供进程统计信息。前者对用户来说稍微更容易处理,因为up 按照通常的方式工作,尽管您无法区分 Exporter 下线和应用程序下线。

登录页

如果访问 http://yourexporter/ 会显示一个简单的 HTML 页面,其中包含 Exporter 的名称以及指向 /metrics 页面的链接,这对用户来说会更好。

端口号

用户可能在同一台机器上安装许多 Exporter 和 Prometheus 组件,为了使其更容易,每个组件都有一个唯一的端口号。

https://github.com/prometheus/prometheus/wiki/Default-port-allocations 是我们跟踪端口号的地方,这是公开可编辑的。

开发 Exporter 时,请随意获取下一个空闲端口号,最好在公开宣布之前获取。如果您尚未准备发布,写上您的用户名和 WIP 即可。

这是一个注册表,旨在方便用户,而非承诺开发特定的 Exporter。对于内部应用程序的 Exporter,我们建议使用默认端口分配范围之外的端口。

公告

一旦您准备好向世界宣布您的 Exporter,请发送电子邮件至邮件列表,并发送 PR 将其添加到可用 Exporter 列表中。

本文档是开源的。请通过提交 issue 或 pull request 帮助改进它。