Actuator 与可观测性:健康检查、指标与监控

bee2026-10-0874 分钟0 次阅读
打开 Actuator 之后:健康检查怎么自定义、指标如何暴露给 Prometheus、日志级别动态调整、自定义端点——把"线上到底怎么样了"变成可回答的问题。
1 / 146
小节
〇、30 秒看懂
2 / 146

Actuator 就是 Spring Boot 给应用装上的仪表盘:程序跑起来以后,你自己看不到「我现在忙不忙、依赖好不好、内存还剩多少、跑的是哪个版本」,Actuator 把这些内部状态打包成一组 HTTP 地址(叫端点 endpoint),你或运维系统用浏览器 / curl 一问,它就答。它本身不画图、不告警,只负责如实回答。

3 / 146

先给五个词一句话解释:

4 / 146
  • 端点(endpoint):一个固定的 URL,比如 /actuator/health,访问它就返回一段 JSON
  • 暴露(expose):决定哪些端点真的能被 HTTP 访问;Boot 默认只开 health 和 info
  • 健康指示器(HealthIndicator):一个「我负责的这块好还是坏」的自检小函数,容器把它们的结果汇总成总状态
  • Micrometer:指标门面(相当于日志界的 SLF4J),代码只管埋点,后端换成 Prometheus 还是 Datadog 都不用改业务代码
  • 探针(probe):Kubernetes 定期来敲的两个门——liveness 问「进程还活着吗」,readiness 问「现在能接流量吗」
5 / 146
类比

汽车仪表盘 + OBD 诊断口。转速表和故障灯是 metrics 与 health——司机随时能看;而修车师傅插上 OBD 口读故障码,那是 loggers、threaddump、heapdump 这些诊断端点。仪表盘可以给你看,OBD 口却能让懂的人改写行车电脑——所以修理厂才允许接它(内网 + 鉴权),绝不能把线头扔在马路边。

6 / 146
类比

健康探针 = 值班交接前先自测血压。交班(K8s 放流量)前你要量血压确认自己能上岗,这是 readiness;而「你今天状态不好就送你去医院抢救」是 liveness——判据只能是你自己的身体(进程),不能是「救护车今天晚点了」(数据库抖动)。否则网络一抖,全科室的护士都被拉去抢救,本来还能干活的也被清空了。

7 / 146
架构图
图 · Actuator 端点家族
图 · Actuator 端点家族
8 / 146

这张树状图是全篇地图:左边一支随便公开,中间一支限内网,右边一支默认干脆不开。第三节的表会把它们逐个讲清楚。

9 / 146

学完这一篇,你应该能回答三个问题:

10 / 146
  • 为什么 /actuator/health 显示 DOWN 却看不出是哪一块坏了?怎么让它自己报出来?
  • include: "*" 到底危险在哪?生产上线时那几行配置该怎么写?
  • 「接口变慢」这件事,能不能提前十分钟从某个指标上看出来?看哪个?
11 / 146
小节
一、可观测性三支柱:给"线上怎么样了"一个答案
12 / 146

项目上线后,最怕的一句话是「系统好像有点慢」。这句话背后其实是三个具体问题:现在发生了什么、发生了什么变化、为什么发生。业界用「可观测性三支柱」来回答它们:

13 / 146
对照表
支柱回答什么问题数据形态常用工具
日志 Logging某个时刻具体发生了什么离散的事件文本Logback + ELK / Loki
指标 Metrics整体趋势和量级如何可聚合的时序数字Micrometer + Prometheus + Grafana
追踪 Tracing一次请求经过了哪些环节、慢在哪带因果关系的链路OpenTelemetry / SkyWalking
14 / 146

日志擅长「精确定位一次事故」,但你要统计「过去一小时的错误率」,就得把几百万行日志数一遍——那太慢了。指标把「可聚合的数字」提前算好:QPS、响应时间分位值、错误率、连接池使用率,都是随时间变化的时序数据,天然适合看板与告警。追踪则串起跨服务的调用链。

15 / 146

Spring Boot 的 Actuator 恰好是获取后两类数据(尤其是指标)的入口:它把应用内部的运行状态通过一组 HTTP 端点暴露出来。这一篇就从「打开 Actuator」讲到「接上 Prometheus 和 Grafana」。

16 / 146
架构图
图 1 · Actuator 的四类能力
图 1 · Actuator 的四类能力
17 / 146
说明

Actuator 不是监控系统的全部,而是被监控的一端。它负责「产出」健康状态和指标,真正的存储、可视化与告警由 Prometheus、Grafana 承担。

18 / 146
小节
二、快速启用:先暴露,再收紧
19 / 146

加一个起步依赖即可,什么都不用改就能用:

20 / 146
xml
<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-actuator</artifactId></dependency>
21 / 146

默认只暴露 health 和 info 两个端点,其余端点在 Web 上不可见——这是 Boot 出于安全的默认选择。要用更多,得显式 expose:

