K8s探针完全指南:startupProbe、livenessProbe、readinessProbe的调优
K8s探针完全指南:startupProbe、livenessProbe、readinessProbe的调优
适读人群:在K8s上部署Java服务的工程师 | 阅读时长:约20分钟 | 适用版本:K8s 1.20+、Spring Boot 2.3+
开篇故事
那是一个黑色星期一,我们刚上线了一个新功能,结果滚动更新期间,有10分钟的时间,大量请求打到了还没完全就绪的新Pod上,返回了一堆503。用户投诉热线都打爆了。
事后复盘,问题出在readinessProbe配置太激进:Spring Boot服务启动需要40秒,但我们的readinessProbe是initialDelaySeconds: 10,10秒后就开始检查,前30秒检查必然失败。但failureThreshold: 3,连续3次失败就标记为不可用,然后重试,重试期间如果Deployment的滚动策略允许,新Pod还是可能接收流量……
最根本的问题在于,我们没有区分三种探针的语义,把三个探针的配置写成一模一样的,完全没有发挥各自的作用。
今天把三种探针的原理、语义和调优策略完整梳理一遍,这是K8s部署Java服务必须掌握的基础功。
一、核心问题分析
三种探针解决的不同问题
livenessProbe(存活探针):解决容器进程"僵尸化"问题。Java进程活着不代表服务正常,可能出现死锁、GC停顿导致响应超时、线程池耗尽等情况。存活探针检测到这些异常后,通过重启容器来恢复服务。
readinessProbe(就绪探针):解决"启动中"的服务不接收流量的问题。Spring Boot启动需要时间,在完全就绪之前,Pod不应该接收任何流量。就绪探针失败时,K8s会把这个Pod从Service的Endpoints里摘除,流量不再分发给它。
startupProbe(启动探针):K8s 1.16引入,解决慢启动应用的问题。在startupProbe检查通过之前,livenessProbe和readinessProbe都不会启动。这允许给启动慢的应用足够的时间,而不会被存活探针误判死亡。
三者的执行时序
二、原理深度解析
三种检查方式的比较
K8s提供三种探针检查机制:
HTTP GET:向指定端口和路径发送HTTP请求,返回2xx或3xx为成功。最常用,Spring Boot Actuator天生支持。
TCP Socket:尝试建立TCP连接,连接成功为健康。适用于没有HTTP接口的服务,比如纯TCP协议的自定义服务。
Exec:在容器内执行命令,退出码为0表示成功。灵活性最高,但有性能开销(每次检查都要fork一个进程)。
Spring Boot Actuator健康端点的分级
Spring Boot 2.3引入了两个分级的健康端点,和K8s的探针完美对应:
/actuator/health/liveness:存活检查,只检查应用自身状态,不检查外部依赖。即使数据库挂了,只要应用进程本身运行正常,这个端点就返回UP。
/actuator/health/readiness:就绪检查,检查所有外部依赖(数据库、Redis、消息队列等),任何一个挂了就返回DOWN。
这个设计非常重要:如果存活探针也检查数据库,数据库一挂所有Pod都被重启,重启期间数据库连接池重建,可能引发雪崩。正确做法是存活探针只检查应用自身,就绪探针检查外部依赖。
三、完整配置实现
Spring Boot Actuator配置
# application.yml
spring:
application:
name: order-service
management:
endpoints:
web:
exposure:
include: health,info,prometheus
base-path: /actuator
endpoint:
health:
show-details: always
# 开启分组健康检查(K8s专用)
probes:
enabled: true
group:
liveness:
include:
- livenessState
- diskSpace
readiness:
include:
- readinessState
- db
- redis
- rabbit # 如果有RabbitMQ
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
db:
enabled: true
redis:
enabled: true完整的K8s探针配置
# k8s-deployment-with-probes.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: order-service
# 滚动更新策略
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多额外创建1个Pod
maxUnavailable: 0 # 不允许任何Pod不可用(零停机更新)
template:
metadata:
labels:
app: order-service
spec:
# 优雅终止时间(要比preStop时间长)
terminationGracePeriodSeconds: 60
containers:
- name: order-service
image: registry.company.com/order-service:1.2.3
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JAVA_OPTS
value: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:+ExitOnOutOfMemoryError
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "2Gi"
# =============================================
# startupProbe:解决Spring Boot慢启动问题
# 最多等待:10s * 30次 = 300秒(5分钟)
# 这段时间内liveness和readiness都不工作
# =============================================
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
# 每10秒检查一次
periodSeconds: 10
# 超时时间
timeoutSeconds: 5
# 最多失败30次(即最多等待5分钟)
failureThreshold: 30
# 成功一次即算通过
successThreshold: 1
# =============================================
# livenessProbe:检测应用是否"僵死"
# 注意:只检查liveness端点,不检查外部依赖!
# =============================================
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
# 可以加请求头,用于日志过滤
httpHeaders:
- name: X-Probe-Type
value: liveness
# 每30秒检查一次(不需要太频繁)
periodSeconds: 30
# 超时时间(如果GC停顿,可能需要更长)
timeoutSeconds: 10
# 连续3次失败才重启(避免因短暂GC导致的误重启)
failureThreshold: 3
successThreshold: 1
# =============================================
# readinessProbe:控制是否接收流量
# 检查所有依赖(DB、Redis等)
# =============================================
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
# 每10秒检查一次(比liveness频繁,流量控制更及时)
periodSeconds: 10
timeoutSeconds: 5
# 连续2次失败就摘除流量(快速响应)
failureThreshold: 2
# 连续2次成功才加入流量(避免抖动后立即恢复)
successThreshold: 2
# 优雅终止:接到SIGTERM后先等30秒再真正退出
lifecycle:
preStop:
exec:
command:
- sh
- -c
- "sleep 10" # 等待负载均衡更新,停止接收新请求针对不同类型应用的探针配置模板
快速启动应用(启动时间 < 30秒):
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 5
failureThreshold: 12 # 最多等60秒
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 20
failureThreshold: 3
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
failureThreshold: 2
successThreshold: 2
timeoutSeconds: 3慢启动应用(启动时间30~120秒,如加载大量缓存):
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
failureThreshold: 18 # 最多等3分钟(180秒)
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 30
failureThreshold: 3
timeoutSeconds: 15 # GC停顿应用要更长的超时
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 10
failureThreshold: 3
successThreshold: 1
timeoutSeconds: 5四、生产最佳实践
探针调优的参考数据
根据我们在十几个Java服务上的实测数据,整理了以下参考基准:
| 应用类型 | 典型启动时间 | startupProbe failureThreshold | liveness periodSeconds | readiness periodSeconds |
|---|---|---|---|---|
| 简单CRUD服务 | 15-25秒 | 12(120秒余量) | 20 | 5 |
| 带缓存预热 | 40-90秒 | 20(200秒余量) | 30 | 10 |
| 大型单体服务 | 60-120秒 | 24(240秒余量) | 30 | 10 |
| 大数据处理服务 | 120-300秒 | 36(360秒余量) | 60 | 30 |
探针与HPA的联动
当Pod的readinessProbe失败时,该Pod会从HPA的统计里排除,避免因为一个不健康的Pod拉低整体指标,导致HPA不必要地扩容。这是个经常被忽视的细节。
自定义健康检查指标
对于某些场景,Spring Boot Actuator的默认健康检查不够用,需要自定义:
// 自定义健康指标:检查内部任务队列是否积压
@Component
public class TaskQueueHealthIndicator implements HealthIndicator {
private final TaskQueue taskQueue;
public TaskQueueHealthIndicator(TaskQueue taskQueue) {
this.taskQueue = taskQueue;
}
@Override
public Health health() {
int queueSize = taskQueue.size();
int maxAllowed = 10000;
if (queueSize > maxAllowed) {
return Health.down()
.withDetail("queueSize", queueSize)
.withDetail("maxAllowed", maxAllowed)
.withDetail("reason", "任务队列积压严重")
.build();
}
return Health.up()
.withDetail("queueSize", queueSize)
.build();
}
}五、踩坑实录
坑一:livenessProbe检查外部依赖导致雪崩
这是最经典的坑,我自己踩过,也见过很多团队踩。把livenessProbe配成检查/actuator/health,这个端点默认包含数据库健康检查。结果某次MySQL主从切换,主库短暂不可用约20秒,这20秒内所有Pod的livenessProbe检查都失败了。
20秒后,达到failureThreshold: 3,所有Pod同时触发重启。重启期间连接池重建,产生大量新连接请求打向刚切换完的数据库,数据库连接数瞬间飙升,数据库再次压力过大……雪崩就这样发生了。
正确配置:livenessProbe只检查/actuator/health/liveness,这个端点不包含数据库检查。数据库挂了,readinessProbe会摘除流量,livenessProbe不重启Pod,等数据库恢复后readinessProbe自然恢复。
坑二:没有startupProbe导致滚动更新缓慢
有个服务启动需要60秒(要做大量缓存预热),我们的livenessProbe是initialDelaySeconds: 90。结果在K8s做滚动更新时,每个新Pod要等90秒才开始接受检查,再等几次成功才接受流量,整个滚动更新异常缓慢,3个Pod的更新要花10分钟以上。
引入startupProbe后,流程变成:新Pod启动→startupProbe每10秒检查一次→约70秒时通过→立即开始readinessProbe检查→一两次后加入流量。每个Pod的等待时间从90秒降到了约80秒,但更重要的是,readinessProbe的检查变得精确,不再有"多等30秒保险"的冗余。
坑三:readinessProbe timeoutSeconds太短触发误判
某个服务在高负载时期,数据库响应慢(但没挂),/actuator/health/readiness响应时间有时超过3秒。而我们的readinessProbe配的是timeoutSeconds: 2,导致流量高峰时Pod频繁被标记为NotReady,流量在剩余Pod之间分配,压力更大,形成恶性循环。
解决方案:readinessProbe的timeoutSeconds要根据实际情况设置,健康检查接口本身也应该加超时控制,避免健康检查因为DB慢查询而响应超时:
management:
health:
db:
# 健康检查的超时时间
timeout: 3sreadinessProbe的timeoutSeconds要比健康检查接口的内部超时大1~2秒的余量。
六、总结
三种探针的核心原则可以浓缩成这几句话:
startupProbe用来给慢启动应用充足的时间,一旦通过就退出历史舞台,把接力棒交给另外两个探针。
livenessProbe只问"你活着吗",不问"你的依赖是否健康",失败触发重启,要保守设置避免误重启。
readinessProbe问"你准备好接受流量了吗",可以检查外部依赖,失败只摘除流量不重启,要积极设置确保流量路由准确。
三个探针分工明确,各司其职,配合Spring Boot Actuator的分级健康端点,才能真正实现零停机发布和故障自愈。
