Actuator 与可观测性:健康检查、指标与监控
Actuator 就是 Spring Boot 给应用装上的仪表盘:程序跑起来以后,你自己看不到「我现在忙不忙、依赖好不好、内存还剩多少、跑的是哪个版本」,Actuator 把这些内部状态打包成一组 HTTP 地址(叫端点 endpoint),你或运维系统用浏览器 / curl 一问,它就答。它本身不画图、不告警,只负责如实回答。
先给五个词一句话解释:
- 端点(endpoint):一个固定的 URL,比如
/actuator/health,访问它就返回一段 JSON - 暴露(expose):决定哪些端点真的能被 HTTP 访问;Boot 默认只开
health和info - 健康指示器(HealthIndicator):一个「我负责的这块好还是坏」的自检小函数,容器把它们的结果汇总成总状态
- Micrometer:指标门面(相当于日志界的 SLF4J),代码只管埋点,后端换成 Prometheus 还是 Datadog 都不用改业务代码
- 探针(probe):Kubernetes 定期来敲的两个门——
liveness问「进程还活着吗」,readiness问「现在能接流量吗」
汽车仪表盘 + OBD 诊断口。转速表和故障灯是 metrics 与 health——司机随时能看;而修车师傅插上 OBD 口读故障码,那是 loggers、threaddump、heapdump 这些诊断端点。仪表盘可以给你看,OBD 口却能让懂的人改写行车电脑——所以修理厂才允许接它(内网 + 鉴权),绝不能把线头扔在马路边。
健康探针 = 值班交接前先自测血压。交班(K8s 放流量)前你要量血压确认自己能上岗,这是 readiness;而「你今天状态不好就送你去医院抢救」是 liveness——判据只能是你自己的身体(进程),不能是「救护车今天晚点了」(数据库抖动)。否则网络一抖,全科室的护士都被拉去抢救,本来还能干活的也被清空了。

这张树状图是全篇地图:左边一支随便公开,中间一支限内网,右边一支默认干脆不开。第三节的表会把它们逐个讲清楚。
学完这一篇,你应该能回答三个问题:
- 为什么
/actuator/health显示 DOWN 却看不出是哪一块坏了?怎么让它自己报出来? include: "*"到底危险在哪?生产上线时那几行配置该怎么写?- 「接口变慢」这件事,能不能提前十分钟从某个指标上看出来?看哪个?
项目上线后,最怕的一句话是「系统好像有点慢」。这句话背后其实是三个具体问题:现在发生了什么、发生了什么变化、为什么发生。业界用「可观测性三支柱」来回答它们:
| 支柱 | 回答什么问题 | 数据形态 | 常用工具 |
|---|---|---|---|
| 日志 Logging | 某个时刻具体发生了什么 | 离散的事件文本 | Logback + ELK / Loki |
| 指标 Metrics | 整体趋势和量级如何 | 可聚合的时序数字 | Micrometer + Prometheus + Grafana |
| 追踪 Tracing | 一次请求经过了哪些环节、慢在哪 | 带因果关系的链路 | OpenTelemetry / SkyWalking |
日志擅长「精确定位一次事故」,但你要统计「过去一小时的错误率」,就得把几百万行日志数一遍——那太慢了。指标把「可聚合的数字」提前算好:QPS、响应时间分位值、错误率、连接池使用率,都是随时间变化的时序数据,天然适合看板与告警。追踪则串起跨服务的调用链。
Spring Boot 的 Actuator 恰好是获取后两类数据(尤其是指标)的入口:它把应用内部的运行状态通过一组 HTTP 端点暴露出来。这一篇就从「打开 Actuator」讲到「接上 Prometheus 和 Grafana」。

Actuator 不是监控系统的全部,而是被监控的一端。它负责「产出」健康状态和指标,真正的存储、可视化与告警由 Prometheus、Grafana 承担。
加一个起步依赖即可,什么都不用改就能用:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency>默认只暴露 health 和 info 两个端点,其余端点在 Web 上不可见——这是 Boot 出于安全的默认选择。要用更多,得显式 expose:
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(可远程关停应用)都交了出去。历史上多次真实事故都源于此。
先跑一遍内核实验,看清楚「暴露面」这道闸门到底怎么开合:
再把闸门全推开,看攻击者三十秒内能拿走什么:
这两种写法的差别不用背,拨一下开关就能看见。下面是本篇的暴露面沙盘——左边是运维在群里喊「快给我看全部」的写法,右边是上线前的正确写法:
GET :9090/actuator -> _links: [self, health, info, metrics, prometheus]GET :9090/actuator/env -> 404 Not FoundPrometheus scrape OK: 15s interval#推荐形态:四个端点 + 独立端口
顺手确认一件事:这些配置写在哪个文件里、谁覆盖谁,直接决定「我在本地改了 yml 为什么线上没生效」。
两种结局对照得最清楚的还是这张图:

