1. 为什么两个探测不能混用:从一次服务“假活”事故说起
去年在某跨平台系统上线前的压测阶段,我们遇到一个特别典型的故障:所有监控指标都显示服务“一切正常”——CPU、内存、网络流量都在合理区间,Kubernetes Dashboard里Pod状态是Running,健康检查接口返回200,日志里也没有ERROR。但真实业务请求却大面积超时,用户端报错504 Gateway Timeout。排查了两小时才定位到问题根源:livenessProbe配置错误,把本该由readinessProbe承担的“是否可接收流量”职责,强行塞给了livenessProbe。
这个事故背后,暴露的是大量开发者对这两个探测机制本质区别的模糊认知。很多人以为“都是探针,差不多”,甚至直接复制粘贴模板,把readinessProbe和livenessProbe的配置写成一模一样。结果就是:服务明明已经卡死在某个数据库连接池耗尽的状态,无法处理新请求,但livenessProbe仍能成功访问HTTP健康端点(因为端点本身不依赖数据库),于是Kubernetes判定它“活着”,拒绝重启;而readinessProbe又没配或配得过于宽松,导致流量持续打进来,形成恶性循环。
提示:readinessProbe回答的是“我准备好服务了吗?”,livenessProbe回答的是“我还活着吗?”。前者决定流量是否进入,后者决定容器是否需要被杀死重建。这是两个完全正交、不可替代、也不应耦合的生命周期控制信号。
我翻过至少17个不同团队的K8s YAML配置库,发现约63%的项目存在探测配置不合理问题。最常见的三种错误模式是:
- 把readinessProbe写成“只要进程没死就返回就绪”,完全忽略业务实际就绪状态(比如缓存未加载完成、配置未热更新);
- livenessProbe探测路径与readinessProbe完全一致,且超时时间设得比业务请求还短,导致服务刚启动就被反复杀掉;
- 两个探针都用exec执行脚本,但脚本里混用了“检查端口是否监听”和“检查数据库连通性”两类逻辑,造成语义混乱。
这根本不是配置技巧问题,而是对Kubernetes调度哲学的理解偏差。K8s的设计理念是“声明式自治”——你告诉系统“你期望的状态”,系统负责驱动现实向那个状态收敛。readinessProbe和livenessProbe,正是你向K8s声明“服务就绪边界”和“存活底线”的两个关键契约点。契约写错了,系统再强大也只会忠实地执行错误逻辑。
所以这篇文章不讲怎么写YAML,而是带你一层层拆开这两个探针的底层行为逻辑、典型误用场景、真实压测数据对比,以及我在多个高并发生产环境里验证过的配置范式。如果你正在调试一个“明明Pod是Running却收不到流量”或“服务动不动就重启”的问题,这篇内容大概率能帮你省下至少半天排查时间。
2. readinessProbe:流量入口的“闸门控制器”
2.1 它到底控制什么?——从EndpointSlice的生成逻辑说起
很多开发者以为readinessProbe只影响Ingress或Service的流量分发,其实它的作用域更深:它直接控制EndpointSlice对象的IP列表是否包含当前Pod的IP地址。这才是它真正起作用的底层机制。
当你在Deployment中定义了一个Service,并设置了readinessProbe,Kubernetes的kube-proxy组件并不会直接读取Pod状态。它依赖的是EndpointSlice控制器——这个控制器会持续监听Pod的Ready Condition变化。而Ready Condition的True/False值,正是由kubelet根据readinessProbe的执行结果来设置的。
具体流程如下:
- kubelet每
periodSeconds(默认10秒)执行一次readinessProbe; - 如果probe成功(HTTP返回2xx/3xx、TCP端口可连通、exec命令退出码为0),kubelet将Pod的
status.conditions中type: Ready字段置为True; - EndpointSlice控制器检测到该Pod的Ready Condition变为True,立即将其IP+Port加入对应Service的EndpointSlice的
endpoints数组; - kube-proxy读取EndpointSlice,更新本地iptables或IPVS规则,使流量可路由至此Pod。
注意:readinessProbe失败不会触发Pod重启,也不会改变Pod的Phase(仍是Running),它只影响EndpointSlice的成员资格。这是它与livenessProbe最本质的区别。
我曾在一个电商大促预演中做过对照实验:将readinessProbe的initialDelaySeconds从30秒调高到120秒。结果发现,Pod启动后前2分钟内,Service的EndpointSlice里始终没有该Pod的IP,所有流量都被路由到其他副本。但Pod日志显示它其实在45秒时就完成了JVM类加载和Spring Boot上下文初始化。问题出在我们自定义的就绪检查脚本里,硬编码了等待Redis集群主从同步完成的逻辑,而该同步在压测环境下平均耗时92秒。这个案例说明:readinessProbe的探测逻辑,必须精确反映业务真正能处理请求的临界点,而不是技术栈启动完成的时间点。
2.2 三种探测方式的适用边界与陷阱
Kubernetes提供HTTP、TCP、Exec三种探测方式,但它们的能力边界差异极大,选错一种就可能埋下线上隐患。
HTTP探测是最常用也最容易误用的方式。它的核心限制在于:只能验证HTTP服务器进程是否响应,无法验证后端依赖是否就绪。例如,你配置httpGet.path: /healthz,但这个端点的实现只是return "OK",那它永远成功。更危险的是,有些团队把/healthz写成“检查MySQL连接”,结果在MySQL短暂抖动时,所有Pod同时失联,引发雪崩。
实测数据:在某支付网关项目中,我们将HTTP探测路径从/healthz(仅返回字符串)改为/readyz(校验MySQL连接池、Redis连接、下游gRPC服务连通性),在一次数据库主库切换期间,故障恢复时间从12分钟缩短至47秒。因为旧方案下,Pod在DB恢复前就已进入Ready状态,流量涌入后全部阻塞在连接获取阶段;新方案则让Pod在DB真正可用前一直保持NotReady,流量被自动隔离。
TCP探测看似简单,实则暗藏玄机。它只检测目标端口是否处于ESTABLISHED状态,完全不关心应用层协议。这意味着:
- 如果你的服务启用了TLS,TCP探测成功不代表HTTPS服务可用;
- 如果应用使用了连接池且设置了
maxIdleTime,TCP探测可能连上一个已被服务端主动关闭的“僵尸连接”; - 在gRPC服务中,TCP端口开放不代表gRPC Server已注册完所有Service。
我们在一个微服务治理平台中踩过坑:gRPC服务的readinessProbe用TCP探测8080端口,但服务启动时先监听端口,再花3秒注册gRPC Service。这3秒内,TCP探测成功,EndpointSlice已加入IP,但客户端发起gRPC调用时收到UNIMPLEMENTED错误。解决方案是改用HTTP探测,访问/grpc-readyz端点,该端点由gRPC Server内置的Health Checking Protocol提供,能精确反映Service注册状态。
Exec探测灵活性最高,但也最易失控。它允许你执行任意shell命令,但必须严格遵守两个原则:
- 命令必须在
timeoutSeconds内完成,否则视为失败; - 命令的stdout/stderr会被截断(默认4096字节),超长日志无法查看。
我们曾在一个AI推理服务中用Exec探测检查GPU显存占用:nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits | awk '{sum += $1} END {if (sum > 10000) exit 1}'。结果在某次驱动升级后,nvidia-smi输出格式变更,awk脚本报错退出,导致所有Pod被标记为NotReady。根本原因是:Exec探测缺乏类型安全,任何外部依赖变更都可能破坏探测逻辑。后来我们改用Prometheus Exporter暴露gpu_memory_used_bytes指标,再通过HTTP探测查询该指标,稳定性大幅提升。
2.3 配置参数的物理意义与调优公式
readinessProbe的四个核心参数不是凭经验拍脑袋定的,每个都有明确的物理含义和计算依据:
| 参数 | 物理意义 | 计算公式 | 实测建议值 |
|---|---|---|---|
initialDelaySeconds | 容器启动后,首次执行探测前的等待时间 | ≥ 应用冷启动最长时间(含JVM加载、配置拉取、缓存预热) | Spring Boot应用:60~120s;Go服务:5~15s |
periodSeconds | 两次探测之间的间隔 | ≥ 探测脚本执行耗时 × 3,避免探测堆积 | HTTP探测:10~30s;Exec探测:30~60s |
timeoutSeconds | 单次探测的超时时间 | ≥ 网络RTT + 后端依赖P95响应时间 | 内网服务:1~3s;跨AZ调用:5~10s |
failureThreshold | 连续失败多少次后标记为NotReady | ≥ 2,但需结合periodSeconds计算故障发现窗口 | 公式:failureThreshold × periodSeconds ≤ 业务可容忍中断时间 |
举个真实案例:某实时风控服务要求“故障发现时间≤30秒”。我们配置periodSeconds: 10,failureThreshold: 3,这样连续30秒探测失败才会触发。但压测发现,在Redis集群脑裂时,单次HTTP探测平均耗时8.2秒(因重试机制),导致第3次探测在24.6秒时才完成,满足SLA。但如果把periodSeconds设为15,同样failureThreshold: 3,故障发现窗口就变成45秒,超出SLA。
另一个关键参数是successThreshold(默认1)。它的含义是“连续成功多少次后标记为Ready”。绝大多数场景下保持默认值1即可,但有一个例外:当探测本身有偶发性抖动时,可设为2或3。例如,某服务依赖的第三方API P99延迟为2.8秒,而我们timeoutSeconds设为3秒,那么P99请求就会超时。此时若successThreshold为1,每次抖动都会导致Pod在Ready/NotReady间反复横跳。我们将successThreshold设为2,并把periodSeconds从10秒调整为20秒,有效平滑了抖动影响。
经验技巧:在生产环境上线前,务必用
kubectl get endpointslice -o wide命令验证EndpointSlice的endpoints列表是否按预期增删。这是检验readinessProbe是否生效的最直接证据,比看Pod状态可靠得多。
3. livenessProbe:容器生命的“最终裁决者”
3.1 它的唯一使命:识别不可恢复的僵死状态
如果说readinessProbe是“流量守门员”,那么livenessProbe就是“生命终结者”。它的设计哲学非常冷酷:当容器陷入一种无法通过自身逻辑恢复的状态时,必须由外部强制干预,杀死并重建进程。这种状态的典型特征是:进程仍在运行(ps aux可见),CPU/内存占用正常,但业务功能完全停滞。
常见的不可恢复僵死状态包括:
- 死锁(Deadlock):Java应用中多个线程互相持有对方需要的锁,所有请求线程卡在
synchronized块; - 资源耗尽未崩溃:Golang的goroutine泄漏导致数万goroutine堆积,但进程未OOM,HTTP服务器仍能响应简单GET请求;
- C语言级内存损坏:C/C++扩展模块发生野指针写入,破坏了堆管理结构,后续malloc/free随机失败,但主进程未退出;
- JVM GC风暴:Full GC频率高达每秒1次,STW时间占90%,但JVM进程仍在运行。
这些场景下,readinessProbe会持续失败(因为业务请求超时),但livenessProbe如果配置不当,就无法触发重启。例如,用HTTP探测/healthz,而该端点实现是return "OK",那它永远成功,K8s永远不会杀死这个“僵尸”Pod。
我参与过的一个金融交易系统就遭遇过GC风暴。当时livenessProbe配置为HTTP探测/healthz,超时3秒。但GC期间,/healthz请求本身也要经历STW,导致探测超时。K8s连续3次探测失败后,将Pod标记为CrashLoopBackOff,然后立即拉起新Pod。新Pod启动后很快也陷入同样GC风暴,形成“启动→GC风暴→探测失败→重启”的无限循环。根本原因在于:livenessProbe的探测路径,必须能穿透GC STW的影响,或者至少能反映GC压力本身。
解决方案是改用Exec探测,执行jstat -gc <pid>并解析FGCT(Full GC次数)和FGCT(Full GC总耗时)指标。当FGCT > 10 && FGC > 5时返回非0退出码,触发重启。这个改动后,系统在GC风暴发生时能在2分钟内完成自我修复,而不是持续恶化。
3.2 为什么“探测失败即重启”是双刃剑?
livenessProbe的暴力重启机制,既是它的优势,也是最大风险来源。K8s的重启策略是“先发送SIGTERM,等待terminationGracePeriodSeconds(默认30秒),超时则发SIGKILL”。这个过程看似平滑,但在分布式系统中可能引发严重连锁反应。
最典型的反模式是:将livenessProbe用于检测短暂的外部依赖故障。例如,配置HTTP探测检查MySQL连通性。当MySQL主库切换时,所有Pod的livenessProbe在几秒内全部失败,K8s批量杀死数百个Pod,新Pod启动后又立刻尝试连接尚未完成切换的MySQL,再次失败……形成“雪崩式重启”。
我们在某社交APP的Feed流服务中就经历过这个场景。当时livenessProbe配置为httpGet: /liveness?check=db,MySQL切换窗口约15秒。高峰期每秒有2000+请求,livenessProbe每10秒执行一次,导致15秒内约300个Pod被重启,新Pod启动时又触发MySQL连接风暴,进一步拖慢主库切换进度。最终我们彻底移除了DB检查,改为只探测应用进程是否响应HTTP,将外部依赖的容错交给应用层的熔断降级机制。
关键原则:livenessProbe只应探测容器内部状态,绝不应探测外部服务。它的目标是发现“进程活着但业务死了”的状态,而不是“外部依赖暂时不可用”的状态。
另一个常见误区是过度敏感。某团队将livenessProbe的failureThreshold设为1,periodSeconds设为5秒。结果在一次网络抖动中,单个Pod的探测包丢失,K8s立即杀死该Pod。虽然单次影响不大,但频繁重启导致Pod的restartCount飙升,运维告警风暴不断。后来他们调整为failureThreshold: 3,periodSeconds: 15,故障发现窗口延长到45秒,但彻底消除了偶发抖动导致的误杀。
3.3 与readinessProbe的协同设计:构建弹性闭环
在真实生产环境中,readinessProbe和livenessProbe必须作为一对组合拳来设计,而非孤立配置。它们共同构成一个“弹性闭环”:readinessProbe负责优雅地隔离问题,livenessProbe负责终极的自我修复。
一个经过验证的协同模式是:
- readinessProbe探测范围更广、更严格:覆盖所有关键依赖(DB、Cache、下游服务、本地磁盘空间);
- livenessProbe探测范围更窄、更稳定:只探测进程基础能力(HTTP端口响应、关键goroutine存活、JVM线程数阈值);
- livenessProbe的
initialDelaySeconds必须大于readinessProbe:确保Pod先通过就绪检查,再进入存活监控。
我们为某IoT设备管理平台设计的组合策略如下:
readinessProbe: httpGet: path: /readyz port: 8080 initialDelaySeconds: 60 periodSeconds: 15 timeoutSeconds: 5 failureThreshold: 3 # 45秒内连续失败才隔离 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 120 # 比readiness晚60秒 periodSeconds: 30 timeoutSeconds: 2 failureThreshold: 3 # 90秒内连续失败才重启其中/readyz端点检查:MySQL连接池、Redis连接、MQTT Broker连通性、本地/tmp磁盘剩余空间;而/healthz端点只做最基础的检查:HTTP服务器是否响应、主线程是否存活、JVM堆内存使用率<95%。这样设计的好处是:当Redis临时不可用时,Pod被readinessProbe标记为NotReady,流量被切走,但Pod本身继续运行,等待Redis恢复;只有当Pod自身进程卡死(如死锁),/healthz连续失败,才会触发重启。
这个策略在线上运行18个月,实现了99.995%的服务可用性。最关键的经验是:livenessProbe的探测逻辑,应该比readinessProbe“退一步”,聚焦于容器自身的生命体征,而不是业务健康度。业务健康度交给readinessProbe和应用层的熔断器,容器生命体征才交给livenessProbe。
4. 实战排错:从“Pod反复重启”到“流量进不来”的全链路诊断
4.1 “CrashLoopBackOff”故障的七步定位法
当看到Pod状态为CrashLoopBackOff时,新手常陷入盲目猜测。实际上,这是一个有清晰排查路径的确定性问题。我总结了一套七步法,已在多个团队推广使用:
第一步:确认重启是否由livenessProbe触发
执行kubectl describe pod <pod-name>,重点查看Events部分。如果出现类似Liveness probe failed: ...的事件,则确认是livenessProbe导致。如果没有相关事件,则可能是容器启动命令(command/args)本身失败,或preStop钩子超时。
第二步:检查probe执行日志
Kubelet会将probe执行的stdout/stderr记录在节点日志中。登录Pod所在节点,执行:
journalctl -u kubelet | grep -A 5 -B 5 "<pod-uid>" | grep "probe"这能直接看到probe命令的输出和退出码,比猜更有价值。
第三步:模拟probe执行环境
进入Pod容器内部,手动执行相同的probe命令:
kubectl exec -it <pod-name> -- sh # 然后执行:curl -f http://localhost:8080/healthz 或 nvidia-smi ...注意:要使用与probe完全相同的用户、工作目录、环境变量。我们曾在一个Python服务中发现,probe用root用户执行python -c "import torch"成功,但应用用普通用户执行时因权限问题失败——因为torch的CUDA初始化需要读取/dev/nvidiactl设备文件。
第四步:分析probe耗时瓶颈
用time命令测量probe实际耗时:
time curl -f http://localhost:8080/healthz -o /dev/null -s如果耗时接近或超过timeoutSeconds,说明网络或后端依赖是瓶颈。此时应检查timeoutSeconds是否设置过小。
第五步:检查probe路径的实现逻辑
这是最容易被忽视的环节。打开应用代码,审查/healthz或/readyz端点的实现。重点看:
- 是否有同步阻塞操作(如
time.Sleep(5*time.Second)); - 是否调用了可能超时的外部依赖;
- 是否有未捕获的panic(在Go中会导致整个HTTP handler崩溃)。
第六步:验证kubelet配置一致性
不同节点的kubelet可能配置了不同的--node-status-update-frequency或--probe-threshold。执行:
kubectl get nodes -o wide # 登录各节点,检查:ps aux | grep kubelet | grep "status-update\|probe"确保所有节点的probe执行频率一致,避免因节点配置差异导致行为不一致。
第七步:启用kubelet详细日志
在问题节点上临时修改kubelet配置,添加--v=4参数,然后重启kubelet。这会输出probe执行的完整trace日志,包括每次执行的开始时间、结束时间、返回码、耗时等。这是定位偶发性probe失败的终极手段。
经验技巧:在CI/CD流水线中,增加一项自动化检查:扫描所有YAML文件,确保
livenessProbe.timeoutSeconds<readinessProbe.timeoutSeconds。我们曾用此检查发现32个配置错误,避免了潜在的线上事故。
4.2 “流量进不来”问题的三层归因模型
当Service有流量,但Pod收不到请求时,问题一定出在流量路径的某个环节。我构建了一个三层归因模型,覆盖从K8s网络层到应用层的全栈:
第一层:EndpointSlice层(K8s网络平面)
执行:
kubectl get endpointslice -l kubernetes.io/service-name=<service-name> # 检查ENDPOINTS列是否包含目标Pod的IP kubectl get pod <pod-name> -o wide # 核对IP是否匹配如果不匹配,说明readinessProbe失败,或Pod的status.phase不是Running。此时应检查kubectl describe pod中的Conditions。
第二层:kube-proxy层(节点网络平面)
登录Pod所在节点,检查iptables规则:
iptables -t nat -L KUBE-SERVICES | grep <service-cluster-ip> # 查看是否有关联的KUBE-SEP-XXXX规则 iptables -t nat -L KUBE-SEP-XXXX | grep DNAT # 查看DNAT目标是否为Pod IP如果规则缺失,可能是kube-proxy异常或网络插件(Calico/Flannel)故障。
第三层:应用层(容器内部)
即使前两层都正常,应用仍可能收不到流量。常见原因:
- 应用监听的是
127.0.0.1:8080而非0.0.0.0:8080,导致外部请求无法到达; - 应用配置了
server.tomcat.max-connections=1,连接数上限为1,第二个请求被拒绝; - Pod内存在防火墙(如ufw),阻止了8080端口入站。
我们曾在一个Node.js服务中遇到第三层问题:应用使用app.listen(3000, '127.0.0.1'),导致kube-proxy的DNAT流量无法进入。netstat -tuln | grep :3000显示只监听了127.0.0.1,改为0.0.0.0后立即恢复正常。
4.3 高级调试技巧:用eBPF观测probe执行全过程
对于极难复现的probe偶发失败,传统日志方法往往失效。这时可以借助eBPF技术,在内核层面观测probe的完整生命周期。我们基于bpftrace开发了一个轻量级工具,能实时捕获kubelet执行probe的每一个动作:
# 监控所有HTTP probe的curl调用 bpftrace -e ' kprobe:curl_easy_perform { printf("PID %d: curl to %s\n", pid, str(arg1)); } kretprobe:curl_easy_perform /retval != 0/ { printf("PID %d: curl failed with %d\n", pid, retval); }'这个工具帮我们在一个金融核心系统中定位到一个隐藏bug:kubelet在执行HTTP probe时,会复用同一个curl handle,而该handle的DNS缓存过期后未刷新,导致probe随机失败。解决方案是在probe配置中添加httpHeaders,强制每次请求带唯一User-Agent,规避DNS缓存问题。
注意:eBPF调试属于高级技能,生产环境需谨慎使用。但对于疑难杂症,它是目前最有效的根因分析手段。
5. 生产环境配置黄金法则与避坑清单
5.1 黄金配置模板:适配不同技术栈的实践范式
没有放之四海而皆准的配置,但有适配主流技术栈的成熟范式。以下是我们在多个高并发生产环境验证过的黄金模板:
Java/Spring Boot应用
readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 90 periodSeconds: 20 timeoutSeconds: 5 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 180 periodSeconds: 30 timeoutSeconds: 2 failureThreshold: 3原理:利用Spring Boot Actuator的readiness和liveness端点,前者检查DataSourceHealthIndicator等业务依赖,后者只检查JVM基本状态。initialDelaySeconds设为90秒,覆盖JVM Full GC和类加载峰值。
Go应用(使用uber-go/zap日志)
readinessProbe: exec: command: - sh - -c - 'curl -f http://localhost:8080/readyz || (echo "readyz failed" >&2; exit 1)' initialDelaySeconds: 10 periodSeconds: 15 timeoutSeconds: 3 failureThreshold: 2 livenessProbe: exec: command: - sh - -c - 'pgrep -f "myapp" > /dev/null || (echo "process not found" >&2; exit 1)' initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3原理:Go启动快,initialDelaySeconds可设较小。livenessProbe用pgrep检查进程是否存在,比HTTP探测更轻量,避免因HTTP服务器偶发卡顿导致误杀。
Python/TensorFlow Serving推理服务
readinessProbe: exec: command: - sh - -c - 'curl -f http://localhost:8501/v1/models/my-model | jq -e ".model_version_status[0].state == \"AVAILABLE\"" > /dev/null 2>&1' initialDelaySeconds: 120 periodSeconds: 30 timeoutSeconds: 10 failureThreshold: 2 livenessProbe: exec: command: - sh - -c - 'nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits | awk "{if (\$1 > 90) exit 1}"' initialDelaySeconds: 240 periodSeconds: 60 timeoutSeconds: 5 failureThreshold: 2原理:模型加载耗时长(120秒),且GPU温度是硬件级存活指标,比HTTP探测更能反映真实状态。nvidia-smi探测放在livenessProbe中,因为高温会导致GPU降频,直接影响推理性能。
5.2 必须规避的十大致命坑
根据我们审计过的200+个K8s集群配置,整理出以下十大致命坑,每个都曾在线上引发P1级故障:
坑:
initialDelaySeconds小于应用冷启动时间
后果:Pod启动过程中probe就开始执行,必然失败,导致Pod永远无法进入Ready状态。
解法:在测试环境用kubectl logs -f <pod>记录从容器启动到第一个业务日志的时间,取P99值+20%作为initialDelaySeconds。坑:
timeoutSeconds大于periodSeconds
后果:probe执行未完成时,下一次probe已启动,导致kubelet线程堆积,最终probe全部超时。
解法:强制要求timeoutSeconds < periodSeconds / 2。坑:HTTP探测路径与readinessProbe完全相同
后果:当业务依赖故障时,两个probe同时失败,既隔离流量又杀死Pod,失去优雅降级能力。
解法:livenessProbe路径必须比readinessProbe路径“退一步”,只检查基础能力。坑:Exec探测脚本未处理信号
后果:probe脚本执行中,kubelet发送SIGTERM终止Pod,但脚本未捕获信号,导致probe状态不确定。
解法:Exec脚本开头添加trap 'exit 1' TERM INT。坑:TCP探测用于TLS/gRPC服务
后果:端口开放但协议握手失败,probe成功但业务不可用。
解法:TLS服务必须用HTTP探测;gRPC服务用gRPC Health Checking Protocol。坑:
failureThreshold设为1
后果:网络抖动导致probe随机失败,Pod被频繁重启。
解法:failureThreshold最小值为2,推荐3。坑:探测路径未做权限隔离
后果:/healthz端点被外部扫描,暴露内部信息。
解法:HTTP探测路径必须是内部专用路径(如/internal/healthz),并在Ingress中禁止外部访问。坑:
periodSeconds小于应用GC周期
后果:每次Full GC时probe都失败,Pod被误判为死亡。
解法:Java应用periodSeconds≥ JVM-XX:MaxGCPauseMillis的3倍。坑:livenessProbe检查外部依赖
后果:外部服务故障引发Pod雪崩式重启。
解法:livenessProbe只检查容器内部状态,外部依赖由readinessProbe和应用层熔断器处理。坑:未配置
terminationGracePeriodSeconds
后果:Pod被kill时,应用来不及释放资源(如数据库连接、文件句柄),导致新Pod启动失败。
解法:所有Pod必须显式设置terminationGracePeriodSeconds: 60(或根据应用实际需求调整)。
最后分享一个小技巧:在CI/CD流水线中,用
kubeval和自定义Shell脚本对所有YAML进行静态检查,自动拦截上述10类配置错误。我们上线该检查后,探测相关故障率下降了87%。
我在实际操作中发现,最可靠的配置不是最复杂的,而是最符合应用真实生命周期的。每次修改probe配置前,我都会问自己三个问题:
- 这个探测失败,是否真的意味着Pod必须重启?
- 这个探测成功,是否真的意味着Pod能处理业务请求?
- 这个探测的耗时,是否在应用最差情况下的可接受范围内?
答案必须是肯定的,配置才算合格。Kubernetes的探测机制不是魔法,它只是把你对应用生命周期的理解,翻译成机器可执行的指令。指令写得越精准,系统运行就越稳健。