Micrometer Prometheus
Prometheus 是一个维度时间序列数据库,具有内置的用户界面、自定义查询语言和数学运算功能。Prometheus 设计为基于拉取模型运行,定期从应用实例中抓取指标,基于服务发现机制。
Micrometer 在底层使用了 Prometheus 的 Java 客户端;该客户端有两个版本,Micrometer 都支持。如果你想使用“新”客户端(1.x
),请使用 micrometer-registry-prometheus
,但如果你想使用“旧”客户端(0.x
),请使用 micrometer-registry-prometheus-simpleclient
。
1. 安装 micrometer-registry-prometheus
建议使用 Micrometer(或您的框架,如果有)提供的 BOM,您可以在此处查看如何配置它 here。以下示例假设您正在使用 BOM。
1.1. Gradle
在 配置 完 BOM 后,添加以下依赖项:
implementation 'io.micrometer:micrometer-registry-prometheus'
由于该依赖项由 BOM 定义,因此不需要指定版本。
1.2. Maven
在 配置 好 BOM 之后,添加以下依赖:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
由于该依赖项的版本由 BOM 定义,因此不需要指定版本。
2. 安装 micrometer-registry-prometheus-simpleclient
建议使用 Micrometer(或您的框架,如果有的话)提供的 BOM,您可以在此处查看如何配置它 here。以下示例假设您正在使用 BOM。
2.1. Gradle
在 BOM 配置 完成后,添加以下依赖项:
implementation 'io.micrometer:micrometer-registry-prometheus-simpleclient'
此依赖项不需要指定版本,因为它由 BOM 定义。
2.2. Maven
在 配置 了 BOM 之后,添加以下依赖:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus-simpleclient</artifactId>
</dependency>
由于此依赖项由 BOM 定义,因此不需要指定版本。
3. 配置
Prometheus 期望通过抓取或轮询单个应用程序实例来获取指标。除了创建 Prometheus 注册表外,你还需要向 Prometheus 的抓取器暴露一个 HTTP 端点。在 Spring Boot 应用程序中,如果存在 Spring Boot Actuator,则会自动配置一个 Prometheus actuator 端点。否则,你可以使用任何基于 JVM 的 HTTP 服务器实现来向 Prometheus 暴露抓取数据。
以下示例使用 JDK 的 com.sun.net.httpserver.HttpServer
来暴露一个抓取端点:
PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
try {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/prometheus", httpExchange -> {
String response = prometheusRegistry.scrape(); 1
httpExchange.sendResponseHeaders(200, response.getBytes().length);
try (OutputStream os = httpExchange.getResponseBody()) {
os.write(response.getBytes());
}
});
new Thread(server::start).start();
}
catch (IOException e) {
throw new RuntimeException(e);
}
PrometheusMeterRegistry
有一个scrape()
函数,它知道如何提供抓取所需的字符串数据。你只需要将其连接到一个端点即可。
如果你使用“新”客户端(micrometer-registry-prometheus
),你也可以使用 io.prometheus.metrics.exporter.httpserver.HTTPServer
,你可以在 io.prometheus:prometheus-metrics-exporter-httpserver
中找到它(如果你想使用它,需要将其添加为依赖项):
PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
HTTPServer.builder()
.port(8080)
.registry(prometheusRegistry.getPrometheusRegistry())
.buildAndStart();
如果你使用 "legacy" 客户端(micrometer-registry-prometheus-simpleclient
),你可以选择使用 io.prometheus.client.exporter.HTTPServer
,你可以在 io.prometheus:simpleclient_httpserver
中找到它:
PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
// you can set the daemon flag to false if you want the server to block
new HTTPServer(new InetSocketAddress(8080), prometheusRegistry.getPrometheusRegistry(), true);
如果你使用“新”客户端(micrometer-registry-prometheus
),另一个替代方案可以是 io.prometheus.metrics.exporter.servlet.jakarta.PrometheusMetricsServlet
,你可以在 io.prometheus:prometheus-metrics-exporter-servlet-jakarta
中找到它,前提是你的应用程序运行在 servlet 容器(如 Tomcat)中:
PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
HttpServlet servlet = new PrometheusMetricsServlet(prometheusRegistry.getPrometheusRegistry());
如果你使用 "legacy" 客户端(micrometer-registry-prometheus-simpleclient
),另一个替代方案可以是 io.prometheus.client.exporter.MetricsServlet
,你可以在 io.prometheus:simpleclient_servlet
中找到它,前提是你的应用程序运行在 servlet 容器中(例如 Tomcat):
PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
HttpServlet servlet = new MetricsServlet(prometheusRegistry.getPrometheusRegistry());
3.1. 抓取格式
默认情况下,PrometheusMeterRegistry
的 scrape()
方法返回 Prometheus 文本格式。
OpenMetrics 格式也可以生成。要指定返回的格式,你可以将内容类型传递给 scrape
方法。例如,要获取 OpenMetrics 1.0.0 格式的抓取数据,你可以使用其对应的 Content-Type,如下所示(以 "new" 客户端 micrometer-registry-prometheus
为例):
String openMetricsScrape = registry.scrape("application/openmetrics-text");
如果你使用 "legacy" 客户端(micrometer-registry-prometheus-simpleclient
),你可以使用 Prometheus Java 客户端的常量来实现:
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.common.TextFormat;
CollectorRegistry.defaultRegistry.metricFamilySamples().forEach(sample -> {
System.out.println(TextFormat.write004(sample));
});
String openMetricsScrape = registry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100);
在 Spring Boot 应用程序中,Prometheus Actuator 端点 支持以任意格式抓取,默认情况下在没有特定 Accept
头的情况下使用 Prometheus 文本格式。
3.2. Prometheus 重命名过滤器
在某些情况下,Micrometer 提供了与常用的 Prometheus 简单客户端模块重叠的检测功能,但为了保持一致性和可移植性,选择了不同的命名方案。如果您希望使用 Prometheus 的“标准”名称,请添加以下过滤器:
prometheusRegistry.config().meterFilter(new PrometheusRenameFilter());
3.3. Prometheus 客户端属性
如果你使用“新”客户端(micrometer-registry-prometheus
),你可以使用 Prometheus Java 客户端支持的一些属性,详见 Prometheus Java Client 配置文档。这些属性可以从 Prometheus Java 客户端支持的任何来源加载(属性文件、系统属性等),也可以通过 Micrometer 使用 PrometheusConfig
获取:
PrometheusConfig config = new PrometheusConfig() {
@Override
public String get(String key) {
return null;
}
@Override
public Properties prometheusProperties() {
Properties properties = new Properties();
properties.putAll(PrometheusConfig.super.prometheusProperties()); 1
properties.setProperty("io.prometheus.exemplars.sampleIntervalMilliseconds", "1"); 2
return properties;
}
};
PrometheusMeterRegistry registry = new PrometheusMeterRegistry(config, new PrometheusRegistry(), Clock.SYSTEM);
你可以重用
PrometheusConfig
中定义的 "default" 属性。你可以从任何属性源设置任何属性。
4. 图表绘制
本节旨在快速入门如何在 Prometheus 中呈现源自 Micrometer 的指标的有用表示。有关 Prometheus 中可能性的更完整参考,请参阅 Prometheus 文档。
4.1. Grafana 仪表盘
在 GrafanaHub 上有很多公开的第三方 Grafana 仪表盘。可以查看一个示例 这里。
仪表板由社区在其外部 GitHub 仓库中维护,因此如果您遇到问题,应在相应的 GitHub 仓库中创建问题。
4.2. 计数器
生成随机游走计数器图形的查询是 rate(counter[10s])
。
图 1. 一个由 Grafana 渲染的随机游走计数器的图表。
在没有速率归一化的情况下表示某个时间窗口内的计数器通常没有太大用处,因为这种表示方式取决于计数器递增的速度以及服务的持续时间。通常,对这类时间序列进行速率归一化是最有用的,以便更好地分析它们。由于 Prometheus 会跟踪所有时间内的离散事件,它有一个优势:允许在查询时选择任意时间窗口进行归一化(例如,rate(counter[10s])
提供了 10 秒窗口内每秒请求数的概念)。在前面的图像中,一旦新实例(例如在生产部署中)开始服务,速率归一化的图表将迅速回到大约 55 的值。
图 2. 相同的随机游走上的计数器,未进行速率归一化。
相比之下,如果没有速率标准化,计数器在服务重启时会回落到零,并且在服务的正常运行时间内,计数会无限制地增加。
4.3. 定时器
Prometheus 的 Timer
会产生两个名称不同的计数器时间序列:
-
${name}_count
: 所有调用的总数。 -
${name}_sum
: 所有调用的总时间。
同样,表示某个时间窗口内未进行速率归一化的计数器通常没有太大用处,因为这种表示方式既取决于计数器递增的速度,也取决于服务的寿命。
使用以下 Prometheus 查询,我们可以绘制出关于计时器的最常用统计信息的图表:
-
平均延迟:
rate(timer_sum[10s])/rate(timer_count[10s])
-
吞吐量(每秒请求数):
rate(timer_count[10s])
图 3. 模拟服务上的计时器。
4.4. 长任务计时器
以下示例展示了一个 Prometheus 查询,用于绘制串行任务的长任务计时器的持续时间,该计时器为 long_task_timer_sum
。在 Grafana 中,我们可以在某个固定点设置警报阈值。
图 4. 使用固定警报阈值的模拟背靠背长任务。
5. 同名不同标签键集的限制
PrometheusMeterRegistry
不允许创建具有相同名称但标签键集不同的计量器,因此您应确保具有相同名称的计量器具有相同的标签键集。否则,具有相同名称但标签键集不同的后续计量器将不会被注册。这意味着您不应执行以下操作:
// Please don't do this
registry.counter("test", "first", "1").increment();
registry.counter("test", "second", "2").increment();
这将导致以下警告,并且第二个 Meter
不会被注册:
WARNING: The meter (MeterId{name='test', tags=[tag(second=2)]}) registration has failed: Prometheus requires that all meters with the same name have the same set of tag keys. There is already an existing meter named 'test' containing tag keys [first]. The meter you are attempting to register has keys [second]. Note that subsequent logs will be logged at debug level.
相反,你可以这样做:
registry.counter("test", "first", "1", "second", "none").increment();
registry.counter("test", "first", "none", "second", "2").increment();
除了警告之外,你还可以注册一个仪表注册失败监听器来处理失败:
registry.config().onMeterRegistrationFailed((id, reason) -> {
throw new IllegalArgumentException(reason);
});
实际上,PrometheusMeterRegistry
有一个快捷方式可以实现这一点,因此你可以通过以下方式达到相同的效果:
registry.throwExceptionOnRegistrationFailure();
6. 示例
示例(Exemplars)是可以附加到时间序列值上的元数据。它们可以引用指标之外的数据。一个常见的用例是存储追踪信息(如 traceId
、spanId
)。示例不是标签/维度(在 Prometheus 术语中称为 labels),它们不会增加基数,因为它们属于时间序列的值。
为了为 PrometheusMeterRegistry
设置 Exemplars,你将需要一个提供跟踪信息的组件。如果你使用的是“新”客户端(micrometer-registry-prometheus
),这个组件是 io.prometheus.metrics.tracer.common.SpanContext
;而如果你使用的是“旧”客户端(micrometer-registry-prometheus-simpleclient
),则是 SpanContextSupplier
。
设置它们有些相似,如果你使用“新”客户端(micrometer-registry-prometheus
):
PrometheusMeterRegistry registry = new PrometheusMeterRegistry(
PrometheusConfig.DEFAULT,
new PrometheusRegistry(),
Clock.SYSTEM,
new MySpanContext() 1
);
registry.counter("test").increment();
System.out.println(registry.scrape("application/openmetrics-text"));
你需要实现
SpanContext
(class MySpanContext implements SpanContext { … }
)或者使用已经存在的实现。
但如果你使用的是 "legacy" 客户端(micrometer-registry-prometheus-simpleclient
):
PrometheusMeterRegistry registry = new PrometheusMeterRegistry(
PrometheusConfig.DEFAULT,
new CollectorRegistry(),
Clock.SYSTEM,
new DefaultExemplarSampler(new MySpanContextSupplier()) 1
);
registry.counter("test").increment();
System.out.println(registry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100));
你需要实现
SpanContextSupplier
(class MySpanContextSupplier implements SpanContextSupplier { … }
) 或者使用现有的实现。
如果您的配置正确,您应该会得到类似这样的结果,# {span_id="321",trace_id="123"} …
部分就是紧跟在值后面的 Exemplar:
# TYPE test counter
# HELP test
test_total 1.0 # {span_id="321",trace_id="123"} 1.0 1713310767.908
# EOF
Exemplars 仅在 OpenMetrics 格式中受支持(它们不会出现在 Prometheus 文本格式中)。你可能需要显式地请求 OpenMetrics 格式,例如:
curl --silent -H 'Accept: application/openmetrics-text; version=1.0.0' localhost:8080/prometheus