1. 为什么说 Actuator 是 Spring Boot 生产环境的“仪表盘”
先讲一个我自己的真实经历。之前负责一个订单服务,线上跑得好好的,突然用户反馈下单变慢。我第一反应是登录服务器,top看一眼 CPU,df看下磁盘,再翻日志找异常。折腾了十几分钟才定位到是数据库连接池被打满,而这一切如果提前接入了 Spring Boot Actuator,其实在监控面板上一眼就能看到。
Actuator 是 Spring Boot 提供的一个“探针式”监控模块,它把应用内部的运行状态、环境信息、指标数据、日志级别、线程快照等内容,统一通过 HTTP 端点或 JMX 暴露出来。你的应用在你的服务器里扮演什么角色——存活、健康、吞吐、内存压力、配置对不对、能不能远程调整日志级别,这些都通过它对外“开口说话”。
这篇文章我会按“引入配置—端点详解—安全加固—实战监控—踩坑排错”的路径,把 Actuator 完整拆一遍。无论你是刚接触 Spring Boot 的新人,还是在维护老项目的开发者,都能从中拿到可以直接落地的配置和思路。
先说一个很多人刚接触 Actuator 时的困惑:为什么我加了依赖,访问/actuator/health只有{"status":"UP"}这么简单?因为 Spring Boot 默认只暴露了health这一个端点,而且默认只展示状态摘要。这种“安全优先”的设计理念贯穿整个 Actuator,后面我会详细展开。
2. 引入 Actuator:最小化配置与暴露策略
2.1 加依赖,就这么简单
在pom.xml中加入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>如果你是 Gradle 项目:
implementation 'org.springframework.boot:spring-boot-starter-actuator'这个 starter 会把你所需要的端点基础设施全部拉进来。注意,它本身不包含第三方监控系统(比如 Prometheus)的适配器,需要额外引入对应依赖。
2.2 默认暴露策略:安全优先
Spring Boot 2.x 之后,端点的逻辑被重新梳理了。所有端点默认是“已启用但未暴露”的状态,你需要在配置中指定通过 HTTP 还是 JMX 暴露哪些端点。
默认情况下,只有health一个端点通过 HTTP 暴露。这是因为生产环境的安全红线——你不希望任何人一访问你的应用根路径,就能看到你的环境变量、配置信息、线程状态这些东西。
我的建议是:先用最小暴露跑通,再按需开放。比如只做存活检查,什么都不配,直接访问/actuator/health就够了。
2.3 常用配置项详解
management: endpoints: web: exposure: include: health,info,metrics,loggers,env exclude: shutdown base-path: /actuator endpoint: health: show-details: always shutdown: enabled: true逐行解释一下:
management.endpoints.web.exposure.include:通过 HTTP 暴露哪些端点,多个用逗号分隔,也可以写*暴露全部(不推荐,除非你做好了安全控制)。management.endpoints.web.exposure.exclude:强制排除哪些端点。exclude的优先级高于include,也就是说即使你写了include: *,被 exclude 的端点也不会暴露。management.endpoints.web.base-path:所有端点 URL 的前缀,默认是/actuator。你可以改成/monitor之类的,在一定程度上避免被扫描工具直接命中默认路径。management.endpoint.health.show-details:控制健康检查端点是否展示详细信息。always在什么情况下都展示,后面会专门说这个配置在生产环境有多重要。
这里有一个容易踩坑的点:include: health,info,metrics中间不要加空格,如果你用 YAML 的列表写法,也要保证格式正确:
management: endpoints: web: exposure: include: - health - info - metrics两种写法等价,但混用容易出问题——我见过有人一行写多个还带了空格,结果启动时端点一个都没暴露,排查了半天。
2.4 JMX 与 HTTP 双通道
Actuator 的端点可以通过 HTTP 和 JMX 两种方式访问。默认情况下,除了health和info,其他端点都暴露在 JMX 中。
生产环境中 JMX 用得相对少了,因为需要通过jconsole或jvisualvm连接,而且很多部署环境根本没开 JMX 端口。但如果你维护的是老项目,又需要通过 JMX 获取运行状态,可以显式配置:
management: endpoints: jmx: exposure: include: health,metrics这里我想强调一个观点:Actuator 暴露什么,取决于你对“监控面”的诉求。如果你只是要一个外部探活 URL,health就够。如果要做容量规划和性能调优,metrics和threaddump就是核心。后面逐个拆解。
3. 核心端点逐个拆解:有的只是数据,有的是“救命稻草”
3.1 health:你的应用是“活着”还是“健康”
这是最常用、也是唯一推荐无条件暴露的端点。它不只是简单返回一个 UP 或 DOWN,而是会对各种健康指示器做聚合判断。
默认情况下,Spring Boot 会注册很多自动的健康指示器,比如:
DiskSpaceHealthIndicator:检查磁盘空间是否充足DataSourceHealthIndicator:检查数据源能否获取连接RedisHealthIndicator:检查 Redis 能否 ping 通MongoHealthIndicator/ElasticsearchHealthIndicator等:对应中间件
你可以在配置里看当前有多少健康指示器生效:
management: endpoint: health: show-details: always show-components: alwaysshow-details: always之后,访问/actuator/health就能看到类似这样的返回:
{ "status": "UP", "components": { "db": { "status": "UP", "details": { "database": "H2", "validationQuery": "isValid()" } }, "diskSpace": { "status": "UP", "details": { "total": 499963170816, "free": 201011122176, "threshold": 10485760, "exists": true } }, "redis": { "status": "UP", "details": { "version": "6.2.6" } } } }从上面返回能看出:健康检查不是简单的“进程在不在”,而是把应用依赖的关键资源全部检查了一遍。
生产环境的建议:show-details: always只在内网监控系统或安全可控环境下使用。如果应用暴露在公网,建议设置为when-authorized,并通过 Spring Security 控制谁能看到详细信息。
再补充一个实用场景:做 K8s 存活探针与就绪探针。
livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080Spring Boot 2.3+ 自动注册了liveness和readiness两组探针端点,如果你只是用/actuator/health做两种探针,很可能会遇到“服务还没就绪就被打流量”或者“已处于不健康状态却一直不被重启”的尴尬。
/actuator/health/readiness反映的是应用是否准备好接收流量,比如 Spring 容器是否初始化完成、消息监听器是否启动;/actuator/health/liveness反映的是应用进程是否活着,如果挂了 K8s 会帮你重启。分开用,别混用。
3.2 info:应用“名片”与构建信息
info端点是一个自定义信息的聚合出口。默认情况下它只返回空的 JSON,因为 Spring Boot 不知道你想展示什么。
你可以通过application.yml或application.properties配置:
info: app: name: @project.name@ version: @project.version@ description: @project.description@这里的@project.name@是 Maven 资源过滤的占位符,构建时会替换成pom.xml中对应的值。如果你用 Gradle,需要手动配置processResources的展开。
还可以通过实现InfoContributor来动态写入内容,比如把 Git 提交号写进去:
@Component public class GitInfoContributor implements InfoContributor { @Override public void contribute(Info.Builder builder) { builder.withDetail("git", Map.of( "commitId", "abc123456", "branch", "main" )); } }实际排查线上问题时,info端点很有用:你可以快速确认当前这个节点跑的是哪个版本、哪个分支的代码,尤其是多环境部署时,能省去很多“以为升级了其实没升”的尴尬。
3.3 metrics:性能数据的“矿产区”
metrics端点是一个两级索引结构。访问/actuator/metrics,你会看到一份指标名称列表,类似:
{ "names": [ "jvm.memory.used", "jvm.memory.max", "jvm.threads.live", "http.server.requests", "process.cpu.usage", "system.cpu.usage", "hikaricp.connections.active", "hikaricp.connections.pending", ... ] }想看具体某个指标的值,要访问完整路径,比如:
/actuator/metrics/jvm.memory.used /actuator/metrics/http.server.requests /actuator/metrics/hikaricp.connections.active每个指标还可以通过tag参数按维度过滤:
/actuator/metrics/http.server.requests?tag=uri:/orders /actuator/metrics/http.server.requests?tag=status:500现场排查时最有用的几个指标:
jvm.memory.used和jvm.memory.max:判断堆内存是否逼近上限jvm.threads.live:看线程数是否异常飙升hikaricp.connections.active和hikaricp.connections.pending:数据库连接池是否耗尽process.cpu.usage:进程 CPU 占用http.server.requests:接口请求量和耗时分布
我记得有一次排查接口性能问题,就是通过http.server.requests?tag=uri:/order/list发现某个接口的P99从 200ms 飙到 5s,顺藤摸瓜找到了一个 N+1 查询问题。
3.4 loggers:动态调整日志级别,不用重启
这个端点我在生产环境用过很多次,它是 Actuator 里“性价比”极高的一个功能。
平时定位问题,最怕的是线上日志级别是 INFO,而关键排查信息打在 DEBUG,你只能加日志重新发版。有了loggers端点,直接现场调。
查看某个包的日志级别:
GET /actuator/loggers/com.example.order返回类似:
{ "configuredLevel": null, "effectiveLevel": "INFO" }动态修改为 DEBUG:
curl -X POST -H "Content-Type: application/json" \ -d '{"configuredLevel":"DEBUG"}' \ http://localhost:8080/actuator/loggers/com.example.order改完立即生效,不需要重启。定位完问题,再改回 INFO 就行。
这里有一个小教训:调 DEBUG 级别前要确认日志量不会把磁盘打爆。我之前在一个高并发服务上对全局 root logger 开了 DEBUG,五分钟内日志文件涨了几个 GB,差点把磁盘写满。正确做法是精确到出问题的那个类或包,而不是一刀切。
3.5 env 和 configprops:配置问题排查的“照妖镜”
线上最恶心的一个问题:本地是好的,测试环境也是好的,一到生产就报错,最后发现是某个配置项在服务器上没生效。env端点就是用来查这类问题的。
GET /actuator/env返回所有Environment中的属性,包括系统环境变量、application.yml、启动参数等。你可以按单个属性名查询:
GET /actuator/env/server.port也能看到这个属性来自哪里、覆盖关系是什么。搭配configprops端点,可以查看@ConfigurationProperties绑定后的真实值:
GET /actuator/configprops比如你配置了spring.datasource.hikari.maximum-pool-size,从env里看到的是原始字符串,从configprops里看到的是绑定到HikariDataSource配置类后的具体值。两者配合,几乎能把“配置没生效”这个疑难杂症一锤定音。
3.6 heapdump 和 threaddump:故障现场的“尸检报告”
heapdump端点用来下载 JVM 堆内存快照:
curl -o heap.hprof http://localhost:8080/actuator/heapdump这个文件可以用 MAT 或 JProfiler 分析,看内存对象引用链、找出谁占着内存不释放。注意,这个操作会触发 Full GC,生产环境高负载时慎用。我一般在服务已经“病入膏肓”且准备重启时才抓 heapdump,否则可能因为一次 Full GC 把服务卡死。
threaddump则是给你一份当前所有线程的快照:
GET /actuator/threaddump包含每个线程的栈信息、锁状态、线程状态。在排查线程死锁、线程池阻塞、接口 hang 住等问题时,这个端点比jstack命令来得方便,因为它不要求你登录服务器,而且能看到 Java 层的完整调用栈。
3.7 shutdown:优雅停机到底要不要开
shutdown端点可以触发应用的优雅关闭:
management: endpoint: shutdown: enabled: true然后:
curl -X POST http://localhost:8080/actuator/shutdown它能调用 Spring 容器关闭流程,执行@PreDestroy回调,释放连接池,但是我强烈不建议在生产环境暴露它。原因很简单:
第一,如果你用POST /actuator/shutdown关停应用,K8s 或容器的优雅停机信号SIGTERM就无法传递到 JVM 进程内部,可能导致 Spring 容器没来得及执行清理逻辑,进程就被强制杀死。
第二,如果你需要远程关停服务,用运维平台的发布系统发 SIGTERM 信号更规范和可控,而不是通过一个 HTTP 请求直接干掉服务。
4. 端点安全:Actuator 暴露了,你的内网“裸奔”了吗
4.1 公网裸奔的风险
很多人觉得“我的服务在内网,没事”。但内网不等于安全,横向移动攻击在真实攻防演练中非常常见。一个暴露了env、heapdump、configprops的 Actuator 端点,等于把你的数据库密码、Redis 密码、中间件地址全递到攻击者手里。
你在/actuator/env里能看到spring.datasource.password,在/actuator/configprops里能看到那些带@ConfigurationProperties的类绑定的完整配置。这些信息在乙方安全团队做渗透测试时,是最容易得手的突破口。
4.2 三种主流防护方式
我把生产中常用的防护手段按推荐程度排一下:
方案一:独立管理端口 + 内网白名单
management: server: port: 9090 address: 127.0.0.1 endpoints: web: base-path: /actuatormanagement.server.port把 Actuator 从业务端口剥离出去,address: 127.0.0.1则只允许本机访问。需要对接监控系统时,通过 Nacos、Consul 等服务发现机制让监控系统访问本机地址,或者配合云服务商的安全组只对监控服务器开放这个端口。
方案二:Spring Security 鉴权
@Configuration public class ActuatorSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/actuator/**") .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health").permitAll() .anyRequest().authenticated() ) .httpBasic(); return http.build(); } }只放行health,其他端点都要认证。这种方案的缺点是引入了 Spring Security 依赖,而且如果你的业务已经有一套安全配置,需要小心过滤器链的顺序冲突。
方案三:网关层拦截
如果服务在多级网关后面,可以在网关层直接过滤/actuator/**路径,只允许来自内网监控系统的 IP 访问。
我的经验是:方案一 + 方案三结合是成本最低、效果最好的组合。在服务上绑定本机回环地址,从根源上断绝远程访问的可能;监控系统通过机房侧的 Agent 采集本机数据,网关层再做一层过滤。
4.3 敏感信息脱敏:那些你迟早要处理的密码
就算做了认证,env端点仍然会返回属性值。Spring Boot 中有SanitizingFunction机制可以自定义脱敏策略,比如内置的password、secret、token等关键词会自动替换为******。
GET /actuator/env/spring.datasource.password返回时密码字段会被打码。但你自定义的配置项如果不是用这些关键词命名,比如:
custom: db: pwd: 123456那env端点会把这个值原样返回。解决办法是使用@Value或@ConfigurationProperties之前,先确认属性名的关键词能被内置脱敏规则覆盖;覆盖不了的,实现一个自定义SanitizingFunction:
@Component public class CustomSanitizer implements SanitizingFunction { @Override public SanitizableData apply(SanitizableData data) { if (data.getKey().contains("pwd") || data.getKey().contains("password")) { return data.withSanitizedValue("******"); } return data; } }这个细节很容易被忽略,等到安全扫描报告出来,第一个被点名的往往就是/actuator/env泄露敏感配置。
5. 实战:把 Actuator 接入 Prometheus + Grafana 监控体系
5.1 让 Actuator 输出 Prometheus 格式指标
自定义指标和 JVM 指标只有转成 Prometheus 格式,才能被 Prometheus 抓取。做法是引入 Micrometer 的 Prometheus 注册表:
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>然后在application.yml中暴露prometheus端点:
management: endpoints: web: exposure: include: health,prometheus,metrics访问/actuator/prometheus,你会看到类似这样的文本输出:
# HELP jvm_memory_used_bytes The amount of used memory # TYPE jvm_memory_used_bytes gauge jvm_memory_used_bytes{area="heap",id="PS Old Gen",} 1.23456789E8这就是 Prometheus 的标准文本协议格式。Prometheus 可以每隔 15 秒抓一次这个端点,完成指标采集。
5.2 自定义业务指标:MeterRegistry 的正确用法
metrics端点默认收集的都是 JVM、Tomcat、HikariCP 这一类“基础设施指标”。真正对业务有价值的,是你自己埋点的指标。
Micrometer 提供了MeterRegistry抽象,在 Spring Boot 中直接注入即可:
@Service public class OrderService { private final Counter orderCreatedCounter; private final Timer orderCreateTimer; public OrderService(MeterRegistry registry) { this.orderCreatedCounter = Counter.builder("order.created.total") .description("Total number of orders created") .register(registry); this.orderCreateTimer = Timer.builder("order.create.duration") .description("Time taken to create an order") .register(registry); } public void createOrder(Order order) { orderCreateTimer.record(() -> { // 实际业务逻辑 orderCreatedCounter.increment(); }); } }这类业务指标接入prometheus端点后被采集,才能在 Grafana 上画出“下单量趋势图”“接口耗时热力图”。只监控 CPU 和内存,不监控业务指标,在系统出问题的时候你很难定位到具体是哪个业务链路在恶化。
5.3 Prometheus 配置与告警规则
Prometheus 抓取配置:
scrape_configs: - job_name: 'order-service' metrics_path: '/actuator/prometheus' static_configs: - targets: ['192.168.1.10:8080']告警规则示例:
groups: - name: order-service-alerts rules: - alert: OrderServiceHighErrorRate expr: sum(rate(http_server_requests_seconds_count{status="500"}[5m])) / sum(rate(http_server_requests_seconds_count[5m])) > 0.05 for: 5m labels: severity: critical annotations: summary: "Order service 5xx error rate above 5%"配合 Alertmanager 做企业微信、钉钉或邮件通知。这套东西搭起来之后,你的服务就不再依赖“用户跑过来说用不了”才被发现问题了。
5.4 Grafana 面板的关键图表
我不建议把自己逼成全职监控开发,但下面这几张基础图值得优先配置好:
- JVM 堆内存使用率(
jvm_memory_used_bytes / jvm_memory_max_bytes) - 活跃线程数与线程池队列长度
- HTTP 请求量、P99 耗时、5xx/4xx 比例
- 数据库连接池活跃连接数与等待线程数
- GC 次数与 GC 耗时
在晚上收到报警之后,我会先打开 Grafana,按照“先看基础设施(CPU/内存)→ 再看中间件(连接池/GC)→ 最后看应用指标”的顺序快速定位问题,而不是一上来就翻日志。
6. 生产环境里那些让你栽跟头的 Actuator 坑
6.1 端口配置不当导致监控全部失效
有些人想当然地设置了:
management: server: port: 8080表面上看没毛病,实际上这个配置表示管理端点和业务端点共用 8080 端口,但暴露路径仍然是/actuator/*。如果你后面又改成 9090 端口,那么访问/actuator/health就会变成访问/9090/actuator/health,很多老监控系统还是按原端口配的,改完就抓不到数据。
我见过一个生产事故:运维把management.server.port改到 9090,结果 Prometheus 的targets还指向 8080,整整一天没有指标数据,直到某个依赖的磁盘告警才发现监控断了。所以要么别单独设端口,要么设了之后把所有监控采集端同步更新。
6.2 show-details: always 的“好心办坏事”
health端点把show-details设为always后,每次被外部探活,都会对所有健康指示器做一次完整检查。如果下游某个组件响应慢了,比如 Redis 连接超时配置了 5 秒,那么健康检查本身也会变慢。
更麻烦的是,健康检查 URL 如果被负载均衡器配置了较短的超时时间(比如 3 秒),一旦某个健康指示器卡住,负载均衡器会认为你的服务不健康,把它摘掉。健康检查链路影响调度决策,所以生产环境我一般这样建议:
- 对外部负载均衡用
show-details: never(只返回 UP/DOWN,响应最快) - 对内部监控系统用另一个带鉴权的端口,开
show-details: when-authorized
6.3 自定义 HealthIndicator 的“阴间操作”
很多团队会自定义健康检查逻辑,比如“检查某个第三方 API 是否能通”。我见过一个反面案例:
@Component public class WechatApiHealthIndicator implements HealthIndicator { @Override public Health health() { String result = restTemplate.getForObject("https://api.weixin.qq.com/cgi-bin/token", String.class); return Health.up().build(); } }这个health()每次被调用都会发起真实的 HTTP 请求,如果第三方接口慢或网络抖动,你的健康检查就会被拖到超时。健康检查是用来反映“本应用能不能正常工作”的,不是用来探活第三方服务的。正确做法是加缓存,每隔 30 秒检测一次,把结果缓存起来,健康检查直接读缓存状态:
@Component public class WechatApiHealthIndicator implements HealthIndicator { private volatile Health cachedHealth = Health.unknown().build(); @PostConstruct public void init() { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(this::check, 0, 30, TimeUnit.SECONDS); } private void check() { try { // 第三方健康探测 cachedHealth = Health.up().build(); } catch (Exception e) { cachedHealth = Health.down(e).build(); } } @Override public Health health() { return cachedHealth; } }这样既不影响健康检查的响应速度,又能反映真实状况。
6.4 Spring Boot 2.x 与 3.x 的端点迁移差异
Spring Boot 3.x 里,Actuator 有几个明显变化:
spring-boot-starter-actuator的坐标没变,但底层基于 Jakarta EE 9+- 一些端点路径微调:比如
/actuator/httptrace改成了/actuator/httpexchanges /actuator/conditions更名为/actuator/beans相关的条件报告路径有调整
如果你从 2.x 直接升级到 3.x,且监控脚本里硬编码了/actuator/httptrace,升级后会遇到 404。建议升级前先扫一遍脚本里所有 Actuator 路径,统一更新。
6.5 依赖冲突:Micrometer 版本不一致
一个隐蔽的坑:如果你的项目里既有micrometer-core又有micrometer-registry-prometheus,但 Spring Boot 版本是 2.4,Micrometer 会自动升级到 1.6+,而某些自研监控组件是基于 1.5 编译的,就会出现NoSuchMethodError。踩过一次之后,我现在都会在pom.xml里显式锁定 Micrometer 版本,避免 Spring 依赖管理帮你“自动升级”。
7. 我的个人实践体会
Actuator 用到现在,我最深刻的体会是:它不是一个可以“加了依赖就完事”的功能,而是一整套可观测性思维的入口。
刚开始我也是一个端点都不看,只知道/actuator/health返回 UP 就觉得服务正常。后来出了几次线上事故,才慢慢理解:健康检查、指标端点、日志动态调整、线程快照不是孤立的功能,它们是“应用运行态的四个切面”。健康检查告诉你服务能不能接流量,指标告诉你性能和容量到没到极限,日志动态调整帮你现场排查问题,线程快照帮你还原故障现场的调用关系。
我建议你按照这个顺序逐步落地:
- 第一步:先接入
health和info,配合 K8s 探针和发布流程,解决“服务挂没挂”的问题 - 第二步:暴露
metrics,接入 Prometheus + Grafana,把基础设施指标可视化,解决“容量够不够”的问题 - 第三步:暴露
loggers和threaddump,配合线上排障手册,解决“出了故障怎么定位”的问题 - 第四步:自定义业务指标和 HealthIndicator,解决“业务健康度怎么看”的问题
最后再分享一个小技巧:每次发版前,把/actuator/health、/actuator/metrics、/actuator/info三个 URL 的返回结果截图或存一份,万一发版后有问题,可以对比前后差异,快速定位是环境变了还是代码变了。这个习惯帮我省了无数次“我本地明明是好的”的争论。