这篇笔记围绕 Metrics、Micrometer、Prometheus 与 Spring Boot 的协作链路展开,重点整理指标语义、数据模型、标签设计、直方图、百分位数、查询方式以及工程实现中的常见误区。
参考资料:
官方文档:Micrometer Concepts 、 Micrometer Timers 、 Micrometer Histograms and Percentiles 、 Micrometer Meter Filters
Prometheus:Metric Types 、 Histograms and Summaries 、 Querying Basics 、 Query Functions
Spring Boot / OpenTelemetry:Spring Boot Metrics 、 OpenTelemetry Metrics API
[TOC]
1. Metrics 是什么
可以先从可观测性分工理解 Metrics 的定位:
- 日志回答“发生了什么”
- Trace 回答“请求经过了哪里”
- Metrics 回答“系统现在整体怎么样”
Metrics 的价值,不在于还原某一次具体请求,而在于:
- 快速感知趋势
- 快速发现异常
- 快速做聚合比较
- 快速触发告警
例如,Metrics 更适合回答下面这类问题:
- QPS 是否突然下降
- 错误率是否上升
- p95 / p99 延迟是否抖动
- 某个实例是不是明显更慢
- 某个线程池是不是长期打满
- 某个队列是不是持续堆积
Metrics 不擅长单点还原,但很适合:
- 低成本、持续、聚合地看系统状态
这也是为什么现代可观测性里,Metrics 几乎是最先接入的一层。
2. Metrics 的核心数据模型
很多问题并不是出在“不会埋点”,而是出在对指标数据模型理解不完整,进而把问题扩散到标签设计、聚合方式和告警规则上。
指标不是一个数,而是一组时间序列
一个 Metric 通常由两部分组成:
- 指标名
- 一组标签
例如:
1
http_server_requests_seconds_count{method="GET", status="200", uri="/users/{id}"}
真正落到监控系统里时,通常是:
- 一个指标名
- 多组 label value 组合
- 每组组合对应一条 time series
所以更精确地说:
metric name + label set = 一条时间序列
后续绝大多数高基数问题,根源都在这里。
标签不是附属品,而是维度
标签的意义是把同一个指标拆成多个观察维度。
例如:
http_requests_total{method="GET"}http_requests_total{method="POST"}
这仍然是同一个指标族,但已经是两条不同序列。
所以标签不是“多写点描述信息”而已,而是会直接影响:
- 序列数
- 存储成本
- 查询性能
- 聚合能力
- 告警可用性
什么是基数
基数可以粗略理解成:
- 某个指标最终会展开出多少种 label 组合
例如:
status只有200/400/500,基数低method只有GET/POST/PUT/DELETE,基数低userId几百万个,基数极高
例如写出:
1
order_pay_total{userId="123456789"}
那么每个用户都会生成自己的序列。
这通常不是更精细的观测,而是:
- 直接把时序系统推向高成本甚至不可用状态
所以 Metrics 设计里最重要的一个纪律就是:
- 标签必须尽量是有限集合
3. Metrics 和日志、Trace 的边界
很多混乱都来自于把三者互相替代。
Metrics 适合聚合,不适合追单条
Metrics 很擅长:
- 每秒请求数
- 错误率
- 平均延迟
- p99 延迟
- 当前线程数
- 队列长度
但它不适合回答:
- 某个具体订单为什么失败
- 某个具体用户请求参数是什么
- 某一次调用链具体卡在哪个 span
因为 Metrics 追求的是:
- 可聚合
- 可低成本长期存储
- 可快速查询
不是保存海量明细。
不要把 TraceId、UserId 当 Metrics 标签
这是最典型的边界错误。
像这些字段:
traceIdrequestIduserIdorderId- 原始搜索词
更适合:
- 日志
- Trace
而不适合当标签。
因为它们天然高基数,几乎等于:
- 一次请求一条新序列
一个简单判断原则
如果某个字段准备进入 Metrics 标签,可以先检查:
- 这个值的候选集合是不是有限的?
- 我真的需要按这个维度长期聚合观察吗?
- 它是不是更适合进日志或 Trace?
只要前两个问题答不上来,通常就不该放进标签。
4. 常见指标类型如何选择
不同监控系统术语略有不同,但主干思想非常稳定。
Prometheus 官方文档给出的核心类型有:
- Counter
- Gauge
- Histogram
- Summary
Micrometer 则从 JVM/Java 使用习惯出发,常见的是:
- Counter
- Gauge
- Timer
- DistributionSummary
- LongTaskTimer
其中 Timer 可以理解成:
- 对“时间分布”做了封装的专用度量
DistributionSummary 则是:
- 对“非时间数值分布”做统计
Counter
Counter 表示单调递增值。
Prometheus 官方定义也很明确:
- Counter 只能增加,或者在进程重启后归零
适合:
- 请求总数
- 错误总数
- 成功任务数
- 消费消息总数
例如:
1
2
3
http_requests_total
payment_failed_total
mq_consume_total
Counter 的正确理解方式
实际分析时,通常更关注它的绝对值变化速度,而不是累积值本身:
- 增长速率
因为绝对值会受到两个因素影响:
- 当前使用强度
- 应用运行时长
所以看 Counter,通常应该看:
rate(...)increase(...)
而不是长期观察一个只会不断增大的总数。
一个常见误区
不要把会下降的值放进 Counter。
例如:
- 当前在线人数
- 当前队列长度
- 当前活跃请求数
这些都可能上上下下,应该用 Gauge,不是 Counter。
Gauge
Gauge 表示当前值,可以上升也可以下降。
适合:
- 当前线程池活跃线程数
- 当前队列堆积长度
- 当前连接数
- 当前 JVM 内存使用量
例如:
1
2
3
queue_size
active_requests
jvm_memory_used_bytes
Gauge 的核心语义不是“累计发生了多少”,而是:
- 这一刻的状态是多少
Gauge 最容易踩的坑
Micrometer 官方文档专门提到过:
- Gauge 绑定对象如果被 GC,可能会出现
NaN或指标消失
很多 Gauge 问题的根源并不是系统没有数据,而是:
- 被观察对象的生命周期不合适
一个工程上更稳的做法是:
- 让 Gauge 绑定到一个真正长期存在的对象
- 不要临时 new 一个对象再让它被回收
Timer
Micrometer 里 Timer 是最实用的类型之一。
它用于统计:
- 某段操作执行了多少次
- 总耗时多少
- 延迟分布如何
Micrometer 官方文档强调:
Timer至少会发布事件数和总时间
这意味着如果你已经对一段代码用了 Timer:
- 通常不需要再额外加一个 Counter 去数“执行次数”
因为计数已经包含在 Timer 里了。
Timer 适合什么
适合:
- HTTP 请求耗时
- 数据库调用耗时
- RPC 调用耗时
- 方法执行耗时
Timer 不适合什么
Micrometer 官方文档明确说它更适合:
- short-duration latencies
也就是:
- 短耗时操作
如果任务可能持续几分钟、几十分钟甚至几小时,更合适的往往是:
LongTaskTimer
DistributionSummary
DistributionSummary 和 Timer 很像,但记录的不是时间,而是任意数值分布。
Micrometer 官方定义非常直接:
- 它用于记录非时间单位的事件分布
适合:
- 响应体大小
- 批处理条数
- 消息体大小
- 订单金额
- 单次写入字节数
例如:
1
2
3
http_response_size_bytes
batch_item_count
mq_payload_size_bytes
一个很重要的区分
- 记录“耗时”用
Timer - 记录“大小/数量/金额”这种分布,用
DistributionSummary
不要图省事把一切都塞进 Counter 或 Gauge。
LongTaskTimer
这是 Micrometer 里很有工程价值、但很多文章不展开的一类指标。
它适合:
- 长任务
- 后台作业
- 数据迁移
- 定时任务执行中时长
它回答的问题不是:
- 某次操作耗时分布如何
而更像:
- 现在有几个任务正在执行
- 它们已经跑了多久
这和普通 Timer 的关注点不一样。
Histogram
Histogram 记录的是分布。
Prometheus 官方文档说得很清楚:
- Histogram 会用 bucket 的方式表示观测值分布
- 同时还会有
count和sum
例如一个耗时直方图,最终通常会暴露:
http_request_duration_seconds_bucket{le="0.1"}http_request_duration_seconds_bucket{le="0.3"}http_request_duration_seconds_bucket{le="1"}http_request_duration_seconds_sumhttp_request_duration_seconds_count
它最大的工程价值是:
- 可以聚合
- 可以在服务端重新计算分位数
Summary
Summary 是另一类分布指标。
Prometheus 官方文档指出:
- Summary 会在客户端侧预先计算 quantile
- 但 quantile 不能再跨实例聚合
这就是它最大的局限。
可以把它理解成:
- 它直接给你分位数答案
- 但代价是后面不太好再重新组合计算
所以 Prometheus 官方实践文档强调:
- 如果需要做跨实例聚合百分位,Histogram 更合适
5. Histogram 和 Summary 如何选择
这是 Metrics 学习里最重要的分水岭之一。
Summary 的优点
- 客户端直接算 quantile
- 对局部实例的分位数观察很直接
Summary 的问题
Prometheus 官方文档明确指出:
- Summary 的 quantile 通常不能聚合
什么意思?
如果存在 10 个实例,并且每个实例都各自计算了 p99:
- 这 10 个 p99 不能直接再合成为整个服务的 p99
这在多副本系统里是个很大的限制。
Histogram 的优点
- 可以聚合 bucket
- 可以跨实例计算分位数
- 可以换时间窗口重新计算
这意味着 Histogram 更适合:
- 现代分布式服务
- Prometheus 生态
- 多实例统一看 p95 / p99
Histogram 的代价
- bucket 需要设计
- bucket 越多,序列和存储成本越高
- bucket 不合理会影响观测精度
因此 Histogram 也需要结合场景和成本评估。
一个工程化结论
在下面这种场景中:
- Prometheus + 多实例服务
那么经验上通常优先考虑:
- Histogram
如果根本不需要跨实例聚合 quantile,Summary 仍然可以使用,但现代主流实践里 Histogram 更常见。
关于 Native Histogram
Prometheus 官方最新文档已经明确把注意力放到:
- Native Histogram
并且给出倾向性建议:
- 如果能用,优先考虑 native histogram
但工程上要注意两点:
- client library / exporter / Prometheus 版本是否完整支持
- 现有监控链路是否仍然以 classic histogram 为主
所以实际项目里更稳妥的理解是:
- 原理上要知道 native histogram 是未来方向
- 落地时仍要看生态支持程度
不要在文档里直接把所有 Histogram 都等同为 native histogram。
6. Timer、Histogram、Summary、DistributionSummary 的关系
这几类指标的关系容易混淆,原因在于它们都涉及“统计分布”,但关注对象和适用场景并不相同。
Timer 不是另一个“新类型”,而是时间语义更强的封装
在 Micrometer 里:
Timer关注时间- 它天然带 count / total time
- 可以进一步配置 histogram / percentiles
所以它可以理解成:
- JVM 应用里统计时延的常用入口
DistributionSummary 是“非时间版的 Timer”
这个类比非常有帮助:
Timer统计耗时分布DistributionSummary统计数值分布
二者在“分布统计”层面很像,但单位不同。
Counter 和 Timer 不要重复打点
Micrometer 官方文档给了一个很重要的提醒:
Timer和DistributionSummary都会带 count
因此,如果已经有:
1
timer.record(...)
通常不需要再写:
1
counter.increment()
去统计同一件事发生了多少次。
否则你会得到:
- 冗余指标
- 冗余序列
- 更高认知成本
一个简单选择表
| 观测目标 | 建议类型 | 说明 |
|---|---|---|
| 事件总数,只增不减 | Counter |
适合请求数、错误数、消费数等累计事件 |
| 当前状态,可升可降 | Gauge |
适合队列长度、连接数、线程池活跃数 |
| 短时延 | Timer |
适合 HTTP、RPC、数据库调用等延迟分布 |
| 非时间数值分布 | DistributionSummary |
适合消息体大小、批量条数、订单金额 |
| 长任务执行中时长 | LongTaskTimer |
适合作业、定时任务、数据迁移 |
| 需要 Prometheus 端聚合百分位 | Histogram |
适合多实例统一计算 p95 / p99 |
7. Metrics 命名和单位怎么设计
Metrics 写得烂,后期比没有 Metrics 还痛苦。
一个好指标名至少要做到三点
- 看名字就知道在量什么
- 看单位就知道数值怎么解释
- 看维度就知道怎么聚合
例如:
http_server_requests_secondsmq_consume_totalcache_hit_totalqueue_sizeorder_amount_cny
如果是在 Micrometer API 中直接定义指标名,通常更建议使用点分命名,由具体 registry 负责转换为目标监控系统的命名形式。例如:
http.server.requestsmq.consumecache.hitorder.queue.size
单位不要含糊
Micrometer 官方文档强调:
- base unit 对可移植性有帮助
因此最好显式标注单位:
- 时间:
seconds - 大小:
bytes - 数量:
items
不要写成这种模糊名字:
request_timepayloadqueue_value
因为后面大家根本不知道:
- 毫秒还是秒
- 字节还是 KB
- 数量还是比率
在 Micrometer 场景中,还需要区分“逻辑指标名”和“后端暴露名”:
- 逻辑指标名用于业务代码埋点,建议保持中立,不手动拼接
_total、_seconds、_bytes - 后端暴露名由具体 registry 按命名约定生成,例如 Prometheus 会把 Counter 暴露为
_total,Timer 暴露为带时间单位后缀的一组序列
如果在代码里直接写出后端后缀,迁移到其他监控后端时更容易出现重复后缀或命名不一致问题。
名称表达“测量对象”,标签表达“切分维度”
这是很重要的分工。
例如:
1
http_server_requests_seconds{method="GET", status="200"}
这里:
http_server_requests_seconds是对象method/status是维度
不要把过多业务语义硬塞进名字里。
8. 标签设计是 Metrics 成败分水岭
好标签的特点
- 候选值有限
- 业务含义清晰
- 真的需要聚合分析
例如:
methodstatusoutcomeregioninstancequeue
坏标签的典型例子
userIdorderIdtraceIdsessionId- 原始 URL
- 搜索关键字
- SQL 原文
这些值的问题不是“太详细”,而是:
- 几乎无限多
这会导致:
- 时序数爆炸
- 采集成本上升
- 存储成本上升
- 查询速度下降
- Dashboard 不可用
- 告警规则复杂化
原始 URL 是高频坑
很多人会不小心把:
/user/123/user/456/user/789
都暴露成不同标签。
这样最终不是一个接口,而是:
- 每个用户一个 URI 维度
更合理的方式通常是:
- 路由模板
例如:
/user/{id}
标签不是越多越好
有些人觉得多加几个标签,后面更灵活。
这往往是错觉。
因为每加一个标签,都会让:
- 组合数乘上去
应当增加的不是“可能以后有用的标签”,而是:
- 当前明确需要按它聚合观察的标签
一个实用判断法
给某个指标加标签前,先问:
- 我真的会经常按这个维度看图或告警吗?
- 这个字段的取值集合是可控的吗?
- 如果不加它,我会失去关键可观测性吗?
三个问题里只要后两个答不上来,就先别加。
9. Metrics 体系应该监控什么
会打点不等于有监控体系。
先看 RED
服务端接口监控里,最常见的是 RED:
- Rate
- Errors
- Duration
也就是:
- 流量
- 错误
- 时延
对 HTTP / RPC 服务来说,这基本就是第一层核心看板。
再看 USE
资源层常见的是 USE:
- Utilization
- Saturation
- Errors
也就是:
- 利用率
- 饱和度
- 错误
适合:
- CPU
- 内存
- 磁盘
- 网卡
- 线程池
- 连接池
- 队列
业务指标一定要补
系统指标只能告诉你:
- 机器忙不忙
- JVM 抖不抖
- 接口快不快
但很多业务问题需要业务指标才能回答:
- 下单成功率
- 支付回调成功率
- 风控拦截量
- 缓存命中率
- 消息重试次数
所以一个完整的 Metrics 体系通常至少包含:
- 基础设施指标
- 应用运行指标
- 接口性能指标
- 业务指标
10. Spring Boot、Micrometer、Prometheus 三者是什么关系
这三者经常被混成一个东西,其实不是。
Micrometer 是指标埋点门面
Micrometer 的定位很像:
- Metrics 世界里的 SLF4J
也就是:
- 代码里用统一 API 打点
- 底层可以接多个后端
这也是 Micrometer 官方强调的一个核心价值:
- 避免 vendor lock-in
Spring Boot Actuator 负责自动集成和暴露
Spring Boot 会:
- 自动收集很多 JVM / Tomcat / HTTP / 线程池指标
- 自动配置
MeterRegistry - 提供
/actuator/metrics - 在接 Prometheus registry 时提供
/actuator/prometheus
Prometheus 是抓取和存储查询系统
Prometheus 的职责是:
- 定期抓取指标
- 存储时序数据
- 用 PromQL 查询
- 做告警计算
所以关系可以理解为:
1
2
3
4
5
应用代码 / Spring Boot
-> Micrometer 记录指标
-> Actuator 暴露指标端点
-> Prometheus 抓取
-> Grafana 展示 / Alertmanager 告警
从代码到看板,这条链路如何运行
如果把整个过程展开,通常是这样:
- 业务代码或框架在进程内更新指标对象
MeterRegistry维护这些 meter 的当前状态或累计值Actuator把它们暴露成/actuator/prometheusPrometheus按固定周期来抓取- 抓到的样本按时间序列存储
- 查询和告警时再用 PromQL 做聚合计算
可以先用一张图把链路串起来:
flowchart LR
A["业务代码 / 框架自动埋点"] --> B["Micrometer MeterRegistry"]
B --> C["Spring Boot Actuator /actuator/prometheus"]
C --> D["Prometheus 定时抓取"]
D --> E["PromQL 查询"]
E --> F["Grafana 看板"]
E --> G["Alertmanager 告警"]
在这个过程中:
Counter记录累计发生了多少Gauge反映当前这一刻是多少Timer记录次数和耗时分布
所以 Metrics 的工作方式,不是“像日志一样记下一条事件明细”,而是:
- 在应用进程里持续维护一组可观测状态
- 再由监控系统周期性采样、存储、聚合、告警
这也是为什么 Metrics 很擅长:
- 看趋势
- 看聚合
- 看异常波动
但不擅长:
- 还原某一次具体请求的完整细节
那个问题更适合日志和 Trace。
Metrics 是不是只看 JVM 内存?如果 JVM 重启了呢
不是。
JVM Metrics 只是整套 Metrics 体系的一部分,常见的还包括:
- 系统资源指标
- HTTP / RPC 指标
- 线程池 / 连接池 / 队列指标
- 业务指标
例如:
jvm_memory_used_bytes反映当前 JVM 内存使用量http_server_requests_seconds反映接口时延和次数executor_active_threads反映线程池活跃线程数order_created_total反映业务事件累计次数
从实现位置上看,很多指标确实先存在于当前应用进程里:
- JVM 指标来自 JVM 运行时状态
- 自定义
Counter/Timer/Gauge也通常由当前进程维护
这就意味着:
- 应用一旦重启,进程内累计值会重新开始
其中最典型的是:
Counter在进程重启后归零Timer/DistributionSummary的本地累计统计也会重新开始Gauge没有“累计归零”的概念,它只反映新进程当前这一刻的值
但这不等于“历史全没了”。
因为通常还有 Prometheus 在外部持续抓取和存储,所以:
- 单个实例的进程内累计会重置
- Prometheus 里已经抓到的历史样本仍然保留
- 查询时应该看
rate(...)、increase(...)这类时间窗口结果,而不是盯着裸的累计值
所以一个很重要的工程认知是:
- Metrics 很适合看趋势和时间窗口统计
- 如果目标是跨重启、跨实例且永久保留的业务总账,不能只依赖进程内 metrics
11. Java Spring Boot 如何接入 Metrics
依赖
最常见的是:
1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
说明:
actuator提供 Metrics 能力和端点micrometer-registry-prometheus让指标可以按 Prometheus 格式暴露
配置端点暴露
1
2
3
4
5
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
Spring Boot 官方文档明确提到:
/actuator/prometheus默认不是自动对外暴露的- 需要显式开放
常见访问路径
/actuator/metrics:按指标名查看/actuator/prometheus:Prometheus scrape 格式
Prometheus 抓取示例
1
2
3
4
5
scrape_configs:
- job_name: "my-service"
metrics_path: "/actuator/prometheus"
static_configs:
- targets: ["localhost:8080"]
12. Micrometer 里怎么写自定义指标
Counter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class OrderMetricsService {
private final Counter orderCreatedCounter;
public OrderMetricsService(MeterRegistry registry) {
this.orderCreatedCounter = Counter.builder("order.created")
.description("Total number of created orders")
.tag("biz", "order")
.register(registry);
}
public void onOrderCreated() {
orderCreatedCounter.increment();
}
}
适合:
- 累计事件数
Gauge
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class QueueMetrics {
private final AtomicInteger queueSize = new AtomicInteger(0);
public QueueMetrics(MeterRegistry registry) {
Gauge.builder("order.queue.size", queueSize, AtomicInteger::get)
.description("Current order queue size")
.register(registry);
}
public void setQueueSize(int size) {
queueSize.set(size);
}
}
适合:
- 当前状态
Timer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final Timer paymentTimer;
public PaymentService(MeterRegistry registry) {
this.paymentTimer = Timer.builder("payment.process")
.description("Payment processing latency")
.tag("biz", "payment")
.register(registry);
}
public void pay() {
paymentTimer.record(() -> {
doPay();
});
}
private void doPay() {
// ...
}
}
适合:
- 延迟统计
DistributionSummary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class PayloadMetrics {
private final DistributionSummary payloadSummary;
public PayloadMetrics(MeterRegistry registry) {
this.payloadSummary = DistributionSummary.builder("mq.payload.size")
.baseUnit("bytes")
.description("MQ payload size")
.register(registry);
}
public void recordPayloadSize(int bytes) {
payloadSummary.record(bytes);
}
}
适合:
- 大小分布
- 条数分布
- 金额分布
13. Spring Boot 自动指标要怎么理解
接入 Actuator 之后,不应只关注自定义指标。
Spring Boot 官方文档已经列出很多现成指标类别,例如:
- JVM Metrics
- System Metrics
- Logger Metrics
- Task Execution Metrics
- Spring MVC Metrics
这意味着你一开始就已经有一批很重要的基础指标可用。
自动指标解决什么
- JVM 内存
- GC
- 线程
- CPU
- HTTP 请求
- 连接池
- 任务执行器
这些是“系统是否健康”的基础层。
自定义指标补什么
自动指标通常不知道:
- 下单成功率
- 支付失败原因分布
- 风控拦截量
- 缓存命中率
- 队列业务堆积
所以正确姿势通常是:
- 自动指标打底
- 自定义指标补业务语义
业务代码多种多样时,如何低侵入接入
一个很实用的原则是:
- 越靠近统一边界层埋点,越优雅
- 越少让业务方法直接操作
MeterRegistry,越容易维护
常见业务形态可以这样接:
- HTTP / RPC 服务:优先吃框架自动指标,再在统一拦截器、过滤器、切面里补充公共标签或业务结果
- MQ 消费:在 listener 容器、消费模板、统一消费包装层记录消费次数、失败次数、处理耗时、重试次数
- 定时任务 / 批处理:在调度器或任务执行器外层记录执行次数、成功失败、执行耗时、运行中任务数
- 数据库 / 缓存 / 三方调用:优先使用现成 instrumentation,或在统一 client 封装层做埋点
- 纯业务事件:抽象成领域指标门面,由业务代码调用语义化方法,而不是直接拼指标名和标签
例如,不建议到处写:
1
registry.counter("order_created_total", "biz", "order").increment();
更建议收口成:
1
orderMetrics.recordCreated();
这样做的好处是:
- 指标名、标签、单位可以统一管理
- 业务代码不会充满打点细节
- 后面改指标口径时,不需要全仓库到处搜
常见的工程化接入方案
如果按“统一治理优先、业务侵入最小”排序,通常可以这样选择:
- 先用框架自动指标
- 再在统一边界层做公共埋点
- 对少量关键业务语义补自定义指标
- 最后才考虑让业务代码直接感知埋点 API
常见方案包括:
- 框架自动采集:最省事,适合 JVM、HTTP、线程池、连接池等技术指标
- Filter / Interceptor / AOP:适合统一统计入口流量、错误、耗时
- 模板方法 / 包装器:适合 MQ、批任务、三方调用、缓存访问等场景
- 指标门面服务:适合订单、支付、风控这类业务指标
- 注解式埋点:适合共性很强的耗时或次数统计,但复杂标签场景往往还得回到代码里补
真正不优雅的方式通常是:
- 每个 service 方法都手写一段
Counter/Timer - 指标命名规则散落在业务代码各处
- 同一个动作重复打多套语义重叠的指标
因此,更稳妥的落地方式通常是:
- 自动指标负责打底
- 边界层负责统一埋点
- 业务指标通过门面或领域服务收口
- 指标名、标签、单位、SLO 规则集中治理
14. Common Tags 要慎用但要会用
Micrometer 官方文档支持:
- common tags
Spring Boot 也支持统一加应用维度标签。
适合做 common tags 的字段
applicationenvregioncluster
这些字段:
- 稳定
- 候选值有限
- 几乎所有指标都需要按它切
不适合做 common tags 的字段
pod_uidcontainer_idhostname如果实例非常多且 churn 很频繁- 任意高基数字段
因为 common tags 的代价是:
- 它会加到很多指标上
一旦字段选错,放大的不是一个指标,而是整套指标体系。
Spring Boot 自定义 common tags
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "demo-service", "env", "prod");
}
}
MeterFilter 可以作为标签治理的兜底手段
仅靠约定并不能完全避免高基数标签进入系统,线上治理通常还需要统一的限制策略。MeterFilter 适合承担这类职责,例如限制某个标签的最大取值数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsFilterConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> metricsMeterFilter() {
return registry -> registry.config().meterFilter(
MeterFilter.maximumAllowableTags(
"http.server.requests",
"uri",
100,
MeterFilter.deny()
)
);
}
}
这类限制并不能替代指标设计,但可以在工程上避免异常标签值持续放大。
15. Histogram 深入理解
这一节值得单独讲透。
Histogram 暴露出来的不只是一个值
一个 Histogram 通常会暴露:
- bucket
- count
- sum
例如:
xxx_bucket{le="0.1"}xxx_bucket{le="0.3"}xxx_bucket{le="1"}xxx_bucket{le="+Inf"}xxx_countxxx_sum
这些指标分别代表:
- 不同阈值以下的累计样本数
- 样本总数
- 样本总和
需要特别注意的是,Prometheus classic histogram 的 _bucket 是累积桶,不是互斥桶:
le="0.3"表示小于等于0.3的样本总数,其中已经包含le="0.1"的样本le="+Inf"理论上应当与_count对齐
这也是 histogram_quantile(...) 必须围绕 _bucket 计算的原因。
bucket 是估算精度和成本的交换
bucket 越细:
- 分位数估算更细
- 但序列更多
bucket 越粗:
- 成本更低
- 但观测精度更差
所以 bucket 设计本质上是:
- 精度和成本的权衡
bucket 应围绕 SLO 设计
一个很实用的经验是:
- bucket 边界要围绕你的 SLO / SLI 来设计
例如 HTTP 延迟重点关心:
- 50ms
- 100ms
- 200ms
- 500ms
- 1s
那么 bucket 不应机械沿用默认值,而应尽量覆盖这些关键阈值。
为什么 Histogram 更适合服务端聚合
因为它暴露的是更底层的分布信息:
- bucket population
因此 Prometheus 可以在查询时:
- 先跨实例聚合 bucket
- 再算 quantile
这就是多副本系统里它比 Summary 更重要的根本原因。
16. Prometheus 里如何看 Histogram
平均值
平均耗时可以这样看:
rate(http_server_requests_seconds_sum[5m])
/
rate(http_server_requests_seconds_count[5m])
这有用,但也有局限:
- 平均值会掩盖长尾
所以它不是最重要的图。
p95 / p99
更常见的是:
histogram_quantile(
0.95,
sum by (le) (rate(http_server_requests_seconds_bucket[5m]))
)
和:
histogram_quantile(
0.99,
sum by (le) (rate(http_server_requests_seconds_bucket[5m]))
)
这正是 Histogram 在 Prometheus 生态里的典型用法。
按实例或接口看
如果你想按接口模板看:
histogram_quantile(
0.95,
sum by (le, uri) (rate(http_server_requests_seconds_bucket[5m]))
)
如果你想按实例看:
histogram_quantile(
0.95,
sum by (le, instance) (rate(http_server_requests_seconds_bucket[5m]))
)
这也是为什么前面一直强调:
- 标签要可聚合
否则查询阶段会非常难受。
17. Metrics 告警如何设计
Counter 告警看 rate,不看绝对值
例如错误总数:
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
或者错误率:
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count[5m]))
时延告警看分位数,不只看平均值
因为很多线上问题都是:
- 平均值看起来正常
- p99 已经炸了
饱和度告警比利用率更重要
例如:
- 线程池队列长度
- 数据库连接池等待
- MQ backlog
这些指标通常比“CPU 70%”更早暴露问题。
告警要结合窗口和持续时间
不要一个瞬时尖峰就报警。
更稳妥的是:
- 看 5m / 10m 窗口
- 配合
for - 避免抖动告警
18. 常见误区
看到一个值就想做成 Gauge
Gauge 不是万能兜底类型。
如果统计目标是:
- 累计事件
那就是 Counter,不是 Gauge。
已经有 Timer,还再单独加同语义 Counter
这通常是重复打点。
Micrometer 官方文档已经明确说了:
Timer和DistributionSummary自带 count
把高基数字段放进标签
这是最危险也最常见的坑。
例如:
- 用户 ID
- 订单 ID
- Trace ID
- 原始 URI
bucket 开得过多
直方图不是 bucket 越多越高级。
过多 bucket 会导致:
- 更多时序
- 更多存储
- 更重查询
用平均值代替长尾指标
平均值经常会掩盖真实体验。
很多系统真正伤人的不是平均耗时,而是:
- 尾延迟
自定义指标很多,但没有统一命名规范
结果通常是:
- 仪表盘难拼
- 查询难写
- 新人看不懂
- 不知道单位
没有区分“业务指标”和“技术指标”
技术指标说明的是:
- JVM 是否有问题
业务指标说明的是:
- 用户是否真的受影响
两者缺一不可。
19. OpenTelemetry Metrics 要知道到什么程度
现在可观测性体系越来越常见:
- Metrics + Logs + Traces 一体化
所以知道一点 OpenTelemetry Metrics 很有必要。
OTel 里的常见 instrument
OpenTelemetry Metrics API 里常见的 instrument 包括:
- Counter
- UpDownCounter
- Histogram
- ObservableCounter
- ObservableUpDownCounter
- ObservableGauge
这个体系和 Prometheus / Micrometer 在概念上是能互相对照的。
例如:
- Counter:非负递增
- Histogram:记录分布
- ObservableGauge:记录当前观测值
- UpDownCounter:可增可减累计量
为什么要知道它
因为越来越多系统会走:
- OTel SDK / Collector
- OTLP
- 再落到 Prometheus / Tempo / Loki / vendor backend
即使当前主栈是:
- Spring Boot + Micrometer + Prometheus
也最好知道:
- OTel 是更通用的上层可观测性标准语言
但当前 Java Spring Boot 场景里,Micrometer 仍然非常重要
因为在 Spring Boot 生态里:
- Micrometer 仍然是最主流、最自然的指标接入方式
所以工程上通常不是“二选一”,而是:
- 知道 OTel 方向
- 用好 Micrometer 落地
20. Metrics 看板的基本结构
服务总览
- QPS
- 错误率
- p50 / p95 / p99
- 实例数
JVM / 进程层
- 堆使用量
- GC 次数和停顿
- 线程数
- CPU 使用率
资源池层
- 线程池活跃数
- 队列长度
- 数据库连接池使用数 / 等待数
- MQ backlog
业务层
- 下单成功率
- 支付失败率
- 回调耗时
- 缓存命中率
一个实用经验
不建议一开始就堆叠大量图表。
先保证这 4 层是通的:
- 流量
- 错误
- 时延
- 饱和度
然后再慢慢补业务图。
21. 核心结论
如果只保留 12 条结论,可归纳为以下内容:
- Metrics 最擅长看趋势和聚合,不擅长追单次请求。
metric name + label set = 一条 time series。- 标签必须控制基数,高基数字段不要进标签。
- Counter 看 rate,不看绝对值。
- Gauge 记录当前状态,适合会上下波动的值。
Timer已经自带 count,不需要再补同语义 Counter。DistributionSummary用于非时间分布。- 多实例场景下,Histogram 通常比 Summary 更适合算分位数。
- 指标命名要带语义和单位,标签只放真正需要聚合的维度。
- 自动指标负责打底,自定义指标负责表达业务语义。
- 大多数 metrics 先存在于当前进程里,进程重启会让本地累计重新开始。
- 技术指标尽量自动采集,业务指标尽量收口到边界层或门面。
22. 相关延伸主题
如果继续向更完整的可观测性体系推进,可以继续展开下面这些主题:
PromQL深度学习笔记Grafana看板设计笔记Micrometer Observation / Tracing笔记Spring Boot Actuator笔记OpenTelemetry Collector笔记
这些主题与本篇内容可以组成一套更完整的可观测性知识链路。