做 Java 后端的人迟早都会接到一套“健康检查”需求。springboot健康检查第一眼看起来极其简单——引入 actuator 依赖访问/actuator/health看到{status:UP}就完事了。但一旦把这些端点放到 Kubernetes 探针、注册中心摘流、监控告警的真实环境里需要补的配置远比一个最小 Demo 多得多。这篇文章就从一次线上事故说起把 Spring Boot 接入健康检查时真正需要关心的配置项、自定义扩展点、探针语义、安全隔离方法以及与注册中心和优雅停机的联动细节一次讲清楚。无论你是刚接触 Spring Boot 的新人还是正在把微服务往 Kubernetes 迁移的同学都可以对照这里面的配置模板直接落地。1. 服务“假活”问题健康检查到底要解决什么1.1 一次让我给健康检查“补课”的线上事故之前在某个项目里维护一套订单服务某次压测刚开始监控面板上所有节点都显示存活但业务成功率却断崖式下跌。登录到一台实例上看Java 进程还在端口也通然而接口几乎全部卡在等待数据库连接池超时。这种“假活”现象在微服务里太常见了。数据库连接池被打满、线程池任务队列堆积到极限、磁盘写满导致日志打不出去这些情况下进程本身依然活着对外 TCP 连接依然能建立。如果你的部署平台只检查“进程在不在”“端口通不通”那它给出的结论就是“服务正常”。健康检查要解决的正是这个误判问题。它不断言“服务是否能处理一切业务”而是尝试回答“这个实例现在能不能完成核心职责”。理解了这一点后面所有配置才有方向感。1.2 健康状态的真实消费方编排系统、流量调度、监控告警Spring Boot 暴露出的健康状态最终消费者通常有三类容器编排平台比如 Kubernetes 的探针会用它决定要不要重启实例、要不要把流量放进来。注册中心和负载均衡比如 Consul、Nacos、Eureka、网关会用它从上游实例列表里把有问题的节点临时摘除。监控与告警平台会定时抓取 UP/DOWN 状态把它落成曲线并触发通知。这三类消费方对“健康”的语义要求并不一样。调度平台需要快、稳定、不误报注册中心需要能反映真实可用性否则摘不干净或者误摘监控系统则希望信息足够丰富能自动关联到具体故障组件。很多团队用同一个端点去服务这三种需求结果配置起来互相牵制最后往往只做到了“能通”而不是“好用”。1.3 UP 不等于能处理业务先接受这个前提再动手Spring Boot 自带了很多健康检查器比如数据库连接检查、Redis Ping、RabbitMQ 连接检查。这些检查已经是比“进程活着”强很多的信号了但它依然回答不了“业务链路是否完全可用”。举个例子一个订单服务依赖物流查询接口但这个依赖既不是数据库也不是 Redis而是一个外部 HTTP 服务。默认健康检查里不会包含对应的 Indicator服务依然返回 UP网关继续转发流量于是大量请求在等待上游超时中堆积。这不是 Spring Boot 的缺陷而是健康检查在设计上本来就限定在“应用可控的组件和基础设施范围”。想要让健康检查贴近真实业务就不得不手写定制检查器。这个部分在第 3 节详细展开。动手之前先守住这个认知健康状态是持续维护的系统指标不是一个写完就不动的静态配置。2. Actuator 接入springboot 健康检查的最小配置与版本差异2.1 引入 Actuator 依赖Spring Boot 项目的健康检查标准实现来自 Actuator 模块。最常见的做法是在pom.xml里加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependencyGradle 工程则是在build.gradle里加implementation org.springframework.boot:spring-boot-starter-actuatorSpring Boot 2.x 和 3.x 在这里的差异主要落在基础框架和 Java 版本要求上健康检查相关类的包路径大体保持稳定。如果你是在老项目上升级最需要关注的是默认暴露端点的策略变化而不是类名怎么变。2.2 暴露端点include 与 exclude 的正确姿势依赖引入后Spring Boot 默认只暴露health和info两个 Web 端点。很多人在本地验证时觉得不够用随手就把全部端点打开了management: endpoints: web: exposure: include: *这个写法在开发环境很方便但生产环境会把shutdown、heapdump、env、beans这些端点一并暴露出去风险非常高。我一般建议生产环境把范围收紧成自己真正需要的management: endpoints: web: exposure: include: health,info,metrics,prometheus如果只想留健康检查那就直接include: health。升级过老项目的人都有印象Spring Boot 2.2 之后默认只暴露health和info更早版本可能是另一套默认策略。升级完端点突然“消失”多数不是配置写错而是默认暴露规则变了。2.3 show-details 与健康检查返回结构默认访问/actuator/health你只能看到一个总体的 UP/DOWN 状态。想看组件明细需要配置management: endpoint: health: show-details: alwaysshow-details有三个取值never永远不展示 detail 和 components这是 Spring Boot 的默认值。always无论请求者是谁都返回完整组件状态。when-authorized只有认证用户可见完整信息匿名请求只看到 summary。生产环境里never或when-authorized更合适。直接用always等于把所有内部组件名、数据库状态、Redis 连接情况全部暴露给任何能访问这个端点的调用方。开发调试时用always最方便我通常只在application-dev.yml里开基线配置保持when-authorized。2.4 从 /actuator/health 到 /health 的路径调整Spring Boot 官方习惯用/actuator前缀但很多运维脚本和基础设施探针只认/health。想改成短路径可以配置management: endpoints: web: base-path: /actuator endpoint: health: path: health有些人会直接把base-path改成空字符串让所有端点都挂在根路径下。这个方法不是不行但需要仔细评估一下访问控制规则是否覆盖到位。我实际操作时更常用 Nginx 层做一个转发把公网请求里的/health代理到内网管理端口然后再由 Actuator 返回结果。权限控制放在最外层比在应用层堆配置更容易统一管理。3. 默认检查不够用之后手写健康指标扩展监控范围3.1 默认健康指标都管了哪些组件只要 classpath 里有对应的客户端Spring Boot 的自动配置就会创建相应健康检查器DataSourceHealthIndicator会做一次连接验证判断数据库是否可用。RedisHealthIndicator向 Redis 发送 PING。MongoHealthIndicator、ElasticsearchHealthIndicator、RabbitHealthIndicator按需装配。这些自动装配省了很多事但它们在真实系统里的盲区非常明显。最常见的有三个第一自定义连接池或者加了代理层的数据源默认检查器可能识别不到第二服务依赖了外部 HTTP 接口系统里根本没有现成的 Indicator第三一些非标准协议的自研中间件自动配置完全管不到。所以自己实现健康检查器是接入健康检查的必修课。3.2 手写一个 MQ 连接健康检查器我现在的场景里有个消费依赖消息队列曾出现过连接数涨高但还没完全断开的情况默认 Rabbit 检查器走的是自带连接不一定能暴露问题。后来我们统一走自定义实现定期探测专用通道。一个最小的实现长这样Component public class CustomMqHealthIndicator implements HealthIndicator { private final MqConnectionManager mqConnectionManager; public CustomMqHealthIndicator(MqConnectionManager mqConnectionManager) { this.mqConnectionManager mqConnectionManager; } Override public Health health() { try { boolean reachable mqConnectionManager.checkReachable(2000); if (reachable) { return Health.up() .withDetail(mq, connection ok) .withDetail(latencyMs, 12) .build(); } return Health.down() .withDetail(mq, connection timeout) .build(); } catch (Exception e) { return Health.down(e).build(); } } }这里有两个细节值得注意。一是检查动作必须有超时通常控制在 500ms 到 2s 之间否则健康检查线程会被外部依赖长时间卡住最终拖垮整个管理端点。二是返回Health.down(e)时Spring Boot 会自动把异常类型和 message 写入 detail排障时一眼就能看到原因比单纯返回down()强得多。3.3 不只是 UP 和 DOWNOUT_OF_SERVICE 与 UNKNOWN 的用途健康状态值一共有四类UP、DOWN、OUT_OF_SERVICE、UNKNOWN。大多数文章只讲 UP 和 DOWN但OUT_OF_SERVICE在做灰度发布和主动摘流量时非常有用。当实例被标记为OUT_OF_SERVICE时注册中心会认为它不应该接收流量但调度系统通常不会立刻重启它。之前我做组件升级时就通过一个自定义状态写入入口让运维一键把某个实例置为OUT_OF_SERVICE等流量排空后再整机替换。这个功能如果靠改代码重启才能生效那发布成本会高很多。3.4 用 HealthContributorRegistry 动态注册与注销组件如果只是写死一个 Indicator运行期想调整就很麻烦。Spring Boot 提供了HealthContributorRegistry允许动态注册、注销组件Component public class HealthOps { private final HealthContributorRegistry registry; public HealthOps(HealthContributorRegistry registry) { this.registry registry; } public void shutdownForTrafficDrain() { registry.registerContributor(manualMaintenance, new AbstractHealthIndicator() { Override protected void doHealthCheck(Health.Builder builder) { builder.outOfService().withDetail(reason, manual maintenance); } }); } }有了 registry 之后你可以根据发布流程和开关配置灵活地把某个依赖从健康检查里移除或者注入故障状态。比如数据库需要重启窗口你想让服务先撑住而不是被调度平台反复重启就可以临时摘掉对应的 Indicator。整个过程不用重启应用也不需要改注册中心配置。注册之后/actuator/health返回结构里会多出一个manualMaintenance组件状态为OUT_OF_SERVICE下游的 readiness 探针能立刻感知。4. 为容器化部署准备的探针配置liveness、readiness、startup 的分工4.1 三种探针的语义区别Kubernetes 里要配置三套探针很多人直接复制网上的模板结果出问题时一头雾水。先看下表探针类型判断依据失败后的动作典型用途livenessProbe进程或核心运行状态重启容器检测死锁、内存泄漏、线程池假死readinessProbe是否可以接收外部流量从 Service Endpoints 中摘除依赖未就绪时先不引流startupProbe容器是否完成启动不重启但 readiness 会一直不通过慢启动应用避免 liveness 误杀绝大多数应用的合理组合是用startupProbe保护慢启动应用用readinessProbe检查业务依赖而livenessProbe只检查进程自身和核心运行状态。千万不要把外部组件状态一股脑挂到liveness上否则某个依赖组件短暂抖动K8s 就会把整个实例重启造成更大范围的连环故障。这是我在实际项目里见过最多的问题。4.2 让 Spring Boot 直接暴露 liveness 和 readiness 探针端点Spring Boot 2.3 开始提供了专门的探针端点/actuator/health/liveness和/actuator/health/readiness。开启方法management: endpoint: health: probes: enabled: true启用之后可以定义自己的分组。比如把liveness组配置成只检查进程本身把readiness组配置成包含数据库、Redis、MQ 等全部依赖management: endpoint: health: group: liveness: include: livenessState readiness: include: db,redis,mq,readinessState访问时GET /actuator/health/liveness只要状态不是 DOWN就返回 200。GET /actuator/health/readiness所有依赖全 UP 且 readinessState 就绪才返回 200。对 Kubernetes 探针来说HTTP 状态码 200 和 503 就是一切。所以让这两个端点专门服务探针不要和展示级健康状态混在一起是设计上的关键点。4.3 Kubernetes 探针配置示例一个典型的 Deployment 片段spec: containers: - name: order-service image: registry.example.com/order-service:1.2.3 ports: - containerPort: 8080 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3 startupProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 0 periodSeconds: 2 failureThreshold: 60参数选择有讲究。periodSeconds太长依赖故障被发现得慢流量会继续灌进去太短K8s 会频繁打探针给管理端造成额外压力。我常用的组合是startupProbe每 2 秒探测一次最多试 60 次readiness每 10 秒一次liveness每 10 秒一次。timeoutSeconds给 2 秒就够了应用正常时健康检查本身应该是毫秒级返回。容器里如果配置了独立的management.server.port那探针里的port也要指向管理端口而不是业务端口。这个坑我踩过一次这里特别记一下。4.4 探针实验中的两个真实坑第一个坑liveness 挂了数据库检查。之前某个组件把数据库依赖放进了 liveness 组结果 MySQL 主从切换的几十秒内K8s 把所有实例连续重启了三遍注册中心剧烈漂移。liveness 只判断“这个实例本身还能不能活着”不要掺入外部依赖。数据库切换期间应用进程是健康的完全不需要重启。第二个坑initialDelaySeconds设置太长。如果业务启动只要 20 秒你却把 readiness 的initialDelaySeconds设成 40 秒实例明明已经能接收流量了却还要空等 20 秒。扩容时这部分时间成本被白白浪费。合理的做法是把启动阶段交给startupProbereadiness只负责就绪后的持续检测初始延迟设一个保守下限。另外readiness 失败只是摘流量liveness 失败会杀掉容器。所以 liveness 的failureThreshold要更大一点我一般给 3 到 5防止偶发抖动直接重启。5. 端点保护与最小暴露健康检查不该向全世界敞开5.1 完整 details 被外部看到等于把架构图送人show-details: always配合include: *时/actuator/health会把所有依赖组件名、状态、版本、错误信息全部展开。一次非授权访问就能从输出里得知应用用的是哪类数据库、Redis 集群规模、消息中间件类型甚至能从错误堆栈里拼出内网 IP 段。这些信息对攻击者来说就是地图。所以在设计上要明确一点基础探活可以匿名内部 details 必须有身份限制。实现可以分三层网络访问隔离、Spring Security 授权、show-details使用when-authorized。下面分别说。5.2 独立管理端口与内网访问限制Spring Boot 支持把 Actuator 的 Web 端点绑定到独立端口management: server: port: 8081 address: 127.0.0.1绑到127.0.0.1之后外部机器完全访问不到管理端口需要由本机的监控 Agent 代拉。如果必须对外提供服务比如给 Kubernetes 探针访问就不能只绑回环地址需要在安全组或防火墙层面限制来源 IP。实际操作中常见两种做法。一种是探针直接访问业务端口上的/actuator/health同时在网关层拦截所有/actuator路径只放行监控来源另一种是绑独立管理端口并让探针通过 Service 访问该端口。后者更干净但要注意 Service 的targetPort要指向 8081端口命名也尽量规范避免后面人看不懂。5.3 结合 Spring Security 做细粒度授权如果项目里已经引入 Spring Security健康检查端点可以做保守授权。最简单的写法Configuration public class ActuatorSecurityConfig { Bean public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception { http.securityMatcher(/actuator/**) .authorizeHttpRequests(auth - auth .requestMatchers(/actuator/health, /actuator/health/readiness, /actuator/health/liveness).permitAll() .anyRequest().authenticated() ); return http.build(); } }上面这段代码把基础健康探活端点放行给探针其余 Actuator 端点全部要求认证。逻辑也很直接探针是机器调用没法带登录态而env、beans、heapdump、metrics这些信息只给管理员看。配合show-details: when-authorized匿名请求只能看到{status:UP}认证之后才能看到所有 components。这样既保证了探针能拿到 200 响应又不至于把完整架构信息暴露给公网。我现在把这段配置写进了服务基线运维同学再也不会问“健康检查怎么什么都没有了”。5.4 敏感环境下不要怕下线部分端点有些团队因为历史原因业务端口和管理端口混用暴露的 Actuator 端点太多。如果经过评估确实风险高最简单的手段就是把端点列表收紧到极致management: endpoints: web: exposure: include: health只留health一个端点是最干净的做法。需要监控数据时再用 Micrometer 暴露 Prometheus 格式不要贪多。监控功能按需扩展风险边界也一目了然。6. 微服务体系下健康状态的对外联动注册中心、监控与优雅停机6.1 注册中心如何利用健康状态摘除实例在 Spring Cloud 体系里Consul、Nacos、Eureka 的健康检查机制并不一样。Eureka 默认靠客户端心跳上报实例状态Consul 的 HTTP 健康检查会定期请求你配置的健康端点Nacos 的临时实例主要靠心跳但健康检查端点允许自定义。共同点是实例状态的 UP/DOWN 直接决定流量是否会调度到它。我在项目里的做法是服务注册时将健康检查路径指向/actuator/health。这样数据库连不上、Redis 掉线等状态会通过这个端点反馈给注册中心实例被标记为 DOWN 后上游调用方就会自动把它从候选列表里剔除。相比单纯心跳机制这个方式能更快摘除“进程活着但已经无法服务”的节点。实际经验是不要把全部 Indicator 都作为注册中心的健康依据。因为注册中心的摘除会造成流量转移如果只是消息队列抖动就把所有实例摘掉反而会放大故障。我通常让注册中心对应一组精简依赖只包含“不摘除就会立刻请求失败”的关键组件比如数据库和核心缓存消息队列这类可以容忍短暂延迟的组件不放进摘除依据里。6.2 通过 Prometheus 持续采集健康状态监控是健康检查的另一个重要消费方。Spring Boot 配合micrometer-registry-prometheus会把健康状态指标也暴露出来management: endpoints: web: exposure: include: health,prometheusPrometheus 抓取/actuator/prometheus之后可以用类似表达式监控spring_boot_health_status{statusDOWN} 0还可以用up指标判断服务本身是否存活哪台实例失去响应就触发告警。面板展示上DOWN 出现才告警UP 是常态不必特意为正常状态设置通知否则就是告警疲劳。把每个组件的状态都记录成图表排障时能少切换好几个页面。6.3 优雅停机与 readiness 配合让摘流与关闭不打架健康检查和优雅停机是必须配合的两个功能。我见过不少团队只做了其中一个结果发布时丢请求。标准的流程应该是运维触发优雅停机流程。服务先把 readiness 状态切换为OUT_OF_SERVICE让注册中心或 K8s 从流量列表里摘掉本实例。服务等待一段排空时间让已经进来的请求完成。最后再关闭 Spring 容器。Spring Boot 对此有现成配置server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s这里的timeout-per-shutdown-phase就是等待已接收请求结束的时间上限。如果业务线程池有慢请求超过这个时间也会被强制结束。我的经验是把它设成该服务 P99 响应时间的 3 到 5 倍。比如 P99 是 500ms就设 2 到 3 秒。不要给太长时间否则节点一多发布速度会被严重拖慢。如果没配优雅停机直接用kill -9把进程干掉readiness 还没来得及把实例摘除流量就打到已关闭的端口上连接直接 RST整批请求失败非常疼。后来我们统一要求所有服务都开启 graceful shutdown同时把切换 readiness 状态的操作纳入了发布脚本。最后把我目前仍在用的简化版配置模板贴出来算是一份可直接抄作业的最终形态management: endpoints: web: exposure: include: health,info,prometheus base-path: /actuator endpoint: health: probes: enabled: true show-details: when-authorized group: liveness: include: livenessState readiness: include: db,redis,mq,readinessState server: port: 8081 address: 127.0.0.1这套配置在我这边扛过了好几次第三方依赖抖动。每次依赖出问题时监控图上都能第一时间看到具体组件变 DOWN而不是盯着进程列表干着急。最后再补充一个小建议健康检查配置不是写完就一劳永逸的每新增一个外部依赖、每次调整探针参数都要按上线流程重新验证一遍这个动作比配置本身更重要。