文章 601
评论 5
浏览 236339
3 行代码给你的 Spring Boot 应用加上健康检查 + 就绪探针

3 行代码给你的 Spring Boot 应用加上健康检查 + 就绪探针

一、引言

"我的应用明明启动成功了,为什么 K8s 一直把流量打过来又切走?"

相信很多同学在 K8s 部署 Spring Boot 应用时都遇到过这个问题。根本原因在于:健康检查配置不当

想象一下这个场景:

  • 应用启动需要 30 秒初始化数据库连接池、加载缓存
  • K8s 在启动后 5 秒就开始探测 readiness
  • 由于初始化未完成,readiness 探针失败
  • K8s 认为 Pod 未就绪,将其从 Service 中移除
  • 30 秒后应用初始化完成,但此时流量已经被分发到其他实例

这就是典型的探针配置与应用启动时间不匹配问题。


二、Spring Boot Actuator 健康检查体系

2.1 三个核心端点

Spring Boot Actuator 自带三个健康检查端点:

# /actuator/health      - 综合健康检查
# /actuator/health/liveness   - 存活探针:应用本身是否正常运行
# /actuator/health/readiness  - 就绪探针:是否准备好接收流量

关键区别

端点作用失败后果检查内容
/actuator/health综合健康状态所有组件的聚合状态
/actuator/health/liveness判断是否需要重启K8s 杀死并重启容器应用进程是否存活
/actuator/health/readiness判断是否可接收流量K8s 从 Service 移除依赖服务是否就绪

2.2 开启探针的 3 行配置

# application.yml
management:
  endpoint:
    health:
      probes:
        enabled: true  # 开启 liveness/readiness 探针

注意:Spring Boot 3.x 在 K8s 环境中会自动检测并开启探针,但建议显式配置以确保一致性。


三、自定义 HealthIndicator

3.1 数据库连接检查

@Component
public class DatabaseHealthIndicator implements HealthIndicator {
    
    @Autowired
    private DataSource dataSource;
    
    @Override
    public Health health() {
        try (Connection connection = dataSource.getConnection()) {
            if (connection.isValid(1)) {
                return Health.up()
                        .withDetail("database", "MySQL")
                        .withDetail("version", connection.getMetaData().getDatabaseProductVersion())
                        .build();
            }
            return Health.down().withDetail("error", "数据库连接无效").build();
        } catch (SQLException e) {
            return Health.down(e).withDetail("error", "数据库连接失败").build();
        }
    }
}

3.2 Redis 连接检查

@Component
public class RedisHealthIndicator implements HealthIndicator {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Override
    public Health health() {
        try {
            String result = redisTemplate.opsForValue().get("health_check");
            if (result != null || redisTemplate.getConnectionFactory().getConnection().ping() != null) {
                return Health.up()
                        .withDetail("redis", "connected")
                        .withDetail("version", "unknown")
                        .build();
            }
            return Health.down().withDetail("error", "Redis 响应为空").build();
        } catch (Exception e) {
            return Health.down(e).withDetail("error", "Redis 连接失败").build();
        }
    }
}

3.3 RabbitMQ 连接检查

@Component
public class RabbitMQHealthIndicator implements HealthIndicator {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Override
    public Health health() {
        try {
            rabbitTemplate.execute(channel -> {
                channel.queueDeclarePassive("health_check");
                return null;
            });
            return Health.up()
                    .withDetail("rabbitmq", "connected")
                    .build();
        } catch (Exception e) {
            return Health.down(e).withDetail("error", "RabbitMQ 连接失败").build();
        }
    }
}

3.4 健康检查响应示例

curl http://localhost:8080/actuator/health

响应:

{
  "status": "UP",
  "components": {
    "database": {
      "status": "UP",
      "details": {
        "database": "MySQL",
        "version": "8.0.33"
      }
    },
    "redis": {
      "status": "UP",
      "details": {
        "redis": "connected"
      }
    },
    "rabbitmq": {
      "status": "UP",
      "details": {
        "rabbitmq": "connected"
      }
    },
    "readinessState": {
      "status": "UP",
      "details": {
        "state": "ACCEPTING_TRAFFIC"
      }
    },
    "livenessState": {
      "status": "UP",
      "details": {
        "state": "CORRECT"
      }
    }
  }
}

四、Liveness vs Readiness 深度解析

