1. 为什么服务端性能监控绕不开 Spring Boot Actuator
做服务端开发的朋友大概率都经历过这样的场景:线上接口突然变慢,用户投诉电话打过来,你登录服务器想看看到底是哪里出了问题,结果发现除了日志文件,手头没有任何实时数据可以参考。CPU 多少、内存用了多少、数据库连接池是不是满了、有多少线程在阻塞——全靠猜。这种“盲人摸象”式的排查体验,相信每个后端人都深有体会。
我最早做监控的时候,走的是“自己动手”的路线:写个定时任务,定时采集系统指标,再写个接口暴露出去,最后对接监控平台。这套方案能用,但维护成本极高,每个项目都要复制一遍,改一处就得全量更新。后来接触到 Spring Boot Actuator,才发现原来官方早就把这件事想明白了——它提供了一套开箱即用的生产级监控能力,你只需要加一个依赖、改几行配置,就能拿到应用内部几乎所有的运行时状态。
这篇文章要聊的,就是 Spring Boot Actuator 在服务端性能监控场景下的最佳实践。我会从它的设计思路讲起,拆解核心端点的用途和配置方式,再结合我实际项目中的落地经验,把那些文档里不会写的坑和技巧一并分享出来。无论你是刚接触服务端监控的新手,还是已经在用 Actuator 但只停留在/health层面的老手,相信都能从中找到一些之前忽略的细节。整篇内容会围绕“怎么用、为什么这么用、用了之后怎么排查问题”这条主线展开,力求让你看完就能在自己的项目里复现。
2. Actuator 的整体设计与核心思路拆解
2.1 它到底解决了什么问题
在没有 Actuator 之前,一个 Spring Boot 应用对外基本是个黑盒。你知道它在跑,但不知道它跑得好不好。想了解内部状态,要么自己写端点,要么依赖外部工具去“猜”。Actuator 的核心价值,就是把应用内部的运行时信息通过 HTTP 或 JMX 暴露出来,让运维和开发人员能够实时观测。
它的设计哲学可以用四个字概括:约定优于配置。你不需要定义任何接口,只要引入spring-boot-starter-actuator依赖,它就会自动注册一系列端点(Endpoint),每个端点负责一类信息的采集和暴露。比如/health看健康状态,/metrics看指标数据,/env看环境变量,/threaddump看线程快照。这种“端点即功能”的设计,让监控能力的接入成本降到了最低。
从架构上看,Actuator 分为两层:端点层和暴露层。端点层负责采集数据,暴露层负责决定通过什么协议、以什么方式对外提供。默认情况下,只有/health和/info是通过 HTTP 暴露的,其余端点需要通过配置手动开启。这个设计很关键——它避免了敏感信息被无意中暴露出去,是一种安全优先的默认策略。
2.2 端点分类与选型逻辑
Actuator 提供的端点有几十个,但常用的其实就那么十来个。我习惯把它们分成三类:
第一类是健康检查类,核心是/health。它不仅能告诉你应用是 UP 还是 DOWN,还能聚合数据库、Redis、磁盘空间、消息队列等组件的健康状态。这个端点通常对接负载均衡或容器编排平台,用来决定是否把流量转发到该实例。
第二类是指标采集类,核心是/metrics。它暴露的是数值型数据,比如 JVM 内存使用量、GC 次数、HTTP 请求耗时分布、数据库连接池活跃数等。这些数据是性能监控的核心原料,通常会被 Prometheus 之类的系统定期抓取。
第三类是诊断排查类,包括/threaddump、/heapdump、/env、/configprops、/loggers等。这类端点在出问题时特别有用,比如线程池满了可以看线程快照,配置不对可以看环境变量,日志级别需要临时调整可以直接通过/loggers动态修改。
选型的时候有个原则:按需暴露,最小权限。生产环境不要图省事把management.endpoints.web.exposure.include设成*,而是明确列出需要的端点。我见过太多项目因为暴露了/env或/heapdump导致敏感信息泄露的案例,这个后面会详细说。
2.3 与主流监控体系的衔接方式
Actuator 本身不是一套完整的监控系统,它更像是一个“数据源”。它负责把应用内部的状态暴露出来,至于怎么存储、怎么展示、怎么告警,那是 Prometheus、Grafana、ELK 这些系统的事。
目前最主流的搭配是Actuator + Micrometer + Prometheus + Grafana。Micrometer 是 Spring Boot 2.x 之后引入的指标门面,Actuator 的/metrics端点底层就是通过 Micrometer 采集的。Micrometer 支持多种监控系统作为后端,你只需要引入对应的 registry 依赖,指标就会自动以该系统的格式暴露。比如引入micrometer-registry-prometheus后,/actuator/prometheus端点就会输出 Prometheus 格式的指标文本,Prometheus 直接抓取即可。
这种分层设计的好处是解耦。应用代码只依赖 Micrometer 的 API,不关心后端是 Prometheus 还是别的什么。将来要换监控系统,只需要换一个依赖和几行配置,业务代码完全不用动。这也是我在技术选型时特别看重的一点——基础设施的替换成本要低。
3. 核心端点详解与配置实操
3.1 健康检查端点:不只是 UP 和 DOWN
/health是 Actuator 最常用的端点,但很多人只把它当成一个“存活探针”,这就浪费了它一半的价值。默认情况下,它会聚合一系列HealthIndicator的结果,包括磁盘空间、数据库、Redis、Elasticsearch 等(取决于你引入了哪些依赖)。
访问/actuator/health默认只返回{"status":"UP"},不展示细节。要看每个组件的具体状态,需要配置:
management: endpoint: health: show-details: always配置后返回的内容会变成这样:
{ "status": "UP", "components": { "db": { "status": "UP", "details": { "database": "MySQL", "validationQuery": "isValid()" } }, "diskSpace": { "status": "UP", "details": { "total": 499963174912, "free": 367656960000, "threshold": 10485760 } }, "redis": { "status": "UP", "details": { "version": "6.2.6" } } } }这里有个细节值得注意:show-details有三个可选值——never、when-authorized、always。生产环境我建议用when-authorized,配合 Spring Security 做权限控制,只有特定角色的用户才能看到详细信息。直接设成always虽然方便,但会把数据库类型、磁盘容量这些信息暴露给所有能访问该端点的人。
另外,健康检查的聚合策略也值得关注。默认情况下,只要有一个组件 DOWN,整体状态就是 DOWN。但有些场景下我们希望某个非核心组件的故障不影响整体健康判断,这时可以通过management.health.status.order调整状态优先级,或者自定义HealthAggregator。我在一个项目里就遇到过 Redis 偶尔抖动导致健康检查频繁失败、容器被反复重启的问题,后来把 Redis 的健康检查改成非致命级别才解决。
3.2 指标端点:性能数据的源头
/metrics端点是性能监控的数据核心。它暴露的指标分为几大类:
- JVM 指标:内存各区域使用量、GC 次数和耗时、线程数、类加载数
- 系统指标:CPU 使用率、文件描述符数、系统负载
- HTTP 指标:各接口的请求次数、响应时间分布、错误率
- 连接池指标:数据库连接池的活跃连接数、空闲连接数、等待次数
- 缓存指标:缓存命中率、加载耗时
访问/actuator/metrics会返回所有可用指标的名称列表。要看具体某个指标的数值,需要带上指标名,比如/actuator/metrics/jvm.memory.used。返回格式如下:
{ "name": "jvm.memory.used", "description": "The amount of used memory", "baseUnit": "bytes", "measurements": [ { "statistic": "VALUE", "value": 1.23456789E8 } ], "availableTags": [ { "tag": "area", "values": ["heap", "nonheap"] }, { "tag": "id", "values": ["G1 Eden Space", "G1 Old Gen", "Metaspace"] } ] }这里的关键是availableTags。它告诉你这个指标可以按哪些维度细分。比如jvm.memory.used可以按area(堆/非堆)和id(具体内存区域)过滤。要查堆内存的使用量,就访问/actuator/metrics/jvm.memory.used?tag=area:heap。
不过说实话,通过 HTTP 手动查指标只适合调试。真正的生产监控,一定是让 Prometheus 定期抓取/actuator/prometheus端点。这个端点会把所有指标以 Prometheus 的文本格式一次性输出,抓取效率高得多。配置方式:
management: endpoints: web: exposure: include: health,info,prometheus,metrics metrics: export: prometheus: enabled: true引入micrometer-registry-prometheus依赖后,/actuator/prometheus会自动可用。Prometheus 的抓取配置大概长这样:
scrape_configs: - job_name: 'spring-boot-app' metrics_path: '/actuator/prometheus' scrape_interval: 15s static_configs: - targets: ['localhost:8080']15 秒的抓取间隔是个比较平衡的选择。太短会增加应用和 Prometheus 的负担,太长则可能漏掉一些瞬时峰值。对于核心服务,我一般设 10 秒;非核心服务 30 秒也够用。
3.3 诊断端点:出问题时的救命稻草
当线上出现性能问题时,/threaddump、/heapdump、/loggers这几个端点往往能救命。
/threaddump返回当前 JVM 所有线程的快照,包括线程名、状态、调用栈。当发现接口响应慢时,连续抓几次线程快照,对比一下就能定位到阻塞点。比如你发现大量线程卡在WAITING状态,调用栈指向某个数据库连接获取操作,那基本可以确定是连接池不够用了。
/heapdump会生成堆转储文件,用于分析内存泄漏。但这个端点要慎用——生成 heapdump 会触发 Full GC,期间应用会暂停,大堆内存下可能暂停好几秒。生产环境一定要加权限控制,而且不要在高峰期手动触发。
/loggers端点支持动态修改日志级别,这个功能在排查问题时特别实用。比如某个接口偶发异常,但日志级别是 INFO 看不到细节,你可以直接发个 POST 请求把对应包的日志级别调到 DEBUG:
curl -X POST http://localhost:8080/actuator/loggers/com.example.service \ -H "Content-Type: application/json" \ -d '{"configuredLevel":"DEBUG"}'排查完再调回去,全程不用重启应用。这个端点我强烈建议在生产环境开启,但同样要加权限控制。
3.4 端点暴露的安全配置
前面反复提到权限控制,这里具体说说怎么做。最直接的方式是引入 Spring Security,然后对/actuator/**路径做拦截:
@Configuration public class ActuatorSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/health").permitAll() .requestMatchers("/actuator/**").hasRole("ADMIN") .anyRequest().authenticated() ); return http.build(); } }这样/health对外公开(供负载均衡探活),其余端点需要 ADMIN 角色才能访问。如果不想引入完整的 Spring Security,也可以自定义一个Filter做简单的 Token 校验,但安全性不如前者。
还有一个容易被忽略的点:管理端口的分离。Actuator 端点默认和应用业务端口共用同一个 HTTP 端口。更好的做法是让管理端点走独立端口:
management: server: port: 8081这样你可以只在管理端口上做访问控制,业务端口完全不受影响。在容器化环境中,管理端口通常不对外暴露,只允许监控系统在内网访问,安全性更高。
4. 完整实操流程与关键环节实现
4.1 依赖引入与基础配置
先从一个空白的 Spring Boot 项目开始。假设你用的是 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 的指标注册器。如果你用的是 Gradle,对应写法是:
implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus'接下来是application.yml的配置。我把它分成三块:端点暴露、指标配置、健康检查。
management: server: port: 8081 endpoints: web: exposure: include: health,info,metrics,prometheus,loggers,threaddump base-path: /manage endpoint: health: show-details: when-authorized probes: enabled: true metrics: enabled: true metrics: tags: application: ${spring.application.name} distribution: percentiles-histogram: http.server.requests: true slo: http.server.requests: 50ms,100ms,200ms,500ms,1s逐项解释一下。management.server.port: 8081让管理端点走独立端口。base-path: /manage把默认的/actuator前缀改成/manage,这算是一个轻量的安全措施——攻击者不知道路径就无从下手。show-details: when-authorized配合权限控制展示健康详情。probes.enabled: true开启 Kubernetes 的存活和就绪探针支持,会额外暴露/manage/health/liveness和/manage/health/readiness两个端点。
metrics.tags.application给所有指标打上应用名标签,这在多应用共用一套 Prometheus 时非常关键,否则你根本分不清指标来自哪个服务。percentiles-histogram和slo是 HTTP 请求指标的进阶配置,前者生成百分位直方图,后者定义 SLO 分桶,两者结合可以精确计算 P95、P99 响应时间。
4.2 自定义业务指标埋点
Actuator 自带的指标覆盖了 JVM 和 HTTP 层面,但业务层面的指标需要自己埋。Micrometer 提供了MeterRegistry,注入后就能创建各种类型的指标。
最常用的是Counter(计数器)和Timer(计时器)。比如统计订单创建次数和耗时:
@Service public class OrderService { private final Counter orderCreatedCounter; private final Timer orderCreateTimer; public OrderService(MeterRegistry registry) { this.orderCreatedCounter = Counter.builder("business.order.created") .description("Number of orders created") .tag("type", "normal") .register(registry); this.orderCreateTimer = Timer.builder("business.order.create.duration") .description("Order creation duration") .publishPercentiles(0.5, 0.95, 0.99) .register(registry); } public Order createOrder(OrderRequest request) { return orderCreateTimer.record(() -> { Order order = doCreateOrder(request); orderCreatedCounter.increment(); return order; }); } }这里有几个经验点。第一,指标命名用点分隔的层级结构,比如business.order.created,这样在 Prometheus 里查询时可以用前缀匹配。第二,Timer.record()接受一个Supplier,自动计算耗时,比手动记开始结束时间更简洁也不容易出错。第三,publishPercentiles会生成 P50、P95、P99 指标,这些比平均值更能反映真实用户体验——平均值会被大量快请求拉低,掩盖长尾问题。
还有一个容易踩的坑:标签的基数问题。给指标打标签时,标签值的取值范围必须有限。比如用用户 ID 做标签,如果有十万用户,就会产生十万条时间序列,直接把 Prometheus 打爆。正确的做法是用用户等级、地区这类有限枚举值做标签。我见过一个项目用请求 URL 做标签,结果因为 URL 里带了参数导致指标爆炸,这个教训值得记住。
4.3 Prometheus 抓取与 Grafana 展示
应用侧配置好后,接下来是监控侧的搭建。Prometheus 的配置文件prometheus.yml核心就是抓取任务:
global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'spring-boot' metrics_path: '/manage/prometheus' static_configs: - targets: - 'app-server-1:8081' - 'app-server-2:8081' labels: env: 'production'注意metrics_path要和我们前面配置的base-path对应上。labels里可以加环境标签,方便区分生产和测试。
Grafana 这边,我一般会导入现成的 Spring Boot 仪表盘模板,然后根据业务需求做定制。核心面板包括:
| 面板名称 | 数据来源 | 关注点 |
|---|---|---|
| JVM 内存使用 | jvm.memory.used | 堆内存增长趋势,是否有泄漏 |
| GC 暂停时间 | jvm.gc.pause | 每次 GC 耗时,频率是否异常 |
| HTTP 请求 QPS | http.server.requests | 流量变化,突增突降 |
| 接口响应时间 P99 | http.server.requests | 长尾延迟,用户体验 |
| 数据库连接池 | hikaricp.connections.active | 连接是否耗尽 |
| 线程数 | jvm.threads.live | 线程是否持续增长 |
这些面板组合起来,基本能覆盖 80% 的性能问题排查场景。我习惯把 JVM 内存和 GC 面板放在最上面,因为内存问题是最常见的性能杀手。然后是 HTTP 指标,最后是连接池和线程。每个面板都设置告警阈值,比如 P99 超过 1 秒、堆内存使用率超过 85% 就触发告警。
4.4 告警规则配置
光有展示不够,还得有告警。Prometheus 的告警规则写在单独的文件里:
groups: - name: spring-boot-alerts rules: - alert: HighHttpLatency expr: histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m])) > 1 for: 5m labels: severity: warning annotations: summary: "接口 P99 延迟超过 1 秒" description: "{{ $labels.uri }} 的 P99 延迟为 {{ $value }}s" - alert: HighHeapUsage expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85 for: 10m labels: severity: critical annotations: summary: "堆内存使用率超过 85%" - alert: DbConnectionPoolExhausted expr: hikaricp_connections_active / hikaricp_connections_max > 0.9 for: 2m labels: severity: critical annotations: summary: "数据库连接池接近耗尽"告警规则的设计有两个要点。一是for字段,表示条件持续多久才触发告警。设得太短会被瞬时抖动误报,设得太长又会延迟发现真问题。我一般对延迟类告警设 5 分钟,对资源耗尽类设 2 分钟。二是告警分级,warning 级别发到群里,critical 级别直接打电话。分级能避免告警疲劳——如果所有告警都一个级别,大家很快就会麻木。
5. 常见问题与排查技巧实录
5.1 端点访问返回 404
这是新手最常遇到的问题。引入依赖后访问/actuator/health却返回 404,原因通常有三个。
第一,端点没暴露。Spring Boot 2.x 默认只暴露/health和/info,其他端点需要在management.endpoints.web.exposure.include里显式列出。注意这个配置是覆盖式的,一旦你写了include: health,那/info也会被关掉。
第二,路径不对。如果你配置了base-path或management.server.port,访问路径要相应调整。比如配了独立端口 8081 和 base-path/manage,正确路径是http://localhost:8081/manage/health。
第三,Spring Security 拦截。如果引入了 Spring Security 但没放行 Actuator 路径,请求会被重定向到登录页或返回 401。检查一下安全配置里有没有对/actuator/**或你自定义的 base-path 做放行。
排查的时候有个小技巧:启动应用时打开 DEBUG 日志,搜索Exposing关键字,会看到类似这样的输出:
Exposing 5 endpoint(s) beneath base path '/actuator'这能直接告诉你实际暴露了哪些端点、base path 是什么。
5.2 指标数据缺失或不更新
有时候/actuator/prometheus能访问,但某些指标就是没有。常见原因有这几个。
依赖缺失。比如想看数据库连接池指标,但用的是 Druid 而不是 HikariCP,那hikaricp.*指标自然不存在。Druid 需要额外引入druid-spring-boot-starter并配置spring.datasource.druid.stat-view-servlet相关参数。
指标未注册。Micrometer 的很多指标是懒加载的,只有第一次被访问时才会注册。比如 HTTP 请求指标,在应用刚启动、还没有任何请求时是查不到的。发几个请求再查就出现了。
标签过滤错误。查指标时带了不存在的标签值,会返回空结果。比如jvm.memory.used?tag=area:heap是对的,但写成tag=area:Heap(大小写错误)就查不到。建议先不带标签查一次,从availableTags里确认正确的标签值。
5.3 健康检查导致容器频繁重启
这个问题在 Kubernetes 环境特别常见。健康检查端点响应慢或者偶尔失败,K8s 就认为 Pod 不健康,触发重启,重启后问题依旧,形成恶性循环。
根因通常是健康检查里包含了不稳定的外部依赖。比如数据库偶尔网络抖动,/health就返回 DOWN,K8s 就把 Pod 杀了。但数据库抖动是暂时的,杀 Pod 根本解决不了问题,反而让服务更不稳定。
解决办法是区分存活探针和就绪探针。存活探针只检查应用进程本身是否正常,不检查外部依赖;就绪探针才检查依赖是否可用,不可用时只是把 Pod 从负载均衡里摘掉,不重启。Actuator 的probes.enabled: true就是干这个的:
management: endpoint: health: probes: enabled: true group: liveness: include: livenessState readiness: include: readinessState,db,redis这样/health/liveness只反映应用自身状态,/health/readiness才包含数据库和 Redis。K8s 的livenessProbe指向前者,readinessProbe指向后者,问题就解决了。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 端点 404 | 未暴露/路径错/被拦截 | 看启动日志 Exposing 行 | 检查 exposure 配置和 base-path |
| 指标缺失 | 依赖缺失/懒加载/标签错 | 查/metrics列表 | 补依赖、发请求触发、核对标签 |
| 健康检查误报 | 外部依赖抖动 | 看/health详情 | 拆分存活/就绪探针 |
| 指标基数爆炸 | 标签值过多 | 看 Prometheus 序列数 | 改用有限枚举值做标签 |
| heapdump 卡顿 | Full GC 暂停 | 看 GC 日志 | 低峰期操作,加权限控制 |
| 管理端口冲突 | 端口被占用 | 看启动报错 | 换端口或复用主端口 |
5.5 几个踩坑心得
关于show-details:生产环境千万别图省事设成always。我见过一个项目把健康详情完全暴露,结果外部扫描器直接拿到了数据库地址和版本号。用when-authorized配合 Spring Security,多写几行配置,安全系数高很多。
关于指标命名:自定义指标一定要加业务前缀,比如business.、order.,避免和框架自带指标冲突。同时命名要稳定,一旦上线就不要改,否则历史数据就断了。我一般会在代码里用常量定义指标名,而不是散落在各处写字符串。
关于告警阈值:不要照搬网上的默认值。每个应用的正常水位不一样,有的应用堆内存常年 70%,有的 40% 就算高。上线后先观察一周,根据实际基线来设阈值。告警宁少勿滥,每条告警都应该是可行动的,否则就是噪音。
关于管理端口:容器化部署时,管理端口不要映射到宿主机,只在容器网络内可达即可。Prometheus 通过服务发现直接抓取 Pod IP 的管理端口,既安全又方便。
关于版本兼容:Spring Boot 2.x 和 3.x 在 Actuator 配置上有一些差异,比如 3.x 对management.endpoints.web.exposure.include的处理更严格。升级版本时一定要回归测试监控功能,别等出问题了才发现端点访问不了。
6. 从单机监控到体系化观测的延伸
Actuator 解决的是单应用内部状态的暴露问题,但真实的生产环境往往是几十上百个服务实例。这时候光有 Actuator 不够,还需要考虑几个延伸方向。
链路追踪。Actuator 的指标是聚合数据,看不到单个请求的完整路径。要定位跨服务的性能瓶颈,需要引入链路追踪,把一次请求经过的所有服务串联起来。Micrometer Tracing 可以和 Actuator 共用同一套标签体系,实现指标和链路的关联。
日志聚合。Actuator 的/loggers能动态调级别,但日志本身还是散落在各台机器上。配合 ELK 或 Loki 做集中式日志收集,才能在海量日志中快速检索。
容量规划。Actuator 采集的历史指标是容量规划的重要依据。通过分析内存增长趋势、QPS 变化曲线,可以预测什么时候需要扩容。我习惯每月做一次指标回顾,看看哪些资源的使用率在持续上升,提前做扩容准备。
混沌工程。有了完善的监控,才能安全地做混沌实验。比如主动杀掉一个实例,观察健康检查是否及时摘除、流量是否自动转移、告警是否触发。这些验证能让你对系统的容错能力有真实的信心,而不是停留在理论层面。
这套体系搭起来之后,服务端性能监控才算真正落地。Actuator 是其中的基石,它把应用内部的状态变得可观测,后续的所有分析和决策都建立在这些数据之上。我在实际项目中的体会是,监控这件事投入越早收益越大,等到出问题才想起来补,往往要付出更大的代价。