做后端时间长了,你会发现一个很朴素的事实:一个服务能不能长期稳定跑下去,很多时候不是看代码写得多漂亮,而是看线上出问题时你手里有没有足够的线索。Spring Boot Actuator 就是专门干这件事的——它是 Spring Boot 官方提供的生产级指标监控组件,能让应用把健康状态、运行指标、线程、堆内存、请求分布这些内部信息通过标准端点暴露出来。无论你是刚把第一个微服务部署上线的同学,还是已经在运维十几个实例的老手,这套指标监控体系都会是你排查线上问题时的第一手资料。这篇文章我会从端点体系、健康检查、Micrometer 指标整合、安全策略、配置实战、告警接入到踩坑实录,完整梳理一遍,每一段都是可以直接照着落地的内容。
1. Actuator 指标监控的核心价值:从日志追查到实时仪表盘
1.1 为什么最终选了 Actuator:对比裸日志排查的真实体验
其实很多人最初对监控的态度是“出问题我再慢慢查”。我之前参与过的一个模拟项目,负责人最初就是这种想法,结果某天夜里服务响应时间从 50 毫秒飙升到 2 秒,大家的第一反应是上服务器翻日志。日志确实有,问题在于日志量太大了,几十个实例同时在打,光定位到具体异常日志就花了一个多小时;更麻烦的是,线程池被占满之后,新请求根本进不来,应用日志本身都打不出来了——服务已经“饿死”了,可日志系统还在老老实实地把旧记录往外吐。
Actuator 的指标是完全不同的玩法。它直接告诉你线程池有多少活跃线程、连接池使用率是多少、JVM 堆当前占用多少、GC 暂停多久。同样是那次故障,如果当时有/actuator/metrics和/actuator/health,切到相关指标页面的那一瞬间就能看到 Tomcat 线程数已经顶到 200 上限,数据源连接数也耗尽,后面的排查完全不用靠猜。一个是案发后翻日志重构现场,一个是实时仪表盘,两种体验的差距比想象中大得多。
我总结下来,它解决的核心问题有四类:
- 健康检查:服务还活着吗?依赖的数据库、缓存、消息队列还通不通?
- 运行状态观察:内存、线程、GC、HTTP 请求量、堆栈、环境配置。
- 现场问题定位:堆转储、线程转储、运行时动态改日志级别。
- 外部监控对接:把指标暴露给 Prometheus 这类时序系统,做趋势告警。
这套能力不需要改业务代码,加依赖、做配置、选端点就能用,所以它才会成为 Spring Boot 应用接入可观测性的默认起点。
1.2 端点与指标的两层能力:一次把概念理清楚
先记住一个关键点:Actuator 有两层信息模型,一层是端点(Endpoints),一层是指标(Metrics)。很多人混在一起看,配置的时候特别容易乱。
端点可以理解成“即时回答问题的接口”。比如你问“服务现在健康吗”,health端点给你一个状态快照;你问“当前注册了哪些 Bean”,beans端点把清单列出来;你再问“数据库连接串密码是什么”,env端点甚至能把配置项翻出来。端点是离散的、当下的、静态或半静态的信息。
指标走的是另一条路。它是一个持续产生的时间序列数值,比如“过去 5 分钟 HTTP 请求成功率”“当前 JVM 堆已用内存”“订单接口的平均耗时 P95”。指标回答的问题是“这段时间发生了什么趋势”,它天然适合告警和聚合分析。Actuator 在指标这一层把 Micrometer 作为门面,默认向内存里登记一套 JVM、系统、HTTP、数据源等自动指标,同时可以推到不同的时序数据库去。
这两层能力的关系可以这样理解:端点负责“现场勘察”,指标负责“长期监测”。线上出了问题,先看指标趋势判断从什么时候开始恶化,再用端点去看现场细节。只有端点没有指标,你只能采样到当下状态;只有指标没有端点,趋势异常时你却少了顺藤摸瓜的抓手。我在实际工作中就是把两层同时开起来,比如health、prometheus常开,heapdump、threaddump按需手动开。
2. 关键端点逐一拆解:生产环境该看哪些,该挡哪些
2.1 端点全景:一张表搞清默认启用与暴露开关
Actuator 内置了一批端点,每个端点是独立的 ID,HTTP 访问路径默认是/actuator/{id}。注意一个容易懵的点:“启用”和“暴露”是两回事。端点先要启用(默认大多数启用),之后还要决定是否通过 Web 暴露出来。哪怕某个端点是启用状态,没在exposure.include里放行,HTTP 也访问不到。
我把核心端点整理成了一张速查表,实际配置时可以直接对着它做取舍。
| 端点 | 作用 | 默认启用 | 默认 Web 暴露 | 私有部署建议 |
|---|---|---|---|---|
| health | 聚合健康状态 | 是 | 是 | 开放 |
| info | 应用自定义信息 | 是 | 是 | 可开放 |
| metrics | 查看可用指标名称 | 是 | 否 | 按需开放 |
| prometheus | 输出 Prometheus 格式指标 | 是 | 否 | 内网开放 |
| loggers | 查看并动态调整日志级别 | 是 | 否 | 建议关闭或内网 |
| threaddump | 输出线程快照 | 是 | 否 | 按需开放 |
| heapdump | 下载堆转储文件 | 是 | 否 | 强烈建议关闭 |
| env | 查看环境属性 | 是 | 否 | 建议关闭 |
| beans | 查看 Spring Bean 清单 | 是 | 否 | 建议关闭 |
| configprops | 查看配置属性 | 是 | 否 | 建议关闭 |
| mappings | 查看 URL 路由映射 | 是 | 否 | 按需开放 |
| scheduledtasks | 查看定时任务 | 是 | 否 | 按需开放 |
| shutdown | 优雅关闭应用 | 否 | 否 | 保持关闭 |
| httptrace | 查看最近 HTTP 请求追踪 | 是 | 否 | 按需开放 |
这里面最值得重点关注的是health、prometheus、loggers和threaddump。health决定了探针和运维判断能不能依赖它;prometheus是外部监控系统的数据出口;loggers是动态排障神器但也容易被滥用;threaddump在面对“线程卡死在哪”这种问题时几乎是唯一快速手段。
2.2 健康检查与探针:让服务“自我体检”
health端点默认只返回一个聚合状态,它是把应用内部所有子健康检查打包汇总的结果。数据源连接、Redis、消息队列这些组件在自动装配时都会提供自己的HealthIndicator。比如DataSourceHealthIndicator会执行一个校验查询来确认数据库连接可用,RedisHealthIndicator会发一个 ping。聚合逻辑是:只要有一个关键项 DOWN,整体状态就是 DOWN;全部 UP 才返回 UP。
生产环境有个细节很关键:show-details默认是never,外部探针只能看到 UP/DOWN,看不到具体是哪个依赖挂了。这对 Kubernetes 探针其实够了,但你在本地调试想看到细节,可以加management.endpoint.health.show-details=always。线上更推荐when_authorized,配合安全认证让细节只对管理员可见。我的习惯是本地看细节用always,线上坚持never,避免给攻击者信息投喂。
自定义健康检查也很简单,实现一个HealthIndicator接口注册成 Bean 即可:
@Component public class OrderServiceHealthIndicator implements HealthIndicator { @Override public Health health() { // 模拟检查下游服务连通性 boolean reachable = checkOrderService(); if (reachable) { return Health.up() .withDetail("orderService", "available") .build(); } return Health.down() .withDetail("orderService", "unreachable") .build(); } }这个接口适合放到依赖了外部非标准组件的场景,比如快速验证某个私有协议的服务是否可达,如果系统里只有标准数据源和中间件,默认的健康检查已经够用。
从 Spring Boot 2.3 开始还多了一类探针端点。开启management.endpoint.health.probes.enabled=true之后,会额外暴露/actuator/health/liveness和/actuator/health/readiness。这两个探针对应容器编排系统的两种生命周期判断:存活探针决定要不要重启实例,就绪探针决定流量能不能打进来。这里有一个必须记住的原则:不要把就绪失败和存活失败混为一谈。数据库连接池暂时耗尽的时候,就绪应该返回失败,让上游把流量摘走,而不是让编排系统立刻重启容器;否则短时间内大规模重启,雪上加霜。
2.3 指标度量体系:Micrometer 是真正的功臣
很多人以为 Actuator 自带完整指标,其实指标底层是 Micrometer 在干活。Actuator 负责暴露端点,Micrometer 负责收集和注册指标,两者都引入之后才能把/actuator/metrics、/actuator/prometheus用起来。
Micrometer 里的指标类型我简单归纳成四类:
| 指标类型 | 数据特点 | 典型场景 |
|---|---|---|
| Counter | 只增不减 | 累计请求数、累计订单数 |
| Gauge | 可升可降 | 当前线程数、当前堆内存使用量 |
| Timer | 记录耗时分布 | 接口耗时、数据库查询耗时 |
| DistributionSummary | 记录分布且可自定义 | 订单金额分布、消息体积分布 |
选型上有两个常见误区。一是把 Counter 当成 Gauge 用,比如想记录“当前在线用户数”,你如果用一个只增不减的 Counter,它根本不会掉下来;正确做法是用 Gauge。二是只记录平均耗时,忽略了 P99。平均值容易掩盖极端情况,200 个请求里 199 个 10 毫秒完成,一个 3 秒超时,平均值看起来还行,但真实用户已经有 0.5% 的请求不可用了。所以 Timer 我一般会配publishPercentiles(0.95, 0.99),让系统直接暴露分位耗时。
开箱即用的指标其实已经很全了。启动 JVM 应用之后,/actuator/metrics/能看到jvm.memory.used、jvm.gc.pause、system.cpu.usage、http.server.requests、tomcat.threads.busy这些。也就是说你什么都不用写,只要把端点暴露出来,基础可观测性就有了。真正的难点在于业务指标埋点,因为框架不可能知道你们“下单成功数”是什么含义,这部分要靠自己定义,后面会专门讲。
3. 从零到一的接入实战:依赖、安全、埋点、告警都配齐
3.1 引入依赖和暴露配置:第一天就能看到指标
先加依赖。如果用的是 Maven,在pom.xml里加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>第一个依赖是 Actuator 本体,第二个依赖是为了输出 Prometheus 格式的指标。如果你当前不需要 Prometheus,只加第一个也能看/actuator/metrics;但我的建议是一步到位加上,因为后面接入告警和趋势图几乎必然要用。注意如果项目是 Spring Boot 1.x,配置方式和端点路径差异很大,现在 1.x 已经非常老了,能升级尽量升级;下面所有配置都以 2.x/3.x 为准。
然后改application.yml:
management: endpoints: web: exposure: include: health,info,prometheus endpoint: health: show-details: when_authorized probes: enabled: true不加任何配置时,默认只暴露health。上面这段把health、info、prometheus放行出去了。启动项目后直接访问:
curl http://localhost:8080/actuator/health curl http://localhost:8080/actuator/info curl http://localhost:8080/actuator/prometheus看到 JSON 和文本格式的指标就说明第一步通了。info端点默认是空对象,需要你手动补充信息,比如:
info: app: name: order-service version: 1.0.0这样访问/actuator/info就能看到应用标识了。还有一个常见需求是把监控服务独立端口,避免和业务接口混在一起,后面安全配置里再展开。
3.2 安全边界:线上暴露出问题之前先堵住
Actuator 把信息暴露得太方便了,所以安全问题必须前置。先说两个容易被忽略的坑:env端点可以把你数据库密码、Redis 连接串、各种密钥全部带出来;heapdump端点下载的堆转储文件在 JVM 内存充足时能到几个 GB,里面可能包含用户身份证、手机号等敏感数据。所以这两个端点在生产环境宁可不用,也不要打开公网访问。
我常用的生产方案是两条路并行。第一,把监控端口和业务端口分开:
management: server: port: 9091 address: 127.0.0.1这样业务请求走 8080,监控信息走 9091。然后运维层面不让外部访问 9091,只有内网监控系统或授权跳板机可以连。第二,如果必须走同一个端口,就接 Spring Security 做认证,给监控端点和白名单路由写明确放行规则:
@Configuration public class ActuatorSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health", "/actuator/prometheus").permitAll() .anyRequest().authenticated()); return http.build(); } }这样健康检查和指标采集放行给探针和监控系统,其余 Actuator 端点全部要求登录。实际项目里我会更激进一点:把不需要对外暴露的端点全部关掉,包括env、beans、configprops、heapdump、threaddump。这些端点确实好用,但它们不是常态监控必需品,真到排查现场时临时开启再关闭成本也不高。
3.3 业务埋点与自定义端点:让你的业务可观测
框架自带的指标是基础设施,业务指标得自己埋。最常见的是下单场景:我想知道“累计创建了多少订单”“下单接口的 P95 耗时是多少”。方法是在服务里注入MeterRegistry,注册一个Counter和一个Timer:
@Service public class OrderService { private final Counter orderCreateCounter; private final Timer orderCreateTimer; public OrderService(MeterRegistry registry) { this.orderCreateCounter = Counter.builder("order.create.total") .description("累计创建订单数") .tag("channel", "app") .register(registry); this.orderCreateTimer = Timer.builder("order.create.duration") .description("创建订单耗时") .publishPercentiles(0.5, 0.95, 0.99) .register(registry); } public Order createOrder(OrderRequest request) { long start = System.nanoTime(); try { // 实际业务逻辑 return doCreate(request); } finally { orderCreateTimer.record(() -> System.nanoTime() - start, TimeUnit.NANOSECONDS); orderCreateCounter.increment(); } } }埋点之后,/actuator/prometheus里就会出现order_create_total和order_create_duration_seconds这样的指标。计数器名称我用小写下划线风格,这是 Micrometer 和 Prometheus 都推荐的标准命名。
如果还有比较特殊的查询需求,可以自定义端点。例如想做一个“订单统计”的专用接口:
@Endpoint(id = "orderStats") @Component public class OrderStatsEndpoint { @ReadOperation public Map<String, Object> stats() { return Map.of( "totalOrders", 128000L, "todayOrders", 360L ); } }然后在exposure.include里加上orderStats,访问/actuator/orderStats就能拿到 JSON 数据。这个机制适合那些“不想为一次内部查询专门写一个 Controller”的场景,但要注意自定义端点同样要遵守安全暴露规范。
3.4 对接 Prometheus 与 Grafana:趋势、面板、告警一起办
本地看端点只是一部分,监控闭环还得靠外部系统。最常用的组合是 Prometheus 采集时序指标,Grafana 画面板和告警。Prometheus 配置一个抓取任务:
scrape_configs: - job_name: 'order-service' metrics_path: '/actuator/prometheus' static_configs: - targets: ['127.0.0.1:9091']抓取路径就是要对应刚才的暴露端点和端口。如果你用了management.server.port=9091,这里的 targets 就写 9091,否则默认是业务端口。抓取间隔我习惯保持默认 15 秒,对 Actuator 开销可以忽略;不要为了“实时”调到 1 秒,有些场景下指标暴露本身的开销反而会超过业务代码。
Grafana 里导入一个 Spring Boot 相关面板模板,马上能看到 JVM 堆曲线、GC 次数、HTTP 流量、线程池状态这些核心图。我的建议是面板不需要太花哨,核心看四块:JVM 堆内存趋势、GC 暂停次数、HTTP 请求 P95/P99、数据库连接池活跃连接数。这四块能覆盖绝大多数线上故障场景。
告警规则用 Prometheus 原生写法即可。比如“HTTP 5xx 比例超过 5% 并持续 5 分钟”:
groups: - name: order-alerts rules: - alert: HighErrorRate expr: | sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) / sum(rate(http_server_requests_seconds_count[5m])) > 0.05 for: 5m labels: severity: warning这套规则延后的是“基于指标趋势告警”,而不是单条日志报错就轰炸你。告警阈值不要照抄网上的通用值,不同业务的正常错误率相差很大,我会在接入后观察两周基线再定阈值。
4. 常见问题排查与线上经验速查
4.1 端点一直 404?按这四个顺序查
遇到过很多新手给我发“/actuator/metrics访问不了”的消息,截图里清一色 404。这类问题其实有固定的排查顺序:
第一,确认监控端点是否暴露。exposure.include没写就是默认只暴露health。写多个端点时用逗号分隔,例如include: health,metrics,prometheus。如果图省事全部放行,写include: "*",注意 YAML 里星号要加引号,否则会被解析成布尔或语法错误。
第二,路径前缀对不对。默认前缀是/actuator,但如果你改了management.endpoints.web.base-path,路径就变了。我见过有人把base-path改成/monitor,却还在访问/actuator/health,自然是 404。
第三,端点是不是没有启用。大多数端点默认启用,但shutdown之类默认关闭;部分版本和扩展端点也可能默认禁用。需要显式启用时加management.endpoint.xxx.enabled=true。
第四,HTTP 端点和 JMX 端点是独立开关。JMX 方式能看到某个端点,不代表 Web 方式能访问;反之亦然。如果你在用可视化工具或测试 JMX,要看对入口。
4.2 Security 加入后健康检查崩了:认证放行与细节隐藏
一个很常见的坑:给项目引入 Spring Security 之后,Kubernetes 就绪探针访问/actuator/health开始返回 401,容器被无限重启。原因很简单——默认的SecurityFilterChain把所有请求都拦住了,包括健康检查端点。
解决方式就是在安全过滤链里把健康检查端点放行,前面代码片段已经给了方案。不过还有一个隐含坑:show-details: when_authorized需要当前用户通过认证且有一定权限,细节信息才会返回。如果探针只是看 UP/DOWN,不受影响;但如果你在浏览器里登录后想看到具体是哪个依赖 DOWN 了,却只有 UP/DOWN,大概率就是权限配置还没到位。
另外要注意,在 Spring Security 配置里,2.6 之前和之后的路由匹配语法不太一样。如果你使用的版本比较老,requestMatchers可能不适用,要换回antMatchers。我遇到过配置复制过去却报错的情况,基本都是版本差异问题。
4.3 生产环境别乱动的端点:heapdump、threaddump、loggers 的边界
这三个端点虽然好用,但生产环境要谨慎。heapdump会触发一次 Full GC 之后再导出堆文件,JVM 堆有好几个 GB 时,导出过程中业务会影响很严重,我在一个模拟压力测试中实际看到过接口耗时翻倍。线上排查堆泄漏是必要的,但选时段比操作本身更重要,凌晨低峰期操作,别在流量高峰做。
threaddump影响相对小,但线程数多的情况下导出的文件也有几 MB,而且诊断线程阻塞时,抓取那一刻的 dump 才有意义。我的建议是:连续抓 3 次,间隔 5 秒,对比线程堆栈变化,比抓一次更能定位死锁或卡住的位置。
loggers端点的风险在于容易被滥用。它能动态把任意类的日志级别调到 DEBUG,这在排障时确实救命;但如果暴露到公网,等于把日志开关交给所有人。某次安全扫描就扫描出了线上环境的loggers端点对外开放,这个教训让我把所有动态控制类端点全部收进内网。
4.4 关于阈值、抓取频率和度量基线的心得
这个章节放在最后,其实是我最想强调的东西。指标监控不是配好了就一劳永逸,阈值必须跟着业务演进走。我见过一个团队把“请求数突增”告警阈值定得很低,结果每次促销活动都疯狂报警,到最后大家直接屏蔽告警,真正的故障发生在被屏蔽的噪声里。阈值方案要定期复查,尤其是大版本发布后。
还有抓取频率的问题,Prometheus 默认 15 秒抓一次,对于绝大多数 Spring Boot 服务是合适的。不要为了让面板看起来“实时”就调到 3 秒、1 秒,指标不是性能监控里的 CPU 采样,它的目标是一段时间内的趋势,不是毫秒级现场。抓取过密在实例上会产生额外的线程和内存消耗,指标一多,反而干扰判断。
最后留一个建议:每次发版后,拿/actuator/prometheus里的指标和上一版本做一次对比,重点看 JVM 内存曲线和请求耗时分布。很多性能回退在灰度期就会露出端倪,等用户开始反馈再去查已经晚了。这个习惯救过我太多次了。
还有一个小技巧:如果团队还没有统一的监控平台,可以先用 Spring Boot Admin 快速搭一个可视化界面,它能直接读取 Actuator 的health、metrics、loggers、threaddump,不用自己写页面。等接入 Prometheus 和 Grafana 之后,再用 Admin 做实例管理和日志级别动态调整。两条线不冲突,反而是互补的——这个问题我在多个项目里实践过,效果都不错。