4.1 一个生动的比喻

Liveness(存活探针)= 人是否活着?
  - 如果死了 → 必须重启(火化+投胎)
  - 如果活着 → 不管是否生病,都保持存活状态

Readiness(就绪探针)= 人是否能工作?
  - 如果感冒发烧 → 暂时不让工作,但人还活着
  - 如果康复了 → 重新开始工作
  - 如果彻底不行了 → 可能需要 liveness 来重启

4.2 关键区别对比

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  Liveness(存活探针)                                            │
│  ├── 作用:判断容器是否还"活着"                                  │
│  ├── 失败行为:K8s 发送 SIGKILL 强制杀死并重启容器               │
│  ├── 检查内容:应用进程是否正常运行                              │
│  ├── 典型场景:JVM 卡死、死循环、内存溢出                        │
│  └── 设计原则:只检查应用本身,不检查依赖                        │
│                                                                 │
│  Readiness(就绪探针)                                           │
│  ├── 作用:判断容器是否"准备好接收流量"                          │
│  ├── 失败行为:K8s 将 Pod 从 Service endpoints 中移除            │
│  ├── 检查内容:依赖服务是否可用、初始化是否完成                  │
│  ├── 典型场景:数据库连接失败、Redis 连接失败、MQ 连接失败        │
│  └── 设计原则:检查所有关键依赖                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.3 设计原则

Liveness 探针设计原则

  1. 轻量级:检查速度要快,避免影响应用性能
  2. 只检查应用本身:不检查外部依赖
  3. 保守策略:宁可误判为存活,也不要轻易重启

Readiness 探针设计原则

  1. 全面性:检查所有关键依赖
  2. 快速失败:依赖不可用时立即返回失败
  3. 自动恢复:依赖恢复后自动重新加入 Service

五、K8s 探针配置最佳实践

5.1 配置参数详解

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 10   # 启动后延迟多久开始检查
  periodSeconds: 5          # 检查间隔
  timeoutSeconds: 3         # 单次检查超时时间
  failureThreshold: 3       # 连续失败多少次判定为失败
  successThreshold: 1       # 连续成功多少次判定为成功

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 60   # 启动后延迟更久才开始检查
  periodSeconds: 10         # 检查间隔更长
  timeoutSeconds: 5         # 超时时间更长
  failureThreshold: 3       # 失败阈值
  successThreshold: 1       # 成功阈值

5.2 参数配置经验值

参数Liveness 推荐值Readiness 推荐值说明
initialDelaySeconds60-120s10-30sliveness 应晚于 readiness
periodSeconds10-30s5-10sreadiness 检查更频繁
timeoutSeconds5-10s3-5sliveness 允许更长响应时间
failureThreshold3-53连续失败次数

5.3 计算探针失败时间

Readiness 失败时间 = initialDelaySeconds + periodSeconds * failureThreshold
                  = 10 + 5 * 3 = 25s

Liveness 失败时间 = initialDelaySeconds + periodSeconds * failureThreshold
                 = 60 + 10 * 3 = 90s

六、优雅停机方案

6.1 问题分析

没有优雅停机的后果:

K8s 删除 Pod → 发送 SIGTERM → 容器立即停止 → 正在处理的请求被中断 → 用户收到 5xx 错误

6.2 正确的优雅停机流程

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  1. K8s 触发 Pod 删除                                           │
│     ↓                                                           │
│  2. preStop Hook 执行(sleep 10s)                               │
│     ↓                                                           │
│  3. Readiness 探针开始失败(可选)                                │
│     ↓                                                           │
│  4. K8s 将 Pod 从 Service endpoints 移除                        │
│     ↓                                                           │
│  5. 新请求不再进入此 Pod                                         │
│     ↓                                                           │
│  6. K8s 发送 SIGTERM 信号                                       │
│     ↓                                                           │
│  7. Spring Boot 开始优雅关闭流程                                 │
│     ↓                                                           │
│  8. 请求处理线程池逐渐关闭                                        │
│     ↓                                                           │
│  9. 在 terminationGracePeriodSeconds 内完成关闭                  │
│     ↓                                                           │
│  10. 容器被删除                                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

6.3 配置优雅停机

Spring Boot 配置

# application.yml
server:
  shutdown: graceful      # 开启优雅停机

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # 每个关闭阶段的超时时间