22 / 146
代码对照
代码yaml
management:  endpoints:    web:      exposure:        include: health,info,metrics,prometheus   # 只开需要的  endpoint:    health:      show-details: when_authorized              # 详情仅授权用户可见  server:    port: 9090                                   # 管理端口与业务端口分离
解读
  • include 用白名单而不是 ,*生产环境永远不要写 include: ""*
  • management.server.port=9090 让管理端点走独立端口,业务端口可以只对公网开放、管理端口只对内网开放
  • show-details 控制 /health 是否展示各组件细节,公网暴露时应收紧

警告:include: "*" 加上把应用暴露到公网,等于把 env(可能含数据库密码)、heapdump(可下载整个堆内存)、shutdown(可远程关停应用)都交了出去。历史上多次真实事故都源于此。

23 / 146

先跑一遍内核实验,看清楚「暴露面」这道闸门到底怎么开合:

24 / 146
内核实验
TeaVM暴露面控制:谁能看见哪些端点未启动
选「暴露面控制」:注意 /actuator 索引页只列出 include 里写过的端点,其余一律 404
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
25 / 146

再把闸门全推开,看攻击者三十秒内能拿走什么:

26 / 146
内核实验
TeaVM全暴露的风险现场:从 env 到 shutdown未启动
切到「全暴露的风险」:/env、/heapdump、/loggers、/shutdown 各自的后果逐一演示
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
27 / 146

这两种写法的差别不用背,拨一下开关就能看见。下面是本篇的暴露面沙盘——左边是运维在群里喊「快给我看全部」的写法,右边是上线前的正确写法:

28 / 146
沙盘
沙盘暴露面开关:include=* vs 白名单
运行结果
GET :9090/actuator -> _links: [self, health, info, metrics, prometheus]
GET :9090/actuator/env -> 404 Not Found
Prometheus scrape OK: 15s interval
#推荐形态:四个端点 + 独立端口
这是本篇第一档练习的目标配置:观测能力齐全,敏感端点一个都不露
29 / 146

顺手确认一件事:这些配置写在哪个文件里、谁覆盖谁,直接决定「我在本地改了 yml 为什么线上没生效」。

30 / 146
内核实验
TeaVM这行 management 配置到底被谁覆盖了未启动
选「谁覆盖谁」:命令行 > JVM 参数 > 环境变量 > application-{profile}.yml > application.yml
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
31 / 146

两种结局对照得最清楚的还是这张图:

32 / 146
架构图
图 · 全暴露的内网便利 vs 生产裸奔事故
图 · 全暴露的内网便利 vs 生产裸奔事故
33 / 146
小节
三、核心端点逐个讲
34 / 146

Actuator 端点很多,但常用的就十来个。按用途和安全等级整理如下(同一批端点在开篇那张端点家族树里是按「谁能访问」分三支长出来的,这里改按风险等级重排一遍):

35 / 146
对照表
端点用途安全等级
health健康状态汇总,供探针与负载均衡使用低(可公开,细节要收紧)
info构建信息、Git 提交、自定义属性低
metrics运行指标(可下钻到具体指标)中
prometheus指标转 Prometheus 文本格式中
loggers查询/动态修改日志级别中
beans容器内所有 Bean 及其依赖关系高
mappings所有 URL 与处理方法的映射高
conditions自动配置的条件求值报告高
threaddump线程快照,排查卡死高
heapdump下载堆内存快照(文件很大)极高
env / configprops环境变量与配置属性(可能含敏感值)极高
httpexchanges最近的 HTTP 请求记录高
36 / 146
要点

beans、mappings、conditions 是开发期诊断神器——「这个 Bean 为什么没注册进来」「这个 URL 到底映射到哪个方法」,一眼就能看到。但它们也暴露了内部结构,生产中应当限制访问来源。

37 / 146

端点名记不住没关系,先记住「你要问的问题」。下面六种典型疑问,各配一个第一时间该打开的端点——配错了,它会把选错的后果讲给你听:

38 / 146
配对闯关
闯关六个疑问,配第一个该打开的端点已配对 0/6 · 配错 0
两列都打乱了;判据是「这个问题属于看状态、查原因,还是要动手」——选错端点,你会在 404 或敏感数据前浪费半小时
先点左边一个
39 / 146
小节
四、Health 健康检查:从默认到自定义
40 / 146

/actuator/health 聚合了多个内置指示器(DB、Redis、磁盘空间、ping 等),返回结构化的 JSON:

41 / 146
代码对照
代码json
{  "status": "UP",  "components": {    "db": { "status": "UP", "details": { "database": "MySQL", "validationQuery": "isValid()" } },    "diskSpace": { "status": "UP", "details": { "free": 107374182400, "total": 536870912000 } },    "redis": { "status": "UP", "details": { "version": "7.2.4" } },    "ping": { "status": "UP" }  }}
解读
  • 顶层 status 是所有组件的汇总:任一组件 DOWN,整体就 DOWN
  • HTTP 状态码映射:UP → 200,DOWN → 503
  • db、redis 指示器由对应 starter 自动装配,你写了数据源它就自动具备
42 / 146

当依赖了健康检查不该覆盖的第三方服务时,写一个自定义 HealthIndicator:

43 / 146
java
@Component("smsProvider")public class SmsProviderHealthIndicator implements HealthIndicator {    private static final Logger log = LoggerFactory.getLogger(SmsProviderHealthIndicator.class);    private final SmsClient smsClient;    public SmsProviderHealthIndicator(SmsClient smsClient) {        this.smsClient = smsClient;    }    @Override    public Health health() {        try {            long latency = smsClient.ping();            return Health.up()                    .withDetail("latencyMs", latency)                    .withDetail("endpoint", smsClient.getEndpoint())                    .build();        } catch (Exception ex) {            log.warn("短信服务探活失败", ex);            return Health.down(ex).withDetail("endpoint", smsClient.getEndpoint()).build();        }    }}
44 / 146

健康检查在 K8s 里对应探针(probe),且 liveness 与 readiness 语义完全不同,必须分组,否则会出现「数据库抖动导致实例被整体摘除」的灾难:

45 / 146
代码对照
代码yaml
management:  endpoint:    health:      group:        liveness:          include: ping            # 只判断"进程是否活着",别放进 db        readiness:          include: db,redis        # 判断"能否接收流量",可包含依赖
解读

坑:如果你把 db 放进 liveness,数据库短暂抖动会让 K8s 判定容器「死了」并重启进程——一个本来几十秒能恢复的网络问题,被放大成全体实例滚动重启。liveness 只该判断进程自身,依赖健康交给 readiness。

46 / 146

聚合过程值得亲眼看一次:三个指示器里有一个 DOWN,顶层状态是怎么被「投票」出来的,细节又在什么条件下才肯显示:

47 / 146
内核实验
TeaVM健康指示器聚合:谁把整体拉成 DOWN未启动
切到「健康指示器聚合」:注意 show-details 不同时,components 是否可见
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
48 / 146

act health 实验把结果给你看了,但「整体」这个词背后是一次真正的投票。把投票规则摊成一次单步执行,一行一行走——连点「下一步」,注意严重度排名如何在第 5 步改判全局:

49 / 146
单步调试台
单步台单步走一遍:三个指示器如何投票出整体状态1 / 7
连点「下一步」;第 5 步开始注意严重度排名,一个 DOWN 如何把整场改判
被调试的代码
1List<HealthIndicator> indicators = List.of(ping, db, redis); // ① 注册顺序
2Status overall = Status.UNKNOWN; // ② 起点:还没有结论
3for (HealthIndicator ind : indicators) { // ③ 逐个点名
4 Status cur = ind.health().getStatus(); // ④ 它这一轮的回答
5 if (severity(cur) > severity(overall)) overall = cur; // ⑤ 更严重者上位
6}
7return overall; // ⑥ 投票结果
此刻的变量
指示器数3
注册顺序ping → db → redis
调用栈
1HealthEndpoint.read
2CompositeHealthIndicator.health
1三个指示器按注册顺序排队。顺序不影响最终票数,只影响你在 details 里「先看见谁」。
50 / 146

再把视角拉到上线现场——探针在启动、抖动、恢复、停机这四个时刻各自答什么:

51 / 146
原理动画
动图 · 上线前后的健康探针全过程
动图 · 上线前后的健康探针全过程
52 / 146

配合优雅停机的完整链路再看一遍,你就知道为什么「摘流量」必须排在「关进程」前面:

53 / 146
内核实验
TeaVM就绪探针 → 摘流量 → 排空 → 退出未启动
先看「就绪探针」什么时候变 true,再切「摘流量与排空」看在途请求怎么被等完
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
54 / 146
小节
五、Metrics 指标:Micrometer 门面
55 / 146

Spring Boot 用 Micrometer 作为指标门面(就像 SLF4J 之于日志),业务只依赖 MeterRegistry,底层可以换成 Prometheus、Datadog 等。四种最基本的计量器要分清:

56 / 146
对照表
计量器语义例子
Counter只增不减的累计量下单总数、错误次数
Gauge瞬时值,可增可减当前在线连接数、队列长度
Timer统计耗时与调用次数接口响应时间、方法执行耗时
DistributionSummary统计数值的分布订单金额分布、请求体大小
57 / 146

内置指标按来源分类,常用的是这些:

58 / 146
对照表
指标前缀来源为什么关注
jvm.*JVM(内存、GC、线程)判断是否内存泄漏、GC 是否频繁
http.server.requestsWeb 请求QPS、P99 响应时间、错误率
hikaricp.connections.*数据库连接池连接是否被占满(慢查询先兆)
tomcat.threads.*Tomcat 线程池线程是否耗尽
system.cpu.usage操作系统CPU 是否饱和
59 / 146
提示

hikaricp.connections.pending(等待连接的线程数)持续大于 0,往往先于「接口变慢」出现,是最有价值的早期告警指标之一。

60 / 146

这些数字不是看板变出来的,而是 /actuator/metrics 一层层问出来的:

61 / 146
内核实验
TeaVM指标怎么取:从名字列表到下钻到标签未启动
切到「指标怎么取」:先 GET /actuator/metrics 拿名单,再带 ?name= 下钻看 availableTags
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
62 / 146
小节
六、自定义业务指标:把业务量变成可观测数字
63 / 146

