
线上排障最怕什么?不是系统彻底挂掉,而是服务半死不活、延迟偶发抖动,对着黑盒束手无策。以前靠"阈值告警+人工 grep 日志"的模式,在微服务里根本不够用。故障往往藏在下游依赖的雪崩、线程池打满或者某个长尾请求里。这几年我们团队把监控体系从"被动看大盘"往"可观测性"推了一遍,踩过不少坑,也沉淀了一套能直接上生产的落地方案。这篇文章不整虚的,直接按 Spring Boot 3.x 生态,把 Actuator、Micrometer、Prometheus、Grafana 和链路追踪、结构化日志怎么串起来,掰开揉碎讲清楚。
1. 理念升级:别再把监控当可观测性
监控和可观测性完全是两码事。监控是告诉你"系统还活着吗",可观测性是告诉你"系统为什么表现异常"。线上出问题,你总不能靠猜。
生产环境里,可观测性就靠三根柱子撑着,而且必须能互相串起来:
- Metrics(指标):聚合后的时序数据。QPS、RT、CPU、内存、GC 次数全在这里。它开销最小,适合做实时告警,帮你快速圈定"哪个服务或接口出了问题"。
- Traces(链路):一次请求在分布式系统里完整的调用轨迹。它负责"定界",告诉你请求到底卡在了网关、订单服务还是 Redis。
- Logs(日志) :离散的事件记录,带业务上下文和堆栈。它负责"挖根因"。但非结构化的日志在排查时就是灾难,必须结构化,并且和 Metrics/Traces 共享同一个
TraceId。
这三样东西如果各玩各的,排障时就得在三个系统里来回切。正确的做法是:Metrics 报警触发告警,拿着 TraceId 去 Jaeger/Zipkin 看调用链,发现某个下游服务耗时飙升,再拿同样的 TraceId 去 Kibana/Loki 过滤出该请求的所有业务日志。数据同源、标识统一,闭环才算真正跑通。
2. 基础暴露:Actuator 端点配置与安全加固
Spring Boot Actuator 是暴露运行时状态的标准入口。但线上环境千万别图省事把端点全打开,尤其是健康检查、线程 Dump、堆内存这些,暴露出去就是安全隐患。
2.1 核心配置要点
yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus # 按需给,别全开
base-path: /actuator
endpoint:
health:
show-details: when-authorized # 线上别用 always,配合安全认证后再展示详情
probes:
enabled: true # K8s 原生探针必须开
server:
port: 8081 # 强烈建议监控端口和业务端口拆开,防 DDoS 或业务流量打满拖垮监控
K8s 的 livenessProbe 和 readinessProbe 会自动映射到 /actuator/health/liveness 和 /actuator/health/readiness,开箱即用。
2.2 安全加固别偷懒
监控端点里藏着 JVM 堆栈、连接池状态,裸奔上线等于送人头。
- 认证拦截 :上了 Spring Security 的项目,直接给
/actuator/**加一层 Basic Auth 或 OAuth2。没认证的一律 401。 - 网络隔离:靠安全组或 K8s NetworkPolicy 做白名单,只放 Prometheus Server 和运维跳板机的 IP 进来。
- 路径混淆 :把
base-path改成类似/sys-metrics-x9k2的无意义字符串,防自动化扫描器瞎撞。
2.3 自定义健康检查
默认的检查只会看 DB、Redis 通不通。但业务层面的关键状态,比如订单积压队列深度、第三方渠道开关,Actuator 管不到。自己实现一个 HealthIndicator 就能接进来:
java
@Component
public class OrderQueueHealthIndicator implements HealthIndicator {
@Override
public Health health() {
long pendingCount = orderService.getPendingQueueSize();
if (pendingCount > 10000) {
return Health.down().withDetail("queue_depth", pendingCount).build();
}
return Health.up().withDetail("queue_depth", pendingCount).build();
}
}
状态变 DOWN 后,K8s 的 readiness 探针会直接摘除该 Pod 的流量。配合自动重启或扩容,系统能自己缓过来。
3. 指标采集:Micrometer 埋点与 Prometheus 抓取
Micrometer 是 Spring Boot 3 默认的指标门面,底层对接 Prometheus、Datadog、NewRelic 都是一套 API,换厂商不用改业务代码。
3.1 依赖引入
xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
加完依赖重启,/actuator/prometheus 端点就自动吐 Prometheus 格式的纯文本了。
3.2 自动采集与基数爆炸陷阱
框架默认已经帮你把 JVM 内存、GC 停顿、HTTP 请求耗时都收上来了。但这里有个生产环境最容易踩的坑:基数爆炸(Cardinality Explosion)。
Prometheus 的存储模型对 Tag 的基数非常敏感。千万别把 userId、orderId、clientIp 这种无限增长的字段塞进 Tag 里,指标数量会指数级膨胀,直接把 Prometheus 内存打满导致 OOM。HTTP 的 uri 标签也必须规范化,/api/v1/orders/123 和 /api/v1/orders/456 在 Prometheus 眼里是两个完全不同的时序。
Spring Boot 3 已经废弃了旧版的 WebMvcTagsProvider,现在统一走 Observation API。你可以通过实现 HttpServerObservationConvention 或者在配置类里注册 ObservationRegistryCustomizer,用正则把 URI 路径里的数字段替换成占位符,从源头掐断基数膨胀。
3.3 业务埋点示例
框架自动采集的指标只能看宏观,核心业务链路的耗时和失败率必须自己埋:
java
@Service
public class PaymentService {
private final Timer paymentTimer;
private final Counter paymentFailedCounter;
public PaymentService(MeterRegistry registry) {
paymentTimer = Timer.builder("payment.duration.seconds")
.description("支付核心链路耗时")
.publishPercentiles(0.5, 0.95, 0.99) // 按需开分位数,别全开
.register(registry);
paymentFailedCounter = Counter.builder("payment.failed.count")
.tag("reason", "timeout")
.register(registry);
}
public void execute(PaymentRequest req) {
paymentTimer.record(() -> {
try { doPay(req); }
catch (TimeoutException e) { paymentFailedCounter.increment(); throw e; }
});
}
}
注意 publishPercentiles 开多了会加重客户端计算负担,生产一般保留 95 和 99 就够看了。
3.4 线程池指标暴露
自定义的 ThreadPoolTaskExecutor 默认不进 Micrometer,得手动绑一下:
java
@Bean
public MeterBinder threadPoolMetrics(ThreadPoolTaskExecutor executor) {
return registry -> {
Gauge.builder("thread.pool.active.count", executor,
e -> e.getThreadPoolExecutor().getActiveCount())
.tag("name", "async-executor").register(registry);
Gauge.builder("thread.pool.queue.size", executor,
e -> e.getThreadPoolExecutor().getQueue().size())
.tag("name", "async-executor").register(registry);
};
}
线上线程池打满往往比 CPU 100% 更致命,把这个 Gauge 接进大盘,能提前看到排队堆积的趋势。
3.5 Prometheus 抓取配置修正
K8s 环境下动态发现是标配。原配置里的 relabel_configs 语法有点问题,直接给个能跑的生产级片段:
yaml
scrape_configs:
- job_name: 'spring-boot-apps'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
scrape_timeout: 10s
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
靠 Pod 的 annotation 控制抓取目标,比硬编码 selector 灵活得多,运维和开发都不用反复改 Prometheus 配置。
4. 可视化:Grafana 大盘怎么画才不踩坑
Grafana 是事实标准,但别一上来就无脑导社区模板。模板往往塞满了用不到的面板,看着眼花。线上大盘讲究"少而精",优先把 RED 方法论(Rate, Errors, Duration)落地。
QPS 与流量基线 :用 sum(rate(http_server_requests_seconds_count{service="$svc"}[5m])) by (uri)。5 分钟的滑动窗口能平滑掉瞬时毛刺。如果流量突然断崖或翻倍,先别急着查代码,看下游网关或缓存层是不是有异常。
延迟分位(P95/P99) :HTTP 请求耗时在 Micrometer 里是 Histogram。表达式用 histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{service="$svc"}[5m])) by (le, uri))。线上千万别只看平均耗时,平均数会被大量快速请求稀释,真正卡用户体验的是那 1% 的长尾。
错误率与资源水位 :错误率用 5xx 状态码的 rate 除以总请求 rate。内存和 CPU 指标直接用 jvm_memory_used_bytes 和系统采集器数据。内存泄漏预警不能只看当前使用量,得结合 jvm_gc_pause_seconds_count 和 GC 耗时一起看。如果 Full GC 频率从一天几次变成几分钟一次,哪怕堆内存还没 OOM,也该介入排查了。
告警配置别设死阈值。用历史分位数做基线告警更靠谱,比如 P99 耗时超过过去 7 天同时间段 P99 的 2 倍,且持续 3 分钟。告警路由一定要分级,P0 直接电话+短信轰炸,P1 走企微/钉钉机器人,P2 仅记录到工单系统。每周定期清理误报,把告警疲劳压下来。
5. 链路追踪:Micrometer Tracing 与 OpenTelemetry
Spring Cloud Sleuth 在 Spring Boot 3 里彻底退场了,现在官方主推 Micrometer Tracing,底层走 OpenTelemetry 标准。
5.1 依赖与协议
xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
OTLP 是目前最通用的导出协议,Jaeger、Tempo、SkyWalking 都原生支持。别再用老版的 Zipkin HTTP v2 了。
5.2 上下文透传与异步断裂
分布式追踪靠的是 traceparent 请求头。Spring MVC、WebClient、RestTemplate 已经做了自动拦截和透传,大部分 HTTP 调用不需要你手动管。
最容易出问题的地方是异步线程和消息队列 。@Async 线程池如果不做上下文传递,Trace 就断了。Micrometer Tracing 提供了 @Observed 注解和 ObservationFilter,或者直接在创建 Runnable/Callable 时用 TracingContextPropagator 包装一下。Kafka/RabbitMQ 消费者同理,监听容器得配置消息头透传,否则链路到这里就黑盒了。
5.3 采样策略与 Jaeger 部署
全量采样在线上就是自杀,性能损耗至少 30%,存储成本也扛不住。生产环境通常用头部采样(Head-Based Sampling),配个 1%~5% 就够覆盖大部分异常路径了:
yaml
management:
tracing:
sampling:
probability: 0.05
zipkin:
tracing:
endpoint: "http://otel-collector:4318/v1/traces" # 走 OTLP 端点
如果某些核心链路(如支付、下单)需要全量追踪,别改全局配置,去写个 Sampler 按 URL 或 Header 动态调整。Jaeger 的部署现在都建议前面挡一层 OTel Collector,做批处理、尾采样和格式转换,别让业务 Pod 直接打 Jaeger。
6. 日志治理:结构化输出与 MDC 透传
日志要是还按 %d [%t] %-5p %c - %m%n 打,上了 ELK/Loki 就是灾难。非结构化日志根本没法做高效检索,关联分析更是无从谈起。
6.1 Logback JSON 改造
引入 logstash-logback-encoder,把日志打成一行 JSON。Micrometer Tracing 启动时会自动把 traceId 和 spanId 塞进 MDC,你只需要在 Encoder 里声明把 MDC 带出来:
xml
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"service":"order-service","env":"${ENV_NAME}"}</customFields>
<includeMdc>true</includeMdc>
</encoder>
includeMdc 设为 true 后,日志行里就会多一个 mdc 字段,直接包含链路 ID。采集端解析时不用写复杂的 Grok 正则,效率能高出一大截。
6.2 采集链路与降本
标准链路是:App -> Filebeat/FluentBit -> Kafka -> Logstash/Vector -> ES -> Kibana。
Filebeat 配置里把 json.keys_under_root: true 打开,避免 JSON 被嵌套在 json.log 里导致字段展开失败。ES 存储成本是大头,必须上 ILM 策略:热节点留 3-7 天查近期问题,温节点压缩归档 30 天,超过期限直接删或导冷存储。别把全量 DEBUG 日志往生产环境推,平时开 INFO,出问题时靠动态调日志级别(Actuator 的 /loggers 端点支持热更新)来捞现场。
6.3 排障动线
线上出问题,动线应该是死的:
告警推送带 TraceId -> 点链接进 Jaeger 看哪个 Span 标红耗时 -> 发现是慢 SQL 或下游超时 -> 复制 TraceId 进 Kibana 过滤该请求全量日志 -> 结合入参和堆栈定位代码或配置。
把这套动作固化成 Runbook,新人来了照做也能在 5 分钟内定界。对已知重试机制产生的 WARN 日志,提前在采集层做去重或降级,不然一次抖动能把你手机震没电。
7. 生产上线 Checklist 与 SRE 实践
监控体系不是一键部署完就万事大吉的,它得跟着业务迭代。上线前把下面这几条过一遍,能省掉 80% 的线上扯皮。
Actuator 层 :端口必须和业务隔离。/health 的详情展示要配合鉴权。K8s 探针配置确认过 readiness 依赖的中间件状态,别出现 Pod 已经 Ready 但 Redis 断连的假健康状态。
Metrics 层 :核对所有自定义指标命名符合 snake_case。重点查一遍有没有把高基数字段当 Tag。核心接口的 Timer 覆盖率必须到 100%,别留盲区。
Prometheus 层:数据保留期(Retention)根据磁盘规划设好,生产建议至少 15 天。单节点扛不住就上 Thanos 或 VictoriaMetrics 做长期存储和联邦聚合。抓取超时时间别设太短,GC 停顿期间端点响应慢,给 10 秒比较稳妥。
Tracing 层 :采样率先压到 5% 观察对 RT 的影响,稳定后再微调。检查所有异步线程、定时任务、MQ 消费端的上下文传递,断链的地方趁早补上 @Observed 或手动包装。
Logging 层 :确认 JSON 格式能被采集器正确解析,没有嵌套炸裂。ERROR 级别日志必须接实时告警。MDC 里的 traceId 字段在日志模板和告警模板里都要透出,方便一键跳转。
SRE 日常建议 :
别盲目堆指标。先定 SLO(比如可用性 99.95%),倒推 SLI(成功率、延迟分位),再落到具体的 Prometheus Query 上。遵循 Google SRE 的黄金信号:延迟、流量、错误、饱和度。这四个维度跑通了,大盘就立住了。告警一定要降噪,每周复盘误报,把固定阈值换成动态基线或分位数对比。定期搞一次混沌演练,主动注入依赖超时或 Pod 驱逐,看告警准不准、大盘跳不跳、值班同学能不能按 Runbook 在 MTTR 要求时间内止血。
体系搭好了只是起点。线上环境永远有意想不到的边界 case,保持对数据的敏感度,把每一次排障经验反哺到监控规则里,这套体系才会越用越顺手。
🎁 福利时间
如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。
知识库地址:https://farerboy.com/