K8s 配置

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 45  # 总停机时间 = preStop + shutdown + buffer
      containers:
      - name: app
        image: your-app:latest
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sleep", "10"]  # 等待流量迁移

6.4 时间计算

总停机时间 = preStop 时间 + timeout-per-shutdown-phase + 缓冲时间

示例:
preStop: 10s
timeout-per-shutdown-phase: 30s
缓冲时间: 5s

terminationGracePeriodSeconds = 10 + 30 + 5 = 45s

七、完整示例

7.1 application.yml

server:
  port: 8080
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
  datasource:
    url: jdbc:mysql://localhost:3306/example_db
    username: admin
    password: password
  redis:
    host: localhost
    port: 6379
  rabbitmq:
    host: localhost
    port: 5672

management:
  endpoint:
    health:
      probes:
        enabled: true
      show-details: always
  endpoints:
    web:
      exposure:
        include: health,info

7.2 deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  labels:
    app: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      terminationGracePeriodSeconds: 45
      containers:
      - name: order-service
        image: order-service:latest
        ports:
        - containerPort: 8080
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"
          requests:
            memory: "256Mi"
            cpu: "250m"
        
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3
          successThreshold: 1
        
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
          successThreshold: 1
        
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sleep", "10"]

7.3 service.yaml

apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
  - port: 80
    targetPort: 8080
  type: ClusterIP

八、常见问题与解决方案

8.1 Readiness 失败但 Liveness 成功

场景:数据库连接失败,但应用本身正常运行

行为

  • Readiness 返回 DOWN → K8s 将 Pod 从 Service 移除
  • Liveness 返回 UP → K8s 不会重启容器
  • 效果:Pod 不再接收流量,但保持运行状态
  • 恢复:数据库恢复后,Readiness 自动恢复,Pod 重新加入 Service

8.2 Liveness 失败导致频繁重启

场景:JVM 卡死、死循环、内存溢出

行为

  • Liveness 返回 DOWN → K8s 杀死并重启容器
  • 效果:应用被强制重启

排查方向

  • 检查 GC 日志,是否存在内存泄漏
  • 检查线程 Dump,是否存在死锁或死循环
  • 检查资源使用情况,是否超过限制

8.3 启动时探针误判

场景:应用启动时间较长,探针在启动过程中失败

解决方案

  • 增大 initialDelaySeconds
  • 使用 startupProbe(K8s 1.18+):
startupProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 12  # 最多等待 10 + 5 * 12 = 70s

8.4 优雅停机导致 Pod 删除缓慢

场景terminationGracePeriodSeconds 设置过大

解决方案

  • 根据实际停机时间调整
  • 优化应用关闭流程
  • 使用 preStop 钩子减少等待时间

九、最佳实践总结

9.1 探针设计原则

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  1. Liveness 探针:只检查应用本身                                │
│     - 轻量级检查                                                │
│     - 不检查外部依赖                                            │
│     - 保守策略                                                  │
│                                                                 │
│  2. Readiness 探针:检查所有关键依赖                              │
│     - 数据库连接                                                │
│     - Redis 连接                                                │
│     - 消息队列连接                                              │
│     - 外部 API                                                  │
│                                                                 │
│  3. 探针参数配置                                                │
│     - initialDelaySeconds 要足够大                              │
│     - liveness 应晚于 readiness 开始检查                        │
│     - 合理设置 failureThreshold                                │
│                                                                 │
│  4. 优雅停机配置                                                │
│     - server.shutdown=graceful                                  │
│     - preStop hook 等待流量迁移                                  │
│     - terminationGracePeriodSeconds >= 总停机时间                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

9.2 记忆口诀

Liveness = 活着吗?死了就重启
Readiness = 准备好了吗?没准备好就不让进

探针配置三要素:
1. 延迟启动(initialDelay)
2. 检查频率(period)
3. 失败阈值(failureThreshold)

优雅停机三要素:
1. preStop 等待
2. graceful shutdown
3. terminationGracePeriodSeconds

💡 互动话题:你们团队的健康检查是怎么配置的?有没有遇到过探针配置不当导致的问题?欢迎在评论区分享!


标题:3 行代码给你的 Spring Boot 应用加上健康检查 + 就绪探针
作者:jiangyi
地址:http://jiangyi.space/articles/2026/07/25/1784450957598.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消