技术指标告诉你「系统健康」,业务指标告诉你「业务健康」。注入 MeterRegistry 即可埋点:

64 / 146
代码对照
代码java
@Servicepublic class OrderMetrics {    private final Counter orderCreated;    private final Counter orderFailed;    private final Timer payLatency;    public OrderMetrics(MeterRegistry registry) {        this.orderCreated = Counter.builder("beeorder.order.created")                .description("下单成功总数")                .register(registry);        this.orderFailed = Counter.builder("beeorder.order.failed")                .description("下单失败总数")                .register(registry);        this.payLatency = Timer.builder("beeorder.pay.latency")                .description("支付回调处理耗时")                .publishPercentileHistogram()                .register(registry);    }    public void markCreated() { orderCreated.increment(); }    public void markFailed() { orderFailed.increment(); }    public void recordPay(Runnable task) {        payLatency.record(task);   // 自动计时并统计调用次数    }}
解读
  • Counter 单调递增,适合「累计次数」;increment 一行搞定
  • Timer 同时记录次数与耗时,还能配 publishPercentileHistogram 输出分位值
  • 命名建议加业务前缀(如 beeorder.),避免与内置指标混淆
65 / 146

方法级耗时也可以直接用 @Timed 注解(需注册 TimedAspect):

66 / 146
代码对照
代码java
@Timed(value = "beeorder.product.detail", description = "商品详情查询耗时")public ProductVO detail(Long productId) {    return productRepository.findById(productId).map(ProductVO::from).orElseThrow(NotFoundException::new);}
解读

要点:业务指标是「告警的起点」。把「支付成功率 = 支付成功数 / 支付回调数」暴露成指标,才能配出「成功率跌破 99% 就告警」这样的规则,而不是等用户投诉。

67 / 146
小节
七、对接 Prometheus + Grafana
68 / 146

先把指标转成 Prometheus 能读的格式——加一个注册表依赖:

69 / 146
xml
<dependency>    <groupId>io.micrometer</groupId>    <artifactId>micrometer-registry-prometheus</artifactId></dependency>
70 / 146

访问 /actuator/prometheus 会得到如下文本(节选):

71 / 146
text
# HELP http_server_requests_seconds Timer for HTTP server requests# TYPE http_server_requests_seconds histogramhttp_server_requests_seconds_count{method="POST",uri="/api/orders",status="201"} 1284.0http_server_requests_seconds_sum{method="POST",uri="/api/orders",status="201"} 41.52http_server_requests_seconds_bucket{method="POST",uri="/api/orders",status="201",le="0.1"} 990.0http_server_requests_seconds_bucket{method="POST",uri="/api/orders",status="201",le="0.5"} 1270.0# HELP beeorder_order_created_total Orders created successfully# TYPE beeorder_order_created_total counterbeeorder_order_created_total 13245.0
72 / 146

Prometheus 通过配置定期抓取这个端点,再把数据存成时序:

73 / 146
yaml
scrape_configs:  - job_name: beeorder    metrics_path: /actuator/prometheus    scrape_interval: 15s    static_configs:      - targets: ['beeorder-app:9090']
74 / 146

Grafana 负责把 Prometheus 里的时序画成看板,配上告警规则即可自动通知:

75 / 146
代码对照
代码yaml
groups:  - name: beeorder-alerts    rules:      - alert: HighErrorRate        expr: |          sum(rate(http_server_requests_seconds_count{status="500"}[5m]))          / sum(rate(http_server_requests_seconds_count[5m])) > 0.01        for: 5m        labels: { severity: critical }        annotations:          summary: "5xx 错误率超过 1%(持续 5 分钟)"
解读
  • rate(...[5m]) 求 5 分钟内的每秒速率,比裸计数更能反映当下
  • for: 5m 表示「持续越界 5 分钟才告警」,避免瞬时抖动误报
  • 一个看板至少放三张图:请求量与错误率、响应时间 P99、连接池与线程池水位
76 / 146
原理动画
动图 · 指标从应用到看板
动图 · 指标从应用到看板
77 / 146
小节
八、动态调整日志级别:不用重启的排障
78 / 146

线上接口报错,但日志级别是 INFO,看不出细节。重启改配置代价太大——用 loggers 端点在线修改:

79 / 146
代码对照
代码bash
# 1) 查看某个 logger 当前级别curl http://localhost:9090/actuator/loggers/com.beeorder.order# 2) 临时把订单包调到 DEBUGcurl -X POST http://localhost:9090/actuator/loggers/com.beeorder.order \     -H "Content-Type: application/json" \     -d '{"configuredLevel":"DEBUG"}'# 3) 排障结束,调回 INFOcurl -X POST http://localhost:9090/actuator/loggers/com.beeorder.order \     -H "Content-Type: application/json" \     -d '{"configuredLevel":"INFO"}'
解读
  • 修改立即生效、无需重启,作用范围精确到包或类
  • 只影响当前实例;多实例部署要逐个改,或用配置中心统一下发
  • 用完务必调回,否则 DEBUG 日志会很快把磁盘写满

提示:配合 httpexchanges 端点可以回看最近若干次请求的路径、状态与耗时,定位「哪个请求出问题」往往比翻全文日志更快。

80 / 146

这个端点改的到底是什么?其实是 Logback 里那个 logger 对象的级别字段——整条链路跑一遍就明白了:

81 / 146
内核实验
TeaVMloggers 端点改级别的完整链路未启动
选「级别过滤」看 INFO 被拦在 Appender 之前;再切「异步 Appender」理解为什么 DEBUG 打满磁盘会拖慢请求
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
82 / 146
小节
九、info 端点与构建信息
83 / 146

/actuator/info 用于展示构建信息、Git 提交号与自定义属性,方便「线上跑的到底是哪个版本」:先让 Maven 生成构建元数据,再配置暴露:

84 / 146
xml
<plugin>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-maven-plugin</artifactId>    <executions>        <execution>            <goals>                <goal>build-info</goal>   <!-- 生成 META-INF/build-info.properties -->            </goals>        </execution>    </executions></plugin>
85 / 146
yaml
management:  info:    git:      mode: full        # 需要 spring-boot-starter-actuator + git-commit-id 插件    env:      enabled: true
86 / 146

这样调用方(或值班同学)访问 /actuator/info,就能看到版本号、构建时间、Git 提交 id——排查「线上是新版还是旧版」这类问题不再靠猜。

87 / 146
注意

Git 信息需要额外引入 git-commit-id-maven-plugin 并保证构建时有 .git 目录;CI 环境用浅克隆时 Git 信息可能为空,需配置插件抓取。

88 / 146
内核实验
89 / 146
小节
十、安全加固清单:别把后门开在公网上
90 / 146

Actuator 好用,但默认是一把双刃剑。上线前逐条对照:

91 / 146
对照表
加固项做法
端点白名单include 只列 health,info,metrics,prometheus
管理端口隔离management.server.port 独立端口,仅内网可达
敏感端点禁用关闭 env、configprops、heapdump(或 enabled: false)
强制鉴权用 Spring Security 保护 /actuator/**,只放行 health
禁用关停management.endpoint.shutdown.enabled: false(默认即 false,别打开)
健康细节收紧公网环境 show-details: never,仅内部探针用 when_authorized
92 / 146

清单背不下来没关系——把该收的几行勾出来,让生成器替你拼一份能直接进生产的 yml 骨架:

93 / 146
生成器
生成器把加固清单落成几行 management 配置application.yml1 / 3
只勾「management」得到白名单 + 9090 管理端口 + liveness/readiness 分组;再补「logging」看级别与滚动策略,补「profiles + 分档配置」看同一份文件如何按环境收紧——对着上面表格逐行核对
产物
server:
  port: 8080

spring:
  application:
    name: demo-service

management:
  server:
    port: 9090                            # 管理端口与业务端口隔离
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus   # 白名单,绝不写 *
  endpoint:
    health:
      show-details: when_authorized
      group:
        liveness: { include: ping }
        readiness: { include: db,redis,diskSpace }
勾了这些,代价与理由在这里
management白名单 + 独立管理端口是 Actuator 的安全底线,* 是泄露事故第一名。
94 / 146
注意

生成器给出的是骨架,不是免检通道——show-details 的取值、网关是否屏蔽 /actuator 路径、网络策略是否只放行内网,这三件事生成器管不了,得你自己逐条确认。

95 / 146
警告

heapdump 端点可以下载整个堆内存快照,里面可能包含数据库连接串、密钥、用户数据。除非在受控的内网排障窗口,否则一律禁用它。

96 / 146
小节
十一、两个坑 + 决策 + 总结
97 / 146
坑

把 db 检查放进 liveness 分组,是「数据库抖动导致实例被负载均衡摘除、甚至被 K8s 重启」的经典事故。记住分组原则:liveness 只问「进程活着吗」,readiness 才问「依赖都好吗、能不能接流量」。

98 / 146
坑

management.endpoints.web.exposure.include: "*" 加上 0.0.0.0 暴露,等于把 /actuator/env、/actuator/heapdump、/actuator/shutdown 全部挂在公网。真实世界已有无数应用因此泄露配置或被人一键关停。

99 / 146
决策
决策健康检查要不要包含数据库?
100 / 146

工具认全了、坑也点过了,最后把「出事那晚你会按什么顺序伸手」串成一条闭环——每一格都用本篇学过的一个端点,一格都不许跳:

101 / 146
原理动画
动图 · 一次线上排障闭环
动图 · 一次线上排障闭环
102 / 146

再配一句总结:观测类端点负责「说得清」,诊断类端点负责「查得到」,而安全加固决定「谁能问」——三者缺一,开篇那张四类能力图就只是一张能力表,而不是一套排障流程。

103 / 146
小节
十二、常见报错速查
104 / 146

这一节的每一条都可以整段复制去搜索。新手最常卡在四个地方:端点根本 404、健康 DOWN 却不知是谁、/env 泄密、管理端口和业务端口搞混。

105 / 146
对照表
现象原文(片段)真实原因30 秒自救深挖看第几篇
Whitelabel Error Page ... type=Not Found, status=404 访问 /actuator 或 /actuator/health 都是它两种可能:压根没引 spring-boot-starter-actuator;或者引了但该端点不在 management.endpoints.web.exposure.include 白名单里(Boot 默认只放 health、info)先 `mvn dependency:tree \grep actuator 确认依赖在,再 curl localhost:8080/actuator 看索引页 _links` 到底列了哪几个——索引页是唯一不会骗你的入口本篇第二节 · 暴露面沙盘
/actuator/health 返回 {"status":"DOWN"},但响应体里没有 componentsmanagement.endpoint.health.show-details 默认是 never;聚合规则又让任一组件 DOWN 拉低整体——你看不到是谁临时设 show-details: always(仅限内网),或用 management.endpoint.health.group.<name>.show-details=always 只对某个分组放开;然后按 components 找真凶本篇第四节 · health 聚合实验
GET /actuator/env 里看到 spring.datasource.password 是一串 ******,但自己写的 app.oss.accessKey、app.wx.secret2 却是明文脱敏靠属性名正则匹配(默认覆盖 password、secret、key、token 等词),名字起了别名就命中不了;@ConfigurationProperties 绑定的对象在 configprops 里更是整块输出自定义正则:management.endpoint.env.keys-to-sanitize=password,secret,key,token,certificate,accessKey(Boot 3.x 对应 management.endpoint.env.show-values=never + keys-to-sanitize),并把 configprops 一起关掉#35 配置与 Profile
management.server.port=9090 配上了,K8s 探针却还是 404 / 连不上探针地址仍写着业务端口 8080;换了管理端口后 /actuator/** 只监听 9090把 livenessProbe/readinessProbe 的 port: http(8080) 改成 port: management(9090),并确认 Service 的网络策略允许集群内访问 9090本篇第四节 · 探针动图
8080 和 9090 都通了,但线上 Nginx 一转发就把 /actuator 送到公网没隔离:要么两端口同源,要么网关按路径转发时未屏蔽 /actuator网关显式拒绝该路径;更稳的做法是让管理端口不写入 Ingress/Service 的对外端口列表第十节加固清单
POST /actuator/loggers/com.example.order 返回 415 Unsupported Media Type 或 400少了 Content-Type: application/json,或 body 字段写成 level(正确字段是 configuredLevel)curl -X POST -H "Content-Type: application/json" -d '{"configuredLevel":"DEBUG"}';传 {"configuredLevel":null} 表示恢复继承第八节
启动日志出现 Cannot find factory with name "Prometheus" 或根本没有 /actuator/prometheus只加了 actuator starter,没加 io.micrometer:micrometer-registry-prometheus;或加了却没把它 include 出来补依赖 → 重启 → include 加 prometheus → curl :9090/actuator/prometheus 应返回 # HELP jvm_... 文本第七节
明明加了依赖,/actuator/shutdown 一直 404,运维说「要能远程重启」shutdown 端点默认连 Bean 都不创建,需要 management.endpoint.shutdown.enabled=true 且被 include,两道开关都开才有想清楚再说:容器环境应该用 SIGTERM + 优雅停机,而不是 HTTP 关停;确需打开就必须加鉴权第四节探针动图 · #46 交付
heapdump 下载下来几百 MB,MAT 打不开或机器 OOM生成堆快照会触发 STW,大堆上这一步能让服务停十几秒——这本身就是次事故只在受控窗口执行,或改用 jcmd <pid> GC.heap_dump;生产环境一律关闭该端点第十节加固清单
106 / 146

表里的报错都还能靠「现象原文」自救。但有一类事故,报错本身不点名凶手——它把线索藏在括号里的数字上。下面这份堆栈来自一次真实的「Pod 被轮转」事故(探针线程上抛出的异常节选),先别看结论,点出你认为的凶手帧:

107 / 146
报错急救
报错急救SQLTransientConnectionException: 探针等不到连接
探针抖动、Pod 被轮转:凶手躲在健康指示器的第 38 行

凌晨 01:40,值班手机炸响:pay-api 的实例十分钟内被重启了两轮,每次都是探针先超时。重启后立刻恢复,过一阵又犯。这是运维从被杀实例的日志里捞出的堆栈节选——探针线程上抛出的异常。先别看结论,点出你认为的凶手帧。

java.sql.SQLTransientConnectionException: beeHikari - Connection is not available, request timed out after 1000ms (total=20, active=20, idle=0, waiting=31)
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:692)
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:189)
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:160)
at org.springframework.jdbc.core.JdbcTemplate.queryForObject(JdbcTemplate.java:802)
at com.bee.pay.ReportHealthIndicator.health(ReportHealthIndicator.java:38)
at org.springframework.boot.actuate.health.HealthEndpoint.health(HealthEndpoint.java:53)
at org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(AbstractWebMvcEndpointHandlerMapping.java:367)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)
点你认为的「凶手行」(可反复试)
不会也没关系:先猜异常名,再猜哪一行在做决定。
108 / 146
随堂自测
随堂自测生产环境只需要 Prometheus 抓取指标和 K8s 探活,其余排障都在内网做。下面哪一份配置最合适?
先自己选一个,选中立刻告诉你对不对
109 / 146
随堂自测
随堂自测/actuator/health 显示 status=DOWN,你希望五分钟内定位到具体是哪一块坏了。最快的正确动作是?
先自己选一个,选中立刻告诉你对不对
110 / 146
小节
十三、动手练习
111 / 146
小节
第一档 · 照做
112 / 146

目标:搭一个「本地就能跑通、配置就是生产骨架」的最小监控工程,亲眼看到白名单带来的 200/404 差异、/health 的 components、以及一行 curl 把日志级别切成 DEBUG。

113 / 146

动手之前,先在这台内核控制台里把这五格实验按顺序跑一遍——每格对应本篇的一个结论,回显全部由内核算出来:

114 / 146
内核控制台
115 / 146
提示

五格跑完,你就把「暴露 → 收紧 → 观测 → 探活」完整走了一遍;接下来三档练习,是把这套流程搬进你自己的工程。

116 / 146

第一步,pom.xml:

117 / 146
xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>3.3.4</version>        <relativePath/>    </parent>    <groupId>com.example</groupId>    <artifactId>monitor-lab</artifactId>    <version>0.0.1-SNAPSHOT</version>    <properties>        <java.version>17</java.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <!-- 监控本体 -->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-actuator</artifactId>        </dependency>        <!-- Prometheus 文本格式注册表 -->        <dependency>            <groupId>io.micrometer</groupId>            <artifactId>micrometer-registry-prometheus</artifactId>        </dependency>        <!-- 制造一个真实的 db 健康指示器 -->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-jdbc</artifactId>        </dependency>        <dependency>            <groupId>com.h2database</groupId>            <artifactId>h2</artifactId>            <scope>runtime</scope>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>                <executions>                    <execution>                        <goals>                            <goal>build-info</goal>   <!-- 让 /actuator/info 有内容 -->                        </goals>                    </execution>                </executions>            </plugin>        </plugins>    </build></project>
118 / 146

第二步,src/main/resources/application.yml:

119 / 146
yaml
server:  port: 8080                      # 业务端口:对公网开放spring:  application:    name: monitor-lab  datasource:    url: jdbc:h2:mem:lab;DB_CLOSE_DELAY=-1    driver-class-name: org.h2.Drivermanagement:  server:    port: 9090                    # 管理端口:仅内网可达  endpoints:    web:      base-path: /actuator      exposure:        include: health,info,metrics,prometheus   # 白名单,绝不写 *  endpoint:    health:      show-details: when_authorized      group:        liveness:          include: ping        readiness:          include: db,diskSpace  info:    env:      enabled: truelogging:  level:    com.example.monitorlab: INFO
120 / 146

第三步,一个自定义健康指示器和一个下单计数埋点(包名 com.example.monitorlab):

121 / 146
java
package com.example.monitorlab;import java.util.concurrent.atomic.AtomicLong;import org.springframework.boot.actuate.health.Health;import org.springframework.boot.actuate.health.HealthIndicator;import org.springframework.stereotype.Component;@Component("inventoryCache")public class InventoryCacheHealthIndicator implements HealthIndicator {    private final AtomicLong lastSyncOkAt = new AtomicLong(System.currentTimeMillis());    @Override    public Health health() {        long staleMs = System.currentTimeMillis() - lastSyncOkAt.get();        if (staleMs > 60_000) {            return Health.down().withDetail("staleMs", staleMs).withDetail("reason", "库存缓存超过 60s 未同步").build();        }        return Health.up().withDetail("staleMs", staleMs).build();    }    public void markSynced() {        lastSyncOkAt.set(System.currentTimeMillis());    }}
122 / 146

第四步,启动并按顺序执行这几条 curl(注意端口分别是 8080 与 9090):

123 / 146
bash
# ① 索引页:确认暴露面只有四个curl -s http://localhost:9090/actuator# ② 健康总览(未授权,所以看不到 components)curl -s http://localhost:9090/actuator/health# ③ 分组探针地址curl -s http://localhost:9090/actuator/health/livenesscurl -s http://localhost:9090/actuator/health/readiness# ④ 业务端口上访问管理端点 → 应当 404curl -i -s http://localhost:8080/actuator/health | head -1# ⑤ 敏感端点应当 404curl -i -s http://localhost:9090/actuator/env | head -1# ⑥ 动态改日志级别,然后立刻观察curl -s -X POST http://localhost:9090/actuator/loggers/com.example.monitorlab \     -H "Content-Type: application/json" -d '{"configuredLevel":"DEBUG"}'curl -s http://localhost:9090/actuator/loggers/com.example.monitorlabcurl -s -X POST http://localhost:9090/actuator/loggers/com.example.monitorlab \     -H "Content-Type: application/json" -d '{"configuredLevel":null}'# ⑦ 指标下钻curl -s http://localhost:9090/actuator/metricscurl -s "http://localhost:9090/actuator/metrics/hikaricp.connections.pending"curl -s http://localhost:9090/actuator/prometheus | grep -E "^jvm_memory_used_bytes|^http_server_requests" | head -3
124 / 146

预期输出(② 在未授权时的默认形态,③ 的两个分组,④⑤ 的状态码):

125 / 146
json
{ "status": "UP" }
126 / 146
json
{ "status": "UP", "group": "liveness" }
127 / 146
json
{ "status": "UP", "group": "readiness", "components": { "db": { "status": "UP" }, "diskSpace": { "status": "UP" } } }
128 / 146
text
HTTP/1.1 404HTTP/1.1 404
129 / 146

验收清单:① 你能解释为什么 liveness 里没有 db;② 把 show-details 改成 always 后再访问 ②,能看到 inventoryCache 这个自定义指示器出现在 components 里;③ 说清 ④ 与 ⑤ 两条 404 的原因分别是什么(前者是端口隔离,后者是白名单)。

130 / 146
小节
第二档 · 变体
131 / 146

只改一处,结论完全不同:

132 / 146
  1. 把 include 换成 "*",重启。你会观察到:curl :9090/actuator 的 _links 一下多出十几个键,其中 env 里 spring.datasource.password 被打码,但你新加的 app.my.access-key-alias=plain-secret 会原样输出——这就是脱敏规则的真实边界。改完记得换回去。
  2. 把 management.server.port 那行注释掉。你会观察到:④ 变成 200,管理端点重新挂在业务端口上;这时如果你在网关上按路径转发,/actuator 就等于对公网开放。这一条能让你真正理解「端口隔离」挡的是什么。
  3. 把 db 加进 liveness 分组,然后用错误的 URL 制造启动失败(jdbc:h2:mem:lab;INIT=RUNSCRIPT FROM 'nonexistent.sql')。你会观察到:/actuator/health/liveness 直接 DOWN,模拟出「K8s 会把本来只需摘流量的实例反复重启」。
  4. 把 InventoryCacheHealthIndicator 里的阈值从 60_000 改成 -1。你会观察到:顶层 status 变 DOWN 且 HTTP 状态码变 503,但默认看不到原因——于是你被迫去调 show-details,正好复现第十二节第二条报错现场。
  5. 在第 ⑥ 步之后连续请求接口十几次。你会观察到:DEBUG 日志哗哗地出来;把它调回 null(继承父级)后立刻安静。这就是「不用重启的排障」的价值与风险。
133 / 146

提示:做完第 3 条再回看第四节的探针动图,两边时间点应当完全对得上。

134 / 146
小节
第三档 · 造一个
135 / 146

给自己做一个「值班自助排障面板」:不装 Grafana 也能在浏览器一屏看清关键结论。

136 / 146

需求:

137 / 146
  • 一个自定义端点 @Endpoint(id = "opsboard"),@ReadOperation 返回一张聚合 JSON:JVM 堆使用率、GC 次数增量、连接池 pending 数、最近 5 分钟 5xx 比例、t_order 行数、当前 Git commit、运行时长
  • 数据全部来自注入的 MeterRegistry、DataSource、HealthContributor,不许新建数据库表
  • 一个 @TransactionalEventListener(AFTER_COMMIT) 或拦截器记录业务成功率,作为面板的一格
  • 面板端点默认不暴露,只能通过内网 profile 的 include 打开;并在文档里写清「怎么开、什么时候关」
  • 附一份 README.md:三条 curl 完成「发现异常 → 定位组件 → 临时提日志级别」的标准动作
138 / 146

验收清单:① curl :9090/actuator/opsboard 输出的每个字段都能追溯到某个已知指标名;② 制造一次数据库不可用(改错 URL 或用 Testcontainers 断网),面板里对应格子应变红且 /health/readiness DOWN、/health/liveness 仍 UP;③ 压测 60 秒后面板上的 5xx 比例与 http_server_requests_seconds_count{status="500"} 手算结果误差小于 5%;④ 关掉内网 profile 后端点返回 404。

139 / 146
小节
十四、要点自查
140 / 146
自检

Actuator 默认的 Web 暴露面包含哪两个端点?为什么要这么设计?

141 / 146
自检

/actuator/health 的顶层 status 是怎么算出来的?show-details 的三个取值分别挡住/放开了什么?

142 / 146
自检

liveness 与 readiness 各自回答什么问题?把 db 放错分组会引发哪一种具体事故?

143 / 146
自检

Counter、Gauge、Timer 三者语义差别是什么?「支付回调耗时分布」应该用哪一个,为什么?

144 / 146
自检

/actuator/env 的脱敏规则有什么局限?给出两条对应的加固做法。

145 / 146
口诀

白名单开门、独立端口收口、细节按需授权;存活只看自己,就绪才问依赖;指标进门、看板出门,OBD 别扔马路边。

146 / 146
总结

Actuator 把应用从「黑盒」变成「可问答」——health 回答「活着吗」,metrics 回答「多快、多忙、多稳」,info 回答「跑的是哪个版本」,beans/mappings/conditions 回答「为什么这么装配」。生产落地只需三步:白名单暴露 + 管理端口隔离 + 接 Prometheus/Grafana 做可视化与告警。记住底线:可观测性的前提是安全,别让排障入口变成攻击入口。