HoRain云上SpringBoot健康检查终极指南我最早被健康检查坑得很惨是在一次不算大的上线里。当时服务在HoRain云上跑得好好的但前端同事反馈接口时不时502。我第一反应是看CPU、看内存全都没问题最后在负载均衡配置里翻了半天才发现健康检查用的是默认的固定路径而我们的服务是带context-path的结果负载均衡永远在探测一个不存在的地址后端其实完全是好的——坏的只是检查健康的探针。所以后来再做任何SpringBoot服务我都会先把健康检查这件事从头到尾理清楚今天这篇文章就算是我自己在云上折腾了无数环境之后沉淀下来的一份偏实战的SpringBoot健康检查操作手册。不管你是刚接触SpringBoot还是已经习惯用Actuator应该都能在这里找到一些平时容易忽略但关键时刻特别有用的细节。1. SpringBoot健康检查在云环境里到底解决了什么问题先说清楚一个容易被忽视的事实在传统单机部署时代服务挂了就是挂了端口连不上运维看得到但在HoRain云这类容器化、多实例、自动扩缩容的环境里机器不再有固定身份流量怎么走、实例什么时候下线全部要看探针的脸色。1.1 为什么固定路径探测在云上不够用很多人第一次接入云负载均衡时习惯性配一个TCP端口探测觉得端口通服务通。这个思路在绝大多数情况下确实能用但它有两个天然盲区端口通并不代表HTTP层可用。Tomcat端口能accept连接但应用上下文可能因为配置加载失败处于半死不活状态。端口通也不代表业务关键依赖可用。数据库连接池耗尽、Redis连接被占满TCP层面依然是通的可请求进来全是超时。所以云环境里的健康检查本质上要回答的不再是进程在不在而是进程能不能真正处理请求。这也是SpringBoot Actuator健康检查在云端成为标配的原因——它把进程存活这个单一维度拆成了磁盘空间、数据库连接、消息队列、缓存组件等多个维度让探针能感知到一个服务是否具备完整的服务能力。1.2 云平台探针和Actuator各管哪一段我第一次接触云平台的时候也犯过迷糊平台自带的健康检查到底查的是啥我的服务里的/actuator/health又是给谁看的这里需要分清楚云平台负载均衡、容器编排平台的探针负责的是流量调度决策。它管的是是否要把新请求转发给你检查路径、超时时间、健康阈值这些都是在平台侧配置的。SpringBoot Actuator负责的是服务自身状态汇报。服务内部把各个组件的状态汇总、判断然后对外输出一个UP或DOWN的结果。两边是上下游协作关系平台的探针只是问Actuator才是答。你完全可以在平台侧配置探针去请求/actuator/health也可以自己写一个自定义的探针路径关键是逻辑要闭环。这个闭环里有几个常见的坑我后面详细讲先记住一个核心原则健康检查的路径、超时阈值、返回码约定必须由开发、运维、平台配置三方明确对齐且要有文档记录。没有对齐就会出现开头我遇到的那种探针探测路径和服务实际健康检查路径不一致的情况。1.3 什么场景下健康检查最容易被忽略结合我在HoRain云上实践过的场景下面这些地方特别容易出问题多实例部署时某个实例内存飙升GC频繁但进程还在平台探针只看端口发现活着流量还在往这个实例打最终引起雪崩。发布新版本时新实例还没完成启动但端口已监听平台就放流量进来了导致一串connection refused或服务启动中的请求失败。数据库或Redis等中间件抖动时API还在响应但全是错误而健康状态却没有及时变成DOWN。服务优雅停机时平台需要先把实例从负载均衡摘掉再让它处理完存量请求退出顺序反了就会出现正在重启和还在接流量同时发生的混乱。以上每一种情况靠端口通就行的朴素探针都解决不了必须要靠应用层的健康检查来细化。2. Actuator核心配置从依赖到端点暴露的完整链路SpringBoot健康检查的地基是spring-boot-starter-actuator但依赖引进去只是起点能不能让探针得到正确结果取决于一堆细节配置。2.1 依赖引入与初次验证在SpringBoot 2.x时代项目里引入Actuator很简单以Maven为例dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency如果你用的是Gradleimplementation org.springframework.boot:spring-boot-starter-actuator引入依赖后重启服务直接访问本地健康检查端点验证一下curl http://localhost:8080/actuator/health此时默认返回的是简化版结果{status:UP}这里有个非常经典的坑SpringBoot 2.x之后Actuator默认只暴露health和info两个端点别的端点即使引入依赖也不会通过Web暴露。而health默认是只有总状态的想看每个组件的明细需要配置show-details。实操里我一般会这样配management: endpoints: web: exposure: include: health,info endpoint: health: show-details: alwaysshow-details配成always之后再访问/actuator/health就会看到类似这样的完整内容{ status: UP, components: { db: { status: UP, details: { database: MySQL, validationQuery: isValid() } }, diskSpace: { status: UP, details: { total: 107374182400, free: 50737418240, threshold: 10485760 } }, ping: { status: UP }, redis: { status: UP, details: { version: 6.2.6 } } } }看这个返回探针能做判断的维度一下就多了。2.2 常见组件的健康指示器是怎么自动出现的Actuator的健康检查不是一个写死的总开关而是由一组HealthIndicator自动装配组成的。只要你在classpath里有对应的客户端依赖对应的Indicator就会自动加入健康检查汇总。我在HoRain云上接触的SpringBoot项目中最常见的几个如下表所示组件依赖健康指示器类检查方式状态含义spring-boot-starter-data-redisRedisHealthIndicator执行ping命令Redis可达且可响应spring-boot-starter-jdbcDataSourceHealthIndicator执行validationQuery数据源连接可用spring-boot-starter-amqpRabbitHealthIndicator建立连接检查RabbitMQ服务可达spring-boot-starter-data-elasticsearchElasticsearchClientHealthIndicator调用cluster health APIES集群健康MongoDB驱动MongoHealthIndicator执行ping命令MongoDB可连接spring-kafkaKafkaHealthIndicator获取分区信息Kafka Broker可达无额外依赖DiskSpaceHealthIndicator检查磁盘剩余空间磁盘剩余空间超过阈值这里想特别说明一下DataSourceHealthIndicator它默认会拿连接池里的一个连接执行SELECT 1或JDBC 4.0的isValid()方法如果数据库不可达或者账号权限不对健康状态就会变成DOWN同时细节里会带上错误信息。这在排查问题时很有用但生产环境要注意show-details如果配成always数据库连接细节可能被探活请求全部拉出来敏感点不少所以更稳妥的做法是配成when-authorized让外部探针只看到UP/DOWN内部账号才能看细节。2.3 云上必须注意的端口和路径坑在HoRain云上部署时服务通常在容器里跑平台探针访问的是服务端口这里有一个很多人容易踩的配置冲突management.server.port如果单独设置Actuator端点就不在业务端口上了。比如这样配置management: server: port: 9090那么/actuator/health就只能通过9090端口访问业务8080端口访问会404。容器化部署时如果不记得给这个端口也做端口映射、或暴露到探针配置里探针就会一直探测失败。我个人的习惯是除非有强烈的安全隔离需求否则不单独拆分management端口让Actuator和业务共用主端口通过路径区分这样平台探针和本地排查的口径完全一致。另一个坑是context-path。如果项目配置了server: servlet: context-path: /myservice那么健康检查的实际地址就变成了/myservice/actuator/health。云平台探针如果配的是/actuator/health一样会404。这类问题在真实环境里特别隐蔽因为你本地访问带了context-path能通但平台探针不带两边谁都没错就是没对齐。3. 健康检查与云平台探针的联动liveness、readiness与存活探针健康检查在云平台里并不是只有一个健康检查的概念。以HoRain云容器编排为例探针按职责可以拆成存活探针liveness、就绪探针readiness以及启动探针startupSpringBoot Actuator的端点设计恰好可以一一对应。3.1 存活探针livenessProbe适合挂在哪个端点上存活探针的作用是判断容器要不要被重启。如果存活探针失败平台会杀掉容器重新拉一个实例。这通常适用于进程死锁、内存泄漏导致无法恢复服务的情况。在SpringBoot里存活探针通常对应的就是/actuator/health这个端点。不过这里要特别留意如果把所有组件的状态都汇总进这个端点那么当数据库短暂抖动时健康状态会短暂变成DOWN平台如果把它配成存活探针就可能把容器杀掉——但数据库抖动恢复后这个实例本来是可以自愈的被杀掉重启反而放大了故障影响。所以实际操作里存活探针更合适的做法是配置一个只检查进程和JVM存活的轻量端点。SpringBoot从2.2版本开始引入了两组独立的健康组management: endpoint: health: group: liveness: include: ping readiness: include: db,redis,diskSpace配置之后存活探针可以指向/actuator/health/liveness它只包含ping这个心跳检查就绪探针指向/actuator/health/readiness它才包含数据库、缓存这些业务依赖。这类机制在Kubernetes的探针配置里非常有用——livenessProbe失败会重启容器readinessProbe失败只会从Service的Endpoints里摘除不会重启。两者的语义完全不一样混用会有大麻烦。3.2 就绪探针readinessProbe的语义映射就绪探针判断的是这个实例有没有准备好接收流量。SpringBoot里对应的就是/actuator/health/readiness。如果数据库连不上readiness应该返回非200状态码平台就会把这个实例从负载均衡池里摘掉流量转到其他健康实例等数据库连接恢复readiness自动回UP实例重新接入。这个机制的妙处在于它能让故障实例不再接收新流量但同时保留进程让它在后台自己恢复。这样比起一发现异常就重启对下游的冲击小很多特别是对数据库一类的共享依赖抖动能显著降低整个系统的雪崩概率。3.3 启动探针与长时间启动服务的优雅处理SpringBoot应用在云上还有一个老大难问题启动慢。尤其是大型单体应用启动过程中要初始化数据源、加载配置、初始化线程池动不动就几十秒。如果不做特殊处理平台的存活探针可能在启动期间就判定不健康而杀掉容器导致启动永远无法完成。Kubernetes从1.16开始引入了startupProbe而HoRain云这类容器平台大多也支持类似的启动保护机制。startupProbe会先独占检查权等它成功了livenessProbe才会接管后续检查。配一个比较宽松的超时和失败阈值比如startupProbe: httpGet: path: /actuator/health port: 8080 failureThreshold: 30 periodSeconds: 5这样服务即便启动需要两分多钟也能安全度过启动期。不过光靠平台侧放宽探针还不是根本解。我见过很多SpringBoot服务启动慢是因为在初始化阶段做了大量同步操作比如启动时拉取远程配置、预热缓存、初始化连接池等。这些操作在本地开发环境感觉不到因为机器性能好、依赖快但云端实例规格如果不高启动时间很容易翻倍。可以把一部分非关键初始化改成异步执行或者把PostConstruct里的逻辑挪到ApplicationReadyEvent之后再跑这样端口监听和完全就绪之间的窗口就会小很多。4. 云上健康检查失灵的典型案例从现象到排查链路配置都对了是不是就万事大吉当然不是。我在一线排查过太多健康检查明明UP但业务异常的诡异问题下面挑几个有代表性的案例把完整排查过程写出来给大家一个可复现的思路。4.1 案例一健康检查一直显示UP但新版本流量依然打到旧实例现象在HoRain云上滚动发布的时候平台显示旧实例已经不再接收流量但实测发现仍有少部分请求落入旧实例。排查过程我先看容器生命周期是不是真的走完了——结果容器确实已经终止。再看平台负载均衡的会话保持会话保持配置发现开启了基于来源IP的会话保持导致一部分客户端连接长期绑定在旧实例上。旧实例虽然收到SIGTERM开始优雅停机但只要进程没完全退出这些存量连接就一直被复用。最后我们调整了发布流程先把这个实例从负载均衡摘除人为等待存量连接自然超时回收再发终止信号同时把会话保持的时间阈值改短避免连接长时间绑死在某一个实例上。这个案例给我们的教训是健康检查只负责新流量别打过来但存量流量怎么断开还要看负载均衡的会话保持策略和优雅停机配合。4.2 案例二数据库连接池耗尽但健康检查还是绿的现象某个服务白天高峰期频繁出现接口超时但看/actuator/health一直返回UP平台探针也没有告警业务方表示很懵。排查过程先看数据库连接池的监控指标发现active连接数已经打到上限等待队列里堆满了请求。为什么连接池耗尽健康检查还是UP因为DataSourceHealthIndicator检查时只是从池里获取一个新连接或借到一个连接并执行验证而如果连接池配置了无限等待它就会一直等着池里释放连接直到超时才标记DOWN。在高并发下这个检查请求自身也排在队伍里自然表现为排队等连接而不是瞬间拿到连接。处理方式上我给连接池配置了足够大的最大等待时间并在DataSourceHealthIndicator里额外设置了validation-query超时时间确保检查请求不会被业务请求堵死。更关键的是把连接池的使用率和等待队列长度接入了监控告警在资源真正耗尽之前就发出预警而不是等健康检查翻红才知道。4.3 案例三健康检查线程池满导致误报现象一个服务的健康检查偶发性地显示DOWN但业务指标全部正常数据库、Redis也都正常。每次DOWN持续十几秒又自动恢复。排查过程先打开健康检查的详细日志看看DOWN时到底是谁触发的。结果发现几乎每次都是由SessionDisconnectException引起的具体看堆栈是Tomcat的NIO线程在处理请求时出的异常。健康检查的HTTP请求和业务请求共享同一个Tomcat容器当业务线程池满了或者某个慢接口占住了线程本轮健康检查请求就可能超时被断连从而返回DOWN。这就造成了一个很尴尬的情况业务繁忙反而导致健康检查误报DOWN负载均衡收到非200状态码就把实例摘掉了流量被集中到其他实例反而加剧整体压力。解决方案是把管理端口独立出来给Actuator单独开一个容器或线程池让健康检查的请求不被业务请求饿死。配置方式就是之前提到的management.server.port单独设定再结合独立的max-threads来管理management: server: port: 9090 tomcat: max-threads: 16运维在配置探针时也要保证探针目标端口是独立的management端口这样健康检查请求和业务请求彻底隔离。如果不方便拆端口至少也要给Tomcat配置合理的最大工作线程数避免线程池被慢请求打满。4.4 案例四优雅停机后健康检查还显示UP的窗口期现象发布时平台显示实例已进入终止中状态但网关还能路由少量请求到这个实例上偶尔报错。排查过程看平台事件流发现实例在收到SIGTERM之后SpringBoot开始执行优雅停机但此时暴露给负载均衡的健康检查端口还开放着负载均衡需要等到下一个健康检查周期才能感知到服务下线这中间存在一个时间窗口。处理方式分两步一是在SpringBoot里配置优雅停机server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s二是让服务在收到关闭信号时提前把健康检查状态改为DOWN相当于主动告知调度中心我不行了。这部分没有现成的配置项需要自己做一个监听Component public class GracefulShutdownListener { EventListener public void onShutdown(ContextClosedEvent event) { // 标记健康检查状态为DOWN配合HealthContributor实现 // 例如通过一个AtomicBoolean控制自定义HealthIndicator } }核心思路很简单收到关闭信号后先把健康状态置为DOWN让平台快速摘除流量再进入SpringBoot的优雅停机阶段处理存量请求。这个先摘流量再停机的顺序是解决滚动发布报错的关键。5. 自定义健康检查指标把业务规则纳入探针默认的健康检查指标主要面向中间件和技术组件但很多故障其实是业务层面的比如依赖的某个第三方接口超时、核心业务表积压数据过多、某个分布式任务调度器失联等。这些情况默认的HealthIndicator根本感知不到需要自己动手扩展。5.1 实现一个自定义HealthIndicator的完整示例在SpringBoot里自定义健康检查很简单核心是继承HealthIndicator接口并实现health()方法Component public class OrderServiceHealthIndicator implements HealthIndicator { private final OrderServiceClient orderServiceClient; public OrderServiceHealthIndicator(OrderServiceClient orderServiceClient) { this.orderServiceClient orderServiceClient; } Override public Health health() { try { // 调用核心业务依赖比如订单服务的可用性检测接口 OrderServiceStatus status orderServiceClient.checkHealth(); if (status.isHealthy()) { return Health.up() .withDetail(orderService, available) .withDetail(code, status.getCode()) .build(); } return Health.down() .withDetail(orderService, unavailable) .withDetail(reason, status.getErrorMessage()) .build(); } catch (Exception e) { return Health.down(e) .withDetail(orderService, exception) .build(); } } }这样写的好处是自动纳入整个Actuator健康检查框架不需要额外的定时任务不需要单独维护状态请求/actuator/health时框架会自动调用这个Indicator汇总进总状态。5.2 如何给健康状态附加业务上下文信息有时候光是一个DOWN还不够运维看到告警冲过去想定位问题还得翻一堆日志。更合理的做法是把上下文信息通过details带出来。比如我遇到过一个场景依赖的上游接口有熔断降级健康检查结果为DOWN但details里如果只带timeout运维依然不知道是哪个接口、哪个环境。所以我通常会这样加return Health.down() .withDetail(upstream, payment-gateway) .withDetail(env, environment.getProperty(spring.profiles.active)) .withDetail(error, e.getMessage()) .withDetail(timestamp, LocalDateTime.now().toString()) .build();这样接到告警后不需要额外登录实例就能从健康检查结果里快速定位到是哪个上游、什么异常、什么时间发生的。5.3 为什么自己写的HealthIndicator容易忽略超时自己写HealthIndicator有个小坑方法内部如果去调用第三方接口没有设置超时时间而第三方接口挂起不返回这个健康检查线程就会被一直占住。多个这类Indicator叠起来健康检查整体响应就会变慢甚至可能连累整个健康检查接口。所以我在写这类代码的时候会坚持两个原则所有远程调用必须显式设置超时时间宁可检查不到也不要阻塞线程。对健康检查里的外部调用可以做超时降级——比如超过2秒就返回DOWN同时details里带上timeout而不是一直等下去。try { OrderServiceStatus status orderServiceClient.checkHealth(); ... } catch (Exception e) { // 包含超时、连接拒绝等 return Health.down(e).build(); }同时在底层HTTP客户端或RPC框架层面也配置好全局超时而不是在Indicator这一层才想到设置。6. 从HoRain云部署视角看健康检查的进阶实践前面讲的大多是应用内部最后把视角拉到部署和运维层面专门聊一聊在HoRain云这类云平台上健康检查在部署编排和日常运维中还能怎么玩以及一些容易被忽略但值得一起配好的细节。6.1 健康检查在容器编排里的配置参考在容器化部署时我一般不会只配一个healthcheck了事而是把启动、存活、就绪分开配。以下是一个YAML片段示例可以作为参考spec: containers: - name: springboot-app image: harbor.example.com/springboot-app:1.0.0 ports: - containerPort: 8080 startupProbe: httpGet: path: /actuator/health port: 8080 failureThreshold: 30 periodSeconds: 5 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 30 periodSeconds: 20 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 0 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3要单独用/actuator/health/liveness和/actuator/health/readiness就需要像前面那样配置健康组。如果不想拆健康组也可以让liveness和readiness都指向/actuator/health但这样语义容易混生产环境我强烈建议拆开。6.2 健康检查与配置中心的联动另一件容易被忽略的事是健康检查结果和配置中心的关系。在HoRain云上很多SpringBoot服务会接配置中心比如Spring Cloud Config、Apollo、Nacos等。健康检查应该反映配置是否可用吗我认为应该但默认的Actuator不一定包含这类Indicator需要自己扩展。比如Nacos有一个NacosHealthIndicatorSpring Cloud Config也有ConfigServerHealthIndicator但这些不是默认激活的要确认依赖版本和是否自动装配成功。如果配置中心不可用但服务本地缓存还能支撑此时把健康状态标DOWN是否合理取决于业务设计。如果配置中心挂了服务还能正常用那充其量是WARN如果配置中心挂了一会服务就会出问题那DOWN才是合理的。这里面没有标准答案要结合业务容错设计来决定。我建议的做法是在就绪探针映射的健康组里把配置中心、路由中心这类强依赖放进去而在存活探针对应的健康组里只保留最基础的进程级检查。这样既不会误杀实例又能及时摘除流量。6.3 健康检查的监控与告警不只是给负载均衡看的最后一点很多人觉得健康检查只是给负载均衡看的配好就让平台自己跑。实际上健康检查结果本身应该接入监控告警体系。我在HoRain云上通常会配这样几个维度的指标和告警服务健康检查状态持续DOWN超过一定时间说明实例异常或依赖故障告警出来不过分。健康检查结果的component维度突然从UP变成DOWN比如Redis健康检查DOWN即使整体服务由于非关键依赖原因还是UP也应该告警因为这可能是故障的前兆。健康检查响应的耗时逐步升高说明服务内部开始出现过载或阻塞也可以作为一个早期信号。这些指标在Actuator的Metrics里基本都能拿到比如spring_boot_health_*、http.server.requests等接入Prometheus之后用Alertmanager或云平台自带的告警规则做阈值告警即可。以Prometheus为例一条简单的告警规则可以这样写groups: - name: springboot-health rules: - alert: InstanceHealthDown expr: spring_boot_health{statusDOWN} 1 for: 2m labels: severity: critical annotations: summary: SpringBoot实例健康检查DOWN把健康检查变成可观测的指标之后很多潜在故障才有机会在真正影响用户之前被我们发现。6.4 几个部署时的额外注意点最后再分享几个我在HoRain云上部署SpringBoot服务时总结的经验容易踩但查起来又很隐蔽一是时区问题。容器默认时区是UTC而健康检查里如果带了时间戳和告警平台的时间对不上排查起来会很痛苦。容器启动命令里加一句-Duser.timezoneAsia/Shanghai或者通过环境变量TZAsia/Shanghai能让很多日志、指标、时间戳口径一致。二是镜像里不要留冗余的调试工具。生产镜像里尽量只装运行所需的东西避免攻击面扩大也避免启动时被安全扫描拖慢。健康检查是应用层行为不依赖容器里的bash或curl所以不要在镜像里刻意保留这些二进制。三是别把健康检查的返回内容打得太详细。生产环境show-details配置成when-authorized或never更稳妥否则每次探针请求都会把数据库版本、Redis版本、内存状态这些信息暴露给任何能访问该端口的人。如果一定需要详细状态用于排查建议放到内网监控平台去拉而不是对平台探针开放。四是探针的请求频率不要太激进。有些平台默认的探针周期非常短比如每2秒一次对SpringBoot应用来说高频率的请求会额外增加JVM压力不说还容易和一些GC日志、监控采集叠加制造出健康检查本身拖垮服务的荒诞场景。建议至少10秒起步配合合理的超时时间和失败阈值。我自己的经验是把健康检查当成一个有业务含义、可监控、可预警的系统能力来建设而不是平台那边随便配一个路径就行的小事。很多线上疑难故障最后兜兜转转都能追溯到健康检查语义不清晰、探针路径不一致、状态维度太粗这些基础问题上。先把这些基础的环节做扎实云上部署SpringBoot的稳定性就赢了一大半。