编写导出器
如果您正在对自己的代码进行插桩,应遵循如何使用 Prometheus 客户端库对代码进行插桩的一般规则。当从其他监控或插桩系统获取指标时,情况往往不是那么黑白分明。
本文档包含了您在编写导出器或自定义收集器时应考虑的事项。所涵盖的理论对于直接进行插桩的人员也会感兴趣。
如果您正在编写导出器,并且对这里的任何内容不清楚,请通过 IRC (libera 上的 #prometheus 频道) 或邮件列表与我们联系。
可维护性与纯粹性
编写导出器时需要做出的主要决定是,您愿意投入多少精力来从中获得完美的指标。
如果所讨论的系统只有少数几个很少变化的指标,那么让一切完美是一个简单的选择,一个很好的例子是 HAProxy 导出器。
另一方面,如果系统有数百个指标,并且随着新版本的发布而频繁变化,而您又试图追求完美,那么您就给自己揽下了一大堆持续性的工作。MySQL 导出器就属于这一类。
node exporter是这两者的混合体,其复杂性因模块而异。例如,mdadm 收集器手动解析一个文件并暴露专为此收集器创建的指标,所以我们不妨把指标做得尽善尽美。对于 meminfo 收集器,其结果会因内核版本的不同而有所差异,因此我们最终只进行足够的转换以创建有效的指标。
配置
在处理应用程序时,您的目标应该是开发一个导出器,除了告诉它应用程序在哪里之外,用户不需要进行任何自定义配置。您可能还需要提供过滤某些指标的功能,因为在大型设置中这些指标可能过于精细和昂贵,例如HAProxy 导出器允许过滤每个服务器的统计信息。同样,可能有一些昂贵的指标默认是禁用的。
当与其他监控系统、框架和协议一起工作时,您通常需要提供额外的配置或自定义来生成适用于 Prometheus 的指标。在最好的情况下,监控系统的数据模型与 Prometheus 足够相似,您可以自动确定如何转换指标。这种情况适用于 Cloudwatch、SNMP 和 collectd。最多,我们需要让用户能够选择他们想要提取的指标。
在其他情况下,来自系统的指标是完全非标准的,这取决于系统的使用方式和底层应用程序。在这种情况下,用户必须告诉我们如何转换指标。JMX 导出器是这方面最典型的例子,Graphite 和 StatsD 导出器也需要配置来提取标签。
建议确保导出器无需配置即可开箱即用,并在需要时提供一系列转换配置示例。
YAML 是标准的 Prometheus 配置格式,所有配置默认应使用 YAML。
指标
命名
遵循指标命名最佳实践。
通常,指标名称应允许熟悉 Prometheus 但不熟悉特定系统的人能够很好地猜测指标的含义。名为 http_requests_total 的指标不是特别有用——这些请求是在传入时、在某个过滤器中还是在到达用户代码时测量的?而 requests_total 则更糟糕,是什么类型的请求?
对于直接插桩,一个给定的指标应该只存在于一个文件中。因此,在导出器和收集器中,一个指标应该只适用于一个子系统,并相应地命名。
指标名称不应以程序化方式生成,除非在编写自定义收集器或导出器时。
应用程序的指标名称通常应以导出器名称为前缀,例如 haproxy_up。
指标必须使用基本单位(例如,秒、字节),并将它们转换为更易读的格式留给绘图工具处理。无论您最终使用什么单位,指标名称中的单位必须与正在使用的单位相匹配。同样,暴露的是比率,而不是百分比。更好的是,为比率的两个组成部分分别指定一个计数器。
指标名称不应包含它们导出的标签,例如 by_type,因为如果该标签被聚合掉,这个名称就没有意义了。
唯一的例外是,当您通过多个指标导出具有不同标签的相同数据时,在这种情况下,这通常是区分它们最明智的方式。对于直接插桩,这种情况只应在导出一个包含所有标签的单一指标会导致基数过高时出现。
Prometheus 指标和标签名称使用 snake_case(蛇形命名法)书写。将 camelCase(驼峰命名法)转换为 snake_case 是可取的,但自动转换对于像 myTCPExample 或 isNaN 这样的名称并不总能产生好的结果,所以有时最好保持原样。
暴露的指标不应包含冒号,这些是为用户定义的记录规则在聚合时使用的。
指标名称中只允许使用 [a-zA-Z0-9:_]。
_sum、_count、_bucket 和 _total 后缀被摘要(Summary)、直方图(Histogram)和计数器(Counter)使用。除非您正在生成其中之一,否则请避免使用这些后缀。
_total 是计数器的惯例,如果您正在使用 COUNTER 类型,您应该使用它。
process_ 和 scrape_ 前缀是保留的。如果它们遵循匹配的语义,可以在这些前缀上添加您自己的前缀。例如,Prometheus 有 scrape_duration_seconds 用于表示抓取花费了多长时间,一个好的做法是也有一个以导出器为中心的指标,例如 jmx_scrape_duration_seconds,表示特定导出器完成其工作所需的时间。对于可以访问 PID 的进程统计信息,Go 和 Python 都提供了可以为您处理此问题的收集器。一个很好的例子是 HAProxy 导出器。
当您有成功请求计数和失败请求计数时,最好的暴露方式是使用一个指标表示总请求数,另一个指标表示失败请求数。这样可以轻松计算失败率。不要使用一个带有 failed 或 success 标签的指标。同样,对于缓存的命中或未命中,最好有一个表示总数的指标和另一个表示命中的指标。
考虑使用监控的人通过代码或网络搜索指标名称的可能性。如果这些名称非常成熟,并且不太可能在习惯于这些名称的人群之外使用(例如 SNMP 和网络工程师),那么保持原样可能是一个好主意。这种逻辑并不适用于所有导出器,例如 MySQL 导出器的指标可能被各种人使用,而不仅仅是 DBA。一个带有原始名称的 HELP 字符串可以提供与使用原始名称大部分相同的好处。
标签
阅读有关标签的一般建议。
避免使用 type 作为标签名称,它太通用了,而且通常没有意义。您还应尽可能避免使用可能与目标标签冲突的名称,例如 region、zone、cluster、availability_zone、az、datacenter、dc、owner、customer、stage、service、environment 和 env。但是,如果应用程序就是这样称呼某个资源的,最好不要通过重命名它来引起混淆。
避免仅仅因为它们共享一个前缀就把东西放到一个指标中的诱惑。除非您确定某些东西作为一个指标是有意义的,否则多个指标更安全。
标签 le 对直方图有特殊含义,quantile 对摘要有特殊含义。通常应避免使用这些标签。
读/写和发送/接收最好作为单独的指标,而不是作为一个标签。这通常是因为您一次只关心其中一个,并且这样使用它们更容易。
经验法则是,一个指标在求和或求平均时应该有意义。在导出器中还有一种情况,即数据基本上是表格形式的,否则将要求用户对指标名称进行正则表达式匹配才能使用。考虑您主板上的电压传感器,虽然对它们进行数学运算没有意义,但将它们放在一个指标中比为每个传感器设置一个指标更有意义。一个指标内的所有值应该(几乎)总是具有相同的单位,例如,考虑如果风扇速度与电压混合在一起,而您又没有办法自动将它们分开。
不要这样做
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_exporter 的 mysqld_perf_schema_events_statements_total 的 digest 标签是完整查询模式的哈希值,足以保证唯一性。然而,如果没有人类可读的 digest_text 标签,它就没什么用处,对于长查询,该标签将只包含查询模式的开头,因此不是唯一的。因此,我们最终既有供人类使用的 digest_text 标签,也有保证唯一性的 digest 标签。
目标标签,而不是静态抓取标签
如果您发现自己想要对所有指标应用相同的标签,请停下来。
通常有两种情况会出现这种情况。
第一种情况是某个标签对指标很有用,例如软件的版本号。相反,应使用 https://www.robustperception.io/how-to-have-labels-for-machine-roles/ 中描述的方法。
第二种情况是某个标签实际上是一个目标标签。这些标签,如区域、集群名称等,来自您的基础设施设置,而不是应用程序本身。应用程序不应该决定它在您的标签分类中的位置,这应该由运行 Prometheus 服务器的人来配置,不同的监控同一应用程序的人可能会给它不同的名称。
因此,这些标签应该位于 Prometheus 的抓取配置中,通过您正在使用的任何服务发现机制来设置。在这里应用机器角色的概念也是可以的,因为这对于至少某些抓取它的人来说可能是有用的信息。
类型
您应该尝试将指标的类型与 Prometheus 的类型相匹配。这通常意味着计数器(counter)和仪表盘(gauge)。摘要(summary)的 _count 和 _sum 也相对常见,偶尔您还会看到分位数(quantile)。直方图(histogram)很少见,如果您遇到一个,请记住暴露格式暴露的是累积值。
通常,指标的类型并不明显,特别是当您自动处理一组指标时。一般来说,UNTYPED 是一个安全的选择。
计数器不能减少,所以如果您有一个来自其他插桩系统(例如 Dropwizard 指标)的计数器类型可以减少,那么它就不是计数器,而是一个仪表盘。UNTYPED 可能是那里最好的类型,因为如果它被用作计数器,GAUGE 会产生误导。
帮助字符串
当您转换指标时,让用户能够追溯到原始指标是什么,以及导致该转换的规则是什么,这将非常有用。将收集器或导出器的名称、任何应用的规则的 ID 以及原始指标的名称和详细信息放入帮助字符串中,将极大地帮助用户。
Prometheus 不喜欢一个指标有不同的帮助字符串。如果您从许多其他指标中创建一个指标,请选择其中一个放入帮助字符串中。
例如,SNMP 导出器使用 OID,JMX 导出器放入一个示例 mBean 名称。HAProxy 导出器有手写的字符串。node exporter也有各种各样的例子。
丢弃不太有用的统计数据
一些插桩系统除了最小值、最大值和标准差外,还暴露 1 分钟、5 分钟、15 分钟的速率,以及自应用程序启动以来的平均速率(例如,在 Dropwizard 指标中称为 mean)。
这些都应该被丢弃,因为它们不是很有用,而且会增加混乱。Prometheus 可以自己计算速率,而且通常更准确,因为暴露的平均值通常是指数衰减的。您不知道最小值或最大值是在什么时间计算的,标准差在统计上是无用的,如果您需要计算它,可以随时暴露平方和、_sum 和 _count。
分位数有相关的问题,您可以选择丢弃它们或将它们放入摘要中。
点分隔字符串
许多监控系统没有标签,而是做类似 my.class.path.mymetric.labelvalue1.labelvalue2.labelvalue3 的事情。
Graphite 和 StatsD 导出器共享一种使用小型配置语言转换这些字符串的方法。其他导出器应该实现相同的方法。该转换目前只在 Go 中实现,如果将其分解为一个独立的库将会受益。
收集器
在实现导出器的收集器时,您不应使用通常的直接插桩方法,然后在每次抓取时更新指标。
相反,每次都应创建新的指标。在 Go 中,这是通过在您的 Collect() 方法中使用 MustNewConstMetric 完成的。对于 Python,请参阅 https://github.com/prometheus/client_python#custom-collectors,对于 Java,在您的 collect 方法中生成一个 List<MetricFamilySamples>,请参阅 StandardExports.java 作为示例。
原因有两方面。首先,两次抓取可能会同时发生,而直接插桩使用的是文件级全局变量,因此会出现竞争条件。其次,如果一个标签值消失了,它仍然会被导出。
通过直接插桩来监测你的导出器本身是可以的,例如,导出器在所有抓取中传输的总字节数或执行的调用次数。对于像 黑盒导出器 (blackbox exporter) 和 SNMP 导出器 这样不与单个目标绑定的导出器,这些指标只应在普通的 /metrics 调用中公开,而不应在抓取特定目标时公开。
关于抓取本身的指标
有时你可能想导出关于抓取本身的指标,比如抓取耗时或处理了多少条记录。
这些指标应该作为仪表盘(gauge)公开,因为它们是关于一个事件,即抓取。指标名称应以导出器名称为前缀,例如 jmx_scrape_duration_seconds。通常 _exporter 后缀会被省略,如果该导出器也可以仅作为收集器使用,那么绝对应该省略它。
应避免使用其他关于抓取的“元”指标。例如,抓取次数的计数器,或抓取持续时间的直方图。让导出器跟踪这些指标会与 Prometheus 自身自动生成的指标重复。这会增加每个导出器实例的存储成本。
机器和进程指标
许多系统,例如 Elasticsearch,会公开机器指标,如 CPU、内存和文件系统信息。由于 Prometheus 生态系统中的 节点导出器 (node exporter) 提供了这些指标,因此应丢弃此类指标。
在 Java 世界中,许多插桩框架会公开进程级和 JVM 级的统计信息,如 CPU 和 GC。Java 客户端和 JMX 导出器已经通过 DefaultExports.java 以首选形式包含了这些信息,因此也应丢弃它们。
其他语言和框架也类似。
部署
每个导出器都应该只监控一个应用程序实例,最好与它部署在同一台机器上。这意味着每运行一个 HAProxy,你就运行一个 haproxy_exporter 进程。对于每台运行 Mesos worker 的机器,你都在上面运行 Mesos 导出器,如果一台机器上既有 master 又有 worker,则为 master 再运行一个。
这背后的理论是,对于直接插桩,你就是这么做的,我们正试图在其他布局中尽可能地接近这种方式。这意味着所有的服务发现都在 Prometheus 中完成,而不是在导出器中。这样做的好处是 Prometheus 拥有所需的目标信息,允许用户使用 黑盒导出器 (blackbox exporter) 来探测你的服务。
有两个例外情况。
第一种情况是,在你正在监控的应用程序旁边运行导出器完全没有意义。SNMP、黑盒和 IPMI 导出器是这方面的主要例子。IPMI 和 SNMP 导出器是因为这些设备通常是黑盒子,无法在上面运行代码(不过,如果可以的话,在上面运行一个节点导出器会更好)。黑盒导出器是因为你监控的是像 DNS 名称这样的东西,也没有地方可以运行代码。在这种情况下,Prometheus 仍然应该进行服务发现,并将要抓取的目标传递过去。可以参考黑盒和 SNMP 导出器的例子。
请注意,目前只有 Go、Python 和 Java 客户端库可以编写这种类型的导出器。
第二个例外是当你从系统的某个随机实例中提取一些统计数据,并且不关心你正在与哪个实例通信时。考虑一组 MySQL 副本,你想对数据运行一些业务查询然后导出。在这种情况下,让一个导出器使用你通常的负载均衡方法与其中一个副本通信是最理智的做法。
这不适用于监控具有主节点选举的系统,在这种情况下,你应该单独监控每个实例,并在 Prometheus 中处理“主节点身份”的问题。这是因为并不总是只有一个主节点,并且在 Prometheus 不知情的情况下改变目标会引起异常。
调度
只有在 Prometheus 抓取指标时,才应从应用程序中拉取指标,导出器不应根据自己的计时器执行抓取。也就是说,所有抓取都应该是同步的。
因此,你不应该在你公开的指标上设置时间戳,让 Prometheus 来处理。如果你认为需要时间戳,那么你可能需要的是 Pushgateway。
如果一个指标的获取成本特别高,即需要超过一分钟,可以接受对其进行缓存。这应该在 HELP 字符串中注明。
Prometheus 的默认抓取超时时间是 10 秒。如果你的导出器预计会超过这个时间,你应该在用户文档中明确指出。
推送
一些应用程序和监控系统只推送指标,例如 StatsD、Graphite 和 collectd。
这里有两个考虑因素。
首先,你何时让指标过期?Collectd 和与 Graphite 通信的系统都会定期导出,当它们停止时,我们希望停止公开这些指标。Collectd 包含一个过期时间,所以我们使用它;Graphite 则没有,所以它在导出器上是一个标志位。
StatsD 有点不同,因为它处理的是事件而不是指标。最好的模型是在每个应用程序旁边运行一个导出器,并在应用程序重启时重启它们,以便清除状态。
其次,这类系统通常允许用户发送增量(delta)或原始计数器。你应该尽可能依赖原始计数器,因为这是 Prometheus 的通用模型。
对于服务级指标,例如服务级的批处理作业,你应该让你的导出器在事件发生后推送到 Pushgateway 并退出,而不是自己处理状态。对于实例级的批处理指标,目前还没有明确的模式。选项要么是滥用节点导出器的文本文件收集器,要么依赖内存中的状态(如果不需要在重启后持久化,这可能是最好的选择),或者实现与文本文件收集器类似的功能。
抓取失败
目前,对于你正在通信的应用程序没有响应或有其他问题的抓取失败,有两种处理模式。
第一种是返回一个 5xx 错误。
第二种是设置一个 myexporter_up 变量,例如 haproxy_up,其值为 0 或 1,取决于抓取是否成功。
后一种方法在即使抓取失败仍能获取一些有用指标的情况下更好,例如 HAProxy 导出器提供进程统计信息。前一种方法对用户来说处理起来稍微容易一些,因为 up 指标能以常规方式工作,尽管你无法区分是导出器宕机还是应用程序宕机。
登录页面
如果访问 http://yourexporter/ 会显示一个简单的 HTML 页面,上面有导出器的名称和指向 /metrics 页面的链接,这对用户来说会更友好。
端口号
用户可能在同一台机器上运行多个导出器和 Prometheus 组件,为了简化操作,每个组件都有一个唯一的端口号。
https://github.com/prometheus/prometheus/wiki/Default-port-allocations 是我们跟踪这些端口的地方,该页面是公开可编辑的。
在开发你的导出器时,随时可以占用下一个空闲的端口号,最好在公开发布之前就这么做。如果你还没有准备好发布,写上你的用户名和“WIP”也是可以的。
这是一个为了让用户生活更轻松的注册表,而不是开发特定导出器的承诺。对于内部应用程序的导出器,我们建议使用默认端口分配范围之外的端口。
发布
一旦你准备好向全世界发布你的导出器,请发送邮件到邮件列表,并提交一个 PR 将其添加到可用导出器列表中。