Actuator 与 Micrometer:指标怎样从代码变成时间序列
lab_operation_duration_seconds_count{outcome="success"} 2
lab_operation_duration_seconds_sum{outcome="success"} 0.008
lab_operations_active 0这三行分别表示累计完成两次成功操作、它们合计耗时 0.008 秒,以及采样时没有正在执行的操作。它们是输出形态示例,实际耗时随运行变化。前两项会随着操作完成累加,最后一项只反映读取时刻的状态。
Micrometer 在应用内记录测量,Actuator 提供管理端点,Prometheus 周期性抓取并保存样本。理解每层保存的对象,才能判断一条曲线为何增长、归零或消失。
Meter 如何组织一次测量
名称、标签与 Registry
一个 Meter 由名称和标签组合标识,例如 lab.operation.duration + outcome=success。同名的 outcome=error 是另一个 Meter;同一标签集反复注册通常返回已有对象。MeterRegistry 负责注册、查找和向具体后端转换名称与单位。Micrometer Meter 模型说明了这些标识规则。
MeterRegistry
└─ lab.operation.duration
├─ outcome=success → Timer
├─ outcome=declined → Timer
└─ outcome=error → Timer
├─ count
├─ total time
├─ window max
└─ histogram buckets(按配置)Java 中的点分名称导出到 Prometheus 后通常变为下划线,并带上后端约定的单位与后缀。lab.operation.duration 可以形成 lab_operation_duration_seconds_count、_sum 等多条序列。查询时读取实际 scrape 文本,不凭 Java 名称猜完整指标名。
Registry 可以是简单内存实现,也可以连接 Prometheus、OTLP 或其他后端。一个 Composite Registry 可以把同一次记录交给多个后端,但各后端的累积周期和导出方式可能不同。Prometheus 使用拉取;其他 Registry 可能按 step 周期推送增量或速率。迁移后端时需要同时检查单位、temporality 和重置语义,不能只替换地址。
Counter、Timer 与 Gauge 的区别
| 类型 | 保存或观察什么 | 适合的对象 |
|---|---|---|
| Counter | 正向累加的次数或数量 | 拒绝数、尝试数、发送字节数 |
| Timer | 完成次数与累计耗时,可配置分布 | 请求耗时、连接获取时间 |
| DistributionSummary | 一组非时间数值的数量和总量 | 消息大小、批次条数 |
| Gauge | 当前对象的一次读取值 | 在途数、队列长度、缓存条目数 |
| LongTaskTimer | 尚未结束任务的数量和持续时间 | 长时间导出、批处理、持续卡住的任务 |
Timer 已经记录次数,计时完成的事件通常不必再增加一个含义相同的 Counter。另一个 Counter 只有在计数点不同才有必要,例如 operation 的 Timer 每个逻辑操作记录一次,而 attempts Counter 记录每次重试。
Timer 的 count 与 totalTime 在一次记录结束后更新。运行了一分钟但仍未返回的操作,还没进入普通 Timer 的完成样本;LongTaskTimer 或 active Gauge 能显示这些正在运行的工作。Timer 的 max 又常使用滚动时间窗口:长时间没有新记录时 max 可回到 0,而进程累计 count 与 sum 仍保留。这个行为不能解释为历史慢请求被撤销。Timer 文档列出了窗口配置。
Gauge 只在被观察时读取状态。数值在两次 scrape 之间经历 0→7→0,Prometheus 可能只看到两个 0。需要保留这七次到达的总量时使用 Counter,需要瞬时峰值时考虑更密的观测或专门的峰值记录。
Gauge 默认对被观测对象使用弱引用,应用要自己持有这个对象。下面把 AtomicInteger 保存在长生命周期字段中,再注册读取函数;不要注册临时对象后丢掉唯一强引用。以同一个 Meter ID 注册另一个对象也不会可靠地替换原读数来源。Gauge 文档解释了引用和重复注册问题。
private final AtomicInteger active = new AtomicInteger();
Gauge.builder("lab.operations.active", active, AtomicInteger::get)
.register(registry);标签如何扩大数据量
每个实际出现的标签组合都可能形成独立序列。20 个路由、5 种结果、10 个实例,对单值指标的组合上界为 20×5×10=1,000;并非所有组合都一定出现。Timer 再暴露 count、sum、max 和若干桶,序列数量还会继续增加。
路由标签用 /orders/{id},不要用 /orders/123456。requestId、traceId、订单号、完整 URL 和异常 message 的取值几乎无界,适合日志或 Trace 的详细记录,不适合指标标签。实例标签也会随扩缩容和发布产生历史序列,当前实例数无法代表保留期内的全部数据量。
MeterFilter 可以在注册前拒绝或映射标签。实验只允许最多三个 outcome:
MeterFilter.maximumAllowableTags(
"lab.operation", "outcome", 3, MeterFilter.deny());这是一道兜底上限。超过上限的新 Meter 会成为不记录数据的实现,业务调用仍可继续;因此应先把取值设计成有限集合,再用测试或注册量观察发现新标签被拒绝。规则顺序、映射和最大标签数语义见 MeterFilter。
在 Spring Boot 中记录并抓取指标
构建并启动实验
下载指标与追踪实验包,解压后进入 observability-signals-lab。Linux 需要 Docker Engine、Compose V2、Bash、curl、jq 和 unzip。宿主使用有 Docker 权限的普通用户;这项权限可以管理 daemon,不因容器内部 UID 而降低权限范围。
实验固定 Spring Boot 4.1.1、Micrometer 1.17.1、Maven 3.9.12,Java 编译目标为 17。Prometheus 3.5.1 负责实际抓取,Collector 0.148.0 接收同一应用的 Trace。两条采集链独立,指标不需要从已采样的 Trace 推算。
mkdir -p .m2 out
docker run --rm --user "$(id -u):$(id -g)" \
--entrypoint mvn -e HOME=/tmp -e MAVEN_CONFIG=/tmp/.m2 \
-v "$PWD:/work" -v "$PWD/.m2:/m2" -w /work \
maven:3.9.12-eclipse-temurin-17 -B -ntp \
-Dmaven.repo.local=/m2 -Duser.home=/tmp clean verify
export LAB_UID=$(id -u) LAB_GID=$(id -g)
docker compose -p ob17-signals up -d构建应通过 21 项 JUnit 测试并生成 target/observability-signals-lab-1.0.0.jar。挂载目录和 Maven 缓存需要对构建 UID 可写;应用以 10001:10001 运行,JAR 只读挂载;Collector 使用宿主 UID/GID 写入 out。Prometheus 使用官方镜像的运行身份。
依赖获取失败时配置可信 Maven 私服或 Docker 镜像源;离线运行需先转移这些精确版本的镜像与依赖缓存。禁止为拉取失败关闭证书验证。应用启动失败先看 docker compose -p ob17-signals logs --tail=60 app,确认 JAR 已构建而不是直接反复重启容器。
业务端口发布在宿主 17780,管理端口发布在 17781;两者都只绑定 127.0.0.1。健康检查成功后再发第一条请求:
curl -q --noproxy '*' --fail-with-body \
http://127.0.0.1:17781/actuator/health
curl -q --noproxy '*' --fail-with-body \
http://127.0.0.1:17780/lab/operations/ok
curl -q --noproxy '*' --fail-with-body -sS \
http://127.0.0.1:17781/actuator/prometheus | grep '^lab_'健康结果为 UP;业务结果含 outcome=success、attempts=1 和本次 Trace ID。抓取结果中 lab_operation_duration_seconds_count{...outcome="success"...} 增加 1,lab_operations_active 在请求完成后回到 0。额外的 application 标签来自应用配置,具体耗时不应与另一台机器逐值比较。
Actuator 暴露的内容由配置控制
POM 除 Web MVC 外引入两个依赖:spring-boot-starter-actuator 与 micrometer-registry-prometheus。Boot 自动配置 MeterRegistry 和常用 JVM、HTTP 等指标,Prometheus Registry 提供对应的输出格式,依赖与自动指标说明见 Boot Metrics。
实验管理配置为:
management.server.port=8081
management.endpoints.web.exposure.include=health,prometheus
management.endpoint.health.show-details=never
management.metrics.tags.application=observability-signals-lab/actuator/metrics 是按 Meter 名称检查测量值的管理接口,/actuator/prometheus 是 Prometheus 抓取入口,两者用途不同。上面的白名单只暴露 health 和 prometheus,所以直接访问 metrics 得到 404 是预期配置结果。需要临时调试时,先限制管理端口访问,再显式增加 metrics,结束后恢复原配置。
生产环境还要使用网络策略、TLS 和适合采集器的认证。单独一个管理端口不会自动产生授权控制;公开暴露 env、heapdump 或可写管理操作可能泄漏配置和运行数据。端点可用性、访问级别和 Web 暴露分别配置,完整规则见 Actuator Endpoints。
在完成点选择结果标签
实验为 success、declined、error 预先创建三个 Timer,用 Sample 保存开始时刻,在 finally 中按最终结果记录:
Timer.Sample sample = Timer.start(registry);
active.incrementAndGet();
String outcome = "error";
try {
// 处理操作;成功或业务拒绝时设置对应 outcome。
} finally {
sample.stop(durations.get(outcome));
active.decrementAndGet();
}durations 是包内 OperationController 持有的三个 Timer。其实现对 ok、decline、error、retry 四种请求分别处理;重试路径主动注入两次受控异常,第三次成功,用于区分尝试结果与最终结果,未模拟网络超时。
业务拒绝请求应完整返回 422。保存响应,再分别判断传输和 HTTP:
status=$(curl -q --noproxy '*' -sS --connect-timeout 2 --max-time 5 \
-o out/decline.json -w '%{http_code}' \
http://127.0.0.1:17780/lab/operations/decline)
test "$?" -eq 0 && test "$status" = 422
jq -e '.outcome == "declined" and .attempts == 0' out/decline.json422 在这个接口表示业务规则拒绝,因此增加 declined 的 Timer 次数,不增加 attempts。不要照搬成所有 422 都应从系统错误中排除;结果分类必须来自具体 API 契约。异步接口返回202时,也应另设业务终态的指标,避免把请求接纳误作最终完成。
源码的 MeterTests 使用真实 Registry 验证三个耗时 5、50、200 ms 对应 count=3、sum=255 ms、三个累积桶计数1/2/3;其他测试检查 Gauge 读数、相同 ID 的重复注册、标签拒绝、LongTaskTimer 以及 max 过期。MockClock 只推进统计窗口,用于验证语义,不用于测量性能。
把累计样本变成速率与分布
抓取时间和业务发生时间
Prometheus 每两秒从本实验管理端点抓取一次。业务可以在两次抓取之间完成多次操作,Counter 和 Timer 的累计字段会把增加量保留下来;每个请求的独立时间戳与详情则应由日志或 Trace 保存。
先查询最近抓到的累计次数:
curl -q --noproxy '*' --fail-with-body -sS -G \
--data-urlencode 'query=sum by(outcome)(lab_operation_duration_seconds_count)' \
http://127.0.0.1:17790/api/v1/query | jq '.data.result'结果按 outcome 分组,value 数组包含采样时刻和字符串形式的值。启动后的首次查询可能为空,因为尚未完成第一次抓取;先检查 target 和抓取间隔,而不是把空数组改成 0。
持续有请求后,可以在 Prometheus 页面 http://127.0.0.1:17790 输入:
sum by (outcome) (
rate(lab_operation_duration_seconds_count[5m])
)rate 根据窗口内样本估计每秒增长率,并处理可见的 counter reset。它需要足够的采样点,窗口通常要覆盖多次 scrape;increase 使用相同思想估计窗口总增量,所以受边界外推影响,结果可以是小数。完整函数语义见 Prometheus 查询函数。
各实例先 rate 再 sum。若先把累计值相加,一个实例重启归零的同时另一个实例继续增长,汇总曲线会掩盖或扭曲重置。应用在两次抓取间启动、执行后又退出,尚未抓到的值仍可能丢失;拉取模型无法重建从未观察到的进程历史。
平均耗时和错误比例
平均耗时用同一组请求的总耗时增长除以次数增长:
sum(rate(lab_operation_duration_seconds_sum{outcome="success"}[5m]))
/
sum(rate(lab_operation_duration_seconds_count{outcome="success"}[5m]))结果单位为秒。分子和分母都只选 success,避免把成功耗时除以所有请求数。跨实例同样先汇总总量和次数,再相除;直接平均各实例均值会给低流量实例过大的权重。
系统错误比例按本实验分类计算:
sum(rate(lab_operation_duration_seconds_count{outcome="error"}[5m]))
/
sum(rate(lab_operation_duration_seconds_count[5m]))业务拒绝仍进入总操作数,但不进入系统错误分子。若要表达用户目标未完成的比例,就要重新定义分子,可能包含 declined,甚至包含异步任务的终态失败。名字相同的“成功率”不能共用互相冲突的定义。
没有操作时分母为 0,结果可能是 NaN;目标没被抓到时可能连分母序列都不存在。告警应区分低流量、抓取失败和业务错误,而非用 or vector(0) 把所有空值变成绿色。相关规则和实际反例见错误归组与告警。
桶和分位数
实验 Timer 配置 10ms、100ms、1s 三个有限阈值,并导出 +Inf 桶。经典 histogram 的 le 表示“小于等于”,桶之间累积重叠:
三个样本:5 ms、50 ms、200 ms
le="0.01" → 1
le="0.1" → 2
le="1.0" → 3
le="+Inf"→ 3
count → 3
sum → 0.255 秒要算 success 中 100ms 内完成的比例,可以直接用对应桶增长除以 count 增长:
sum(rate(lab_operation_duration_seconds_bucket{outcome="success",le="0.1"}[5m]))
/
sum(rate(lab_operation_duration_seconds_count{outcome="success"}[5m]))要估计 p95,则保留 le 聚合后交给 histogram_quantile:
histogram_quantile(
0.95,
sum by (le) (
rate(lab_operation_duration_seconds_bucket{outcome="success"}[5m])
)
)这是由桶推算的估计值,精度受桶分布约束。实验的三个有限桶用于看懂计数关系;正式延迟分析应围绕 SLO 阈值和实际耗时范围设计桶,不能指望稀疏桶恢复每条请求的准确延迟。按服务或路由查询时,聚合中还应保留相应标签。
publishPercentiles(0.95) 在客户端计算分位数;publishPercentileHistogram() 向支持的后端提供可聚合分布。客户端 p95 不能跨实例相加或平均,而兼容的桶可以先合并。配置区别见 Micrometer 分位数与直方图。
Prometheus native histogram 把桶数据组织为另一类样本,查询与容量特征不同;上面的 _bucket + le 写法针对经典 histogram。迁移时同时核对 Registry、采集器、远程存储和查询支持,保留对照窗口再替换仪表盘。Prometheus histogram 说明解释了这些类型的区别。
指标不对时逐层核查
自动指标和业务指标怎样并存
Actuator 自动采集的 JVM、HTTP、连接池指标适合判断运行资源和请求处理。业务 Timer 则可以只包围一次核算、审批或重试整体,起止点和结果标签不同。两者保留不同名称,避免把相同操作记录两遍后又混在同一个聚合里。
Micrometer Observation 能让一段操作同时交给指标和追踪处理器。低基数字段适合进入两类信号,高基数字段只进入追踪;用户 ID 仍需遵守脱敏规则。@Timed、@Observed 等注解需要对应拦截支持,已有自动插桩的 Controller 再套一层相同观测可能产生重复记录。Boot Observability列出了注解启用条件与自动插桩的关系。
本实验明确使用业务 Timer 和手工 OpenTelemetry Span,没有同时安装 Java Agent 或第二套自动 SERVER Span。接入真实项目时先确认谁创建 HTTP 观测,再决定在哪个业务方法补一层。
没有序列、值不增长和曲线异常
应用注册 Meter
→ 业务路径执行 record/increment
→ /actuator/prometheus 有对应输出
→ Prometheus target 抓取成功
→ 查询标签、窗口匹配
→ 仪表盘单位与聚合正确从首次缺失的位置往前检查:
| 现象 | 常见原因 | 下一步 |
|---|---|---|
| 端点404 | Registry 依赖或暴露名单不匹配,访问了业务端口 | 对照 POM、管理端口和 exposure 配置 |
| 端点401/403 | 采集器身份无权限 | 为采集器配置专用认证,不关闭整个管理面的安全控制 |
| 本地没有目标指标 | 业务分支未执行、标签被拒绝、注册到另一个 Registry | 运行已知请求,并检查实际 Meter ID |
| 本地有值但 Prometheus 空 | target地址、网络、抓取路径或认证错误 | 查看 target 状态与 last error |
| Gauge 为 NaN 或长期不变 | 被观测对象失去强引用,或重复注册仍观察旧对象 | 检查对象字段与注册位置 |
| 发布后曲线尖峰 | reset 处理、单位变化、桶变化、重复采集 | 比对原始序列与旧查询,先恢复兼容配置 |
实验 target 状态可直接查询:
curl -q --noproxy '*' --fail-with-body -sS \
http://127.0.0.1:17790/api/v1/targets |
jq '.data.activeTargets[] | {scrapeUrl,health,lastError}'
docker compose -p ob17-signals logs --tail=40 prometheus app容器中的 Prometheus 使用 app:8081 访问应用,不能把自己的 127.0.0.1 当成另一个容器。修复后检查 target 恢复 up,触发新的 ok 请求,等待一次抓取并核对 count 增量。仅让健康端点返回 UP,无法验证业务 Meter 的注册和采集路径。
完整 HTTP 对照脚本和 PromQL 规则可分别执行:
bash scripts/verify-http.sh
docker run --rm --entrypoint /bin/promtool \
-v "$PWD:/work:ro" -w /work prom/prometheus:v3.5.1 \
test rules rules-test.yamlHTTP 脚本会增加四次操作,重复执行时累计值也会增加。规则测试使用真实 PromQL 引擎,覆盖持续错误、低样本、抓取失败、目标消失与实例重启;规则文件格式见 Prometheus 单元测试。
指标量突然增加时,先比较发布前后的标签取值、活跃序列和抓取大小,再处理存储空间。删除无界标签或停止错误插桩能减少新序列生成,但已写入的数据仍按后端保留策略存在。迁移名称、标签或桶时,应同步修改规则和仪表盘,避免新旧指标同时汇总而重复计数。
实验结束运行 docker compose -p ob17-signals down,停止这组容器与网络。out 内的 Trace 诊断文件保留,按需检查后清理。
权威资料与规范地址
Micrometer 测量对象
- Meter:https://docs.micrometer.io/micrometer/reference/concepts/meters.html
- Timer:https://docs.micrometer.io/micrometer/reference/concepts/timers.html
- Gauge:https://docs.micrometer.io/micrometer/reference/concepts/gauges.html
- MeterFilter:https://docs.micrometer.io/micrometer/reference/concepts/meter-filters.html
- 分位数与直方图:https://docs.micrometer.io/micrometer/reference/concepts/histogram-quantiles.html
Spring Boot 接入
- Metrics:https://docs.spring.io/spring-boot/reference/actuator/metrics.html
- Endpoints:https://docs.spring.io/spring-boot/reference/actuator/endpoints.html
- Observability:https://docs.spring.io/spring-boot/reference/actuator/observability.html