Actuator 端点很多,但常用的就十来个。按用途和安全等级整理如下(同一批端点在开篇那张端点家族树里是按「谁能访问」分三支长出来的,这里改按风险等级重排一遍):
| 端点 | 用途 | 安全等级 |
|---|---|---|
health | 健康状态汇总,供探针与负载均衡使用 | 低(可公开,细节要收紧) |
info | 构建信息、Git 提交、自定义属性 | 低 |
metrics | 运行指标(可下钻到具体指标) | 中 |
prometheus | 指标转 Prometheus 文本格式 | 中 |
loggers | 查询/动态修改日志级别 | 中 |
beans | 容器内所有 Bean 及其依赖关系 | 高 |
mappings | 所有 URL 与处理方法的映射 | 高 |
conditions | 自动配置的条件求值报告 | 高 |
threaddump | 线程快照,排查卡死 | 高 |
heapdump | 下载堆内存快照(文件很大) | 极高 |
env / configprops | 环境变量与配置属性(可能含敏感值) | 极高 |
httpexchanges | 最近的 HTTP 请求记录 | 高 |
beans、mappings、conditions 是开发期诊断神器——「这个 Bean 为什么没注册进来」「这个 URL 到底映射到哪个方法」,一眼就能看到。但它们也暴露了内部结构,生产中应当限制访问来源。
端点名记不住没关系,先记住「你要问的问题」。下面六种典型疑问,各配一个第一时间该打开的端点——配错了,它会把选错的后果讲给你听:
/actuator/health 聚合了多个内置指示器(DB、Redis、磁盘空间、ping 等),返回结构化的 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 自动装配,你写了数据源它就自动具备
当依赖了健康检查不该覆盖的第三方服务时,写一个自定义 HealthIndicator:
@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(); } }}健康检查在 K8s 里对应探针(probe),且 liveness 与 readiness 语义完全不同,必须分组,否则会出现「数据库抖动导致实例被整体摘除」的灾难:
management: endpoint: health: group: liveness: include: ping # 只判断"进程是否活着",别放进 db readiness: include: db,redis # 判断"能否接收流量",可包含依赖坑:如果你把 db 放进 liveness,数据库短暂抖动会让 K8s 判定容器「死了」并重启进程——一个本来几十秒能恢复的网络问题,被放大成全体实例滚动重启。liveness 只该判断进程自身,依赖健康交给 readiness。
聚合过程值得亲眼看一次:三个指示器里有一个 DOWN,顶层状态是怎么被「投票」出来的,细节又在什么条件下才肯显示:
act health 实验把结果给你看了,但「整体」这个词背后是一次真正的投票。把投票规则摊成一次单步执行,一行一行走——连点「下一步」,注意严重度排名如何在第 5 步改判全局:
List<HealthIndicator> indicators = List.of(ping, db, redis); // ① 注册顺序Status overall = Status.UNKNOWN; // ② 起点:还没有结论for (HealthIndicator ind : indicators) { // ③ 逐个点名 Status cur = ind.health().getStatus(); // ④ 它这一轮的回答 if (severity(cur) > severity(overall)) overall = cur; // ⑤ 更严重者上位}return overall; // ⑥ 投票结果| 指示器数 | 3 |
| 注册顺序 | ping → db → redis |
HealthEndpoint.readCompositeHealthIndicator.health再把视角拉到上线现场——探针在启动、抖动、恢复、停机这四个时刻各自答什么:

配合优雅停机的完整链路再看一遍,你就知道为什么「摘流量」必须排在「关进程」前面:
Spring Boot 用 Micrometer 作为指标门面(就像 SLF4J 之于日志),业务只依赖 MeterRegistry,底层可以换成 Prometheus、Datadog 等。四种最基本的计量器要分清:
| 计量器 | 语义 | 例子 |
|---|---|---|
| Counter | 只增不减的累计量 | 下单总数、错误次数 |
| Gauge | 瞬时值,可增可减 | 当前在线连接数、队列长度 |
| Timer | 统计耗时与调用次数 | 接口响应时间、方法执行耗时 |
| DistributionSummary | 统计数值的分布 | 订单金额分布、请求体大小 |
内置指标按来源分类,常用的是这些:
| 指标前缀 | 来源 | 为什么关注 |
|---|---|---|
jvm.* | JVM(内存、GC、线程) | 判断是否内存泄漏、GC 是否频繁 |
http.server.requests | Web 请求 | QPS、P99 响应时间、错误率 |
hikaricp.connections.* | 数据库连接池 | 连接是否被占满(慢查询先兆) |
tomcat.threads.* | Tomcat 线程池 | 线程是否耗尽 |
system.cpu.usage | 操作系统 | CPU 是否饱和 |
hikaricp.connections.pending(等待连接的线程数)持续大于 0,往往先于「接口变慢」出现,是最有价值的早期告警指标之一。
这些数字不是看板变出来的,而是 /actuator/metrics 一层层问出来的:
技术指标告诉你「系统健康」,业务指标告诉你「业务健康」。注入 MeterRegistry 即可埋点:
@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.),避免与内置指标混淆
方法级耗时也可以直接用 @Timed 注解(需注册 TimedAspect):
@Timed(value = "beeorder.product.detail", description = "商品详情查询耗时")public ProductVO detail(Long productId) { return productRepository.findById(productId).map(ProductVO::from).orElseThrow(NotFoundException::new);}要点:业务指标是「告警的起点」。把「支付成功率 = 支付成功数 / 支付回调数」暴露成指标,才能配出「成功率跌破 99% 就告警」这样的规则,而不是等用户投诉。
先把指标转成 Prometheus 能读的格式——加一个注册表依赖:
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId></dependency>访问 /actuator/prometheus 会得到如下文本(节选):
# 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.0Prometheus 通过配置定期抓取这个端点,再把数据存成时序:
scrape_configs: - job_name: beeorder metrics_path: /actuator/prometheus scrape_interval: 15s static_configs: - targets: ['beeorder-app:9090']Grafana 负责把 Prometheus 里的时序画成看板,配上告警规则即可自动通知:
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、连接池与线程池水位

线上接口报错,但日志级别是 INFO,看不出细节。重启改配置代价太大——用 loggers 端点在线修改:
# 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 端点可以回看最近若干次请求的路径、状态与耗时,定位「哪个请求出问题」往往比翻全文日志更快。
这个端点改的到底是什么?其实是 Logback 里那个 logger 对象的级别字段——整条链路跑一遍就明白了:
/actuator/info 用于展示构建信息、Git 提交号与自定义属性,方便「线上跑的到底是哪个版本」:先让 Maven 生成构建元数据,再配置暴露:
<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>management: info: git: mode: full # 需要 spring-boot-starter-actuator + git-commit-id 插件 env: enabled: true这样调用方(或值班同学)访问 /actuator/info,就能看到版本号、构建时间、Git 提交 id——排查「线上是新版还是旧版」这类问题不再靠猜。
Git 信息需要额外引入 git-commit-id-maven-plugin 并保证构建时有 .git 目录;CI 环境用浅克隆时 Git 信息可能为空,需配置插件抓取。
Actuator 好用,但默认是一把双刃剑。上线前逐条对照:
| 加固项 | 做法 |
|---|---|
| 端点白名单 | 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 |
清单背不下来没关系——把该收的几行勾出来,让生成器替你拼一份能直接进生产的 yml 骨架:
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 }
生成器给出的是骨架,不是免检通道——show-details 的取值、网关是否屏蔽 /actuator 路径、网络策略是否只放行内网,这三件事生成器管不了,得你自己逐条确认。
heapdump 端点可以下载整个堆内存快照,里面可能包含数据库连接串、密钥、用户数据。除非在受控的内网排障窗口,否则一律禁用它。
把 db 检查放进 liveness 分组,是「数据库抖动导致实例被负载均衡摘除、甚至被 K8s 重启」的经典事故。记住分组原则:liveness 只问「进程活着吗」,readiness 才问「依赖都好吗、能不能接流量」。
management.endpoints.web.exposure.include: "*" 加上 0.0.0.0 暴露,等于把 /actuator/env、/actuator/heapdump、/actuator/shutdown 全部挂在公网。真实世界已有无数应用因此泄露配置或被人一键关停。
工具认全了、坑也点过了,最后把「出事那晚你会按什么顺序伸手」串成一条闭环——每一格都用本篇学过的一个端点,一格都不许跳:

再配一句总结:观测类端点负责「说得清」,诊断类端点负责「查得到」,而安全加固决定「谁能问」——三者缺一,开篇那张四类能力图就只是一张能力表,而不是一套排障流程。
这一节的每一条都可以整段复制去搜索。新手最常卡在四个地方:端点根本 404、健康 DOWN 却不知是谁、/env 泄密、管理端口和业务端口搞混。
| 现象原文(片段) | 真实原因 | 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"},但响应体里没有 components | management.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;生产环境一律关闭该端点 | 第十节加固清单 |
表里的报错都还能靠「现象原文」自救。但有一类事故,报错本身不点名凶手——它把线索藏在括号里的数字上。下面这份堆栈来自一次真实的「Pod 被轮转」事故(探针线程上抛出的异常节选),先别看结论,点出你认为的凶手帧:
凌晨 01:40,值班手机炸响:pay-api 的实例十分钟内被重启了两轮,每次都是探针先超时。重启后立刻恢复,过一阵又犯。这是运维从被杀实例的日志里捞出的堆栈节选——探针线程上抛出的异常。先别看结论,点出你认为的凶手帧。
目标:搭一个「本地就能跑通、配置就是生产骨架」的最小监控工程,亲眼看到白名单带来的 200/404 差异、/health 的 components、以及一行 curl 把日志级别切成 DEBUG。
动手之前,先在这台内核控制台里把这五格实验按顺序跑一遍——每格对应本篇的一个结论,回显全部由内核算出来:
五格跑完,你就把「暴露 → 收紧 → 观测 → 探活」完整走了一遍;接下来三档练习,是把这套流程搬进你自己的工程。
第一步,pom.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>第二步,src/main/resources/application.yml:
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第三步,一个自定义健康指示器和一个下单计数埋点(包名 com.example.monitorlab):
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()); }}第四步,启动并按顺序执行这几条 curl(注意端口分别是 8080 与 9090):
# ① 索引页:确认暴露面只有四个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预期输出(② 在未授权时的默认形态,③ 的两个分组,④⑤ 的状态码):
{ "status": "UP" }{ "status": "UP", "group": "liveness" }{ "status": "UP", "group": "readiness", "components": { "db": { "status": "UP" }, "diskSpace": { "status": "UP" } } }HTTP/1.1 404HTTP/1.1 404验收清单:① 你能解释为什么 liveness 里没有 db;② 把 show-details 改成 always 后再访问 ②,能看到 inventoryCache 这个自定义指示器出现在 components 里;③ 说清 ④ 与 ⑤ 两条 404 的原因分别是什么(前者是端口隔离,后者是白名单)。
只改一处,结论完全不同:
- 把
include换成"*",重启。你会观察到:curl :9090/actuator的_links一下多出十几个键,其中env里spring.datasource.password被打码,但你新加的app.my.access-key-alias=plain-secret会原样输出——这就是脱敏规则的真实边界。改完记得换回去。 - 把
management.server.port那行注释掉。你会观察到:④ 变成 200,管理端点重新挂在业务端口上;这时如果你在网关上按路径转发,/actuator就等于对公网开放。这一条能让你真正理解「端口隔离」挡的是什么。 - 把
db加进liveness分组,然后用错误的 URL 制造启动失败(jdbc:h2:mem:lab;INIT=RUNSCRIPT FROM 'nonexistent.sql')。你会观察到:/actuator/health/liveness直接 DOWN,模拟出「K8s 会把本来只需摘流量的实例反复重启」。 - 把
InventoryCacheHealthIndicator里的阈值从60_000改成-1。你会观察到:顶层status变 DOWN 且 HTTP 状态码变 503,但默认看不到原因——于是你被迫去调show-details,正好复现第十二节第二条报错现场。 - 在第 ⑥ 步之后连续请求接口十几次。你会观察到:DEBUG 日志哗哗地出来;把它调回
null(继承父级)后立刻安静。这就是「不用重启的排障」的价值与风险。
提示:做完第 3 条再回看第四节的探针动图,两边时间点应当完全对得上。
给自己做一个「值班自助排障面板」:不装 Grafana 也能在浏览器一屏看清关键结论。
需求:
- 一个自定义端点
@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 完成「发现异常 → 定位组件 → 临时提日志级别」的标准动作
验收清单:① curl :9090/actuator/opsboard 输出的每个字段都能追溯到某个已知指标名;② 制造一次数据库不可用(改错 URL 或用 Testcontainers 断网),面板里对应格子应变红且 /health/readiness DOWN、/health/liveness 仍 UP;③ 压测 60 秒后面板上的 5xx 比例与 http_server_requests_seconds_count{status="500"} 手算结果误差小于 5%;④ 关掉内网 profile 后端点返回 404。
Actuator 默认的 Web 暴露面包含哪两个端点?为什么要这么设计?
/actuator/health 的顶层 status 是怎么算出来的?show-details 的三个取值分别挡住/放开了什么?
liveness 与 readiness 各自回答什么问题?把 db 放错分组会引发哪一种具体事故?
Counter、Gauge、Timer 三者语义差别是什么?「支付回调耗时分布」应该用哪一个,为什么?
/actuator/env 的脱敏规则有什么局限?给出两条对应的加固做法。
白名单开门、独立端口收口、细节按需授权;存活只看自己,就绪才问依赖;指标进门、看板出门,OBD 别扔马路边。
Actuator 把应用从「黑盒」变成「可问答」——health 回答「活着吗」,metrics 回答「多快、多忙、多稳」,info 回答「跑的是哪个版本」,beans/mappings/conditions 回答「为什么这么装配」。生产落地只需三步:白名单暴露 + 管理端口隔离 + 接 Prometheus/Grafana 做可视化与告警。记住底线:可观测性的前提是安全,别让排障入口变成攻击入口。