
凌晨三点线上服务的Pod集体进入了“假死”状态容器进程还在端口也能连通但所有请求要么超时、要么卡到让人崩溃。当时第一反应是看资源、看网络折腾半天才发现问题出在服务本身的健康检查设计上。那次之后我把k8s探针当做交付到Kubernetes上每个服务的基础红线而不是一个“配了就行”的YAML字段。这篇文章不聊大而全的Kubernetes理论而是从一个实际运维和开发者的角度把k8s探针这件事拆开讲它解决什么问题、三类探针怎么分工、参数怎么算、配置怎么写、踩坑怎么排。无论你是刚开始接触Kubernetes的初级开发者还是已经在维护大规模集群的资深工程师这中间都有可以直接照着抄的东西。1. 为什么要配置k8s探针——先讲清它解决什么问题1.1 没有探针时会发生什么Kubernetes调度一个Pod到节点上之后kubelet负责把容器拉起来。但容器进程“跑起来了”和“服务能正常对外提供能力”完全是两回事。进程起来了可能还没完成初始化进程没退出可能已经死锁、卡死、内存打满端口监听了可能因为依赖的数据库连接池耗尽所有请求进来就直接排队超时。没有探针的情况下kubelet判断Pod是否健康的依据只有一条容器主进程有没有退出。主进程在Pod就是Running主进程挂了kubelet才会根据restartPolicy去重启。也就是说你的服务哪怕内部已经烂成一锅粥只要主进程不退出在Kubernetes眼里它依然是个好Pod。流量照常打进来用户拿到一堆5xx而集群毫无反应。这还只是第一阶段的问题。更严重的是没有探针意味着滚动发布基本靠赌。新版本Pod启动失败但进程没挂Service的Endpoints照样把它纳入转发列表流量被导到一批坏Pod上线上故障时长直接拉满。我当时处理的假死事故本质就是这个原因。1.2 探针的三个角色存活、就绪、启动Kubernetes之所以比传统部署方式更适合跑大规模服务核心在于它把“健康”这件事拆成了三个维度分别对应三类探针livenessProbe存活探针回答“容器是不是活着”。如果探测失败kubelet会按restartPolicy杀掉容器并重新创建。它保护的是进程级别的假死、死锁、内存泄漏后无法恢复等场景。readinessProbe就绪探针回答“容器能不能接收流量”。如果探测失败kubelet不会杀容器而是把这个Pod从Service的Endpoints列表里摘掉。它保护的是服务短暂不可用但不想被重启的场景比如连接池满、依赖服务抖动。startupProbe启动探针回答“容器是否完成了启动”。它专门用来保护启动慢的容器在启动探针成功之前存活探针和就绪探针都处于禁用状态。它解决的是老问题容器启动要30秒而存活探针在20秒时就把容器杀了于是无限循环重启。这三者不是替代关系而是协作关系。你可以把容器想象成一个新员工startupProbe负责“完成了入职培训”readinessProbe负责“现在可以接客户电话了”livenessProbe负责“别摸鱼装死干不动就换人”。1.3 探针类型怎么选HTTP、TCP、Exec还是gRPC确定了用哪几类探针之后还得选探测方式。Kubernetes支持四种基础探测方式各有各的适用场景exec探针在容器内执行一条命令根据退出码判断是否成功0为成功非0为失败。适合没有HTTP端点、无法暴露端口的工具型容器、初始化容器也适合检查一些进程内部状态。HTTP Get探针向容器的某个HTTP路径发起GET请求响应状态码在200到399之间算成功。这是最常用的一种适合所有能暴露HTTP接口的业务服务。TCP Socket探针尝试与容器指定端口建立TCP连接能连上就算成功。适合数据库、缓存、消息队列这类基于TCP协议的服务只关心端口通不通。gRPC探针较新版本Kubernetes支持的方式需要服务实现gRPC健康检查协议适合微服务之间走gRPC通信的场景。做一个简单的选择表探针方式成功判定标准适用场景注意点exec命令退出码为0无HTTP端口的工具类容器、进程状态检查命令本身不能太耗时不能依赖外部命令HTTP GetHTTP状态码 200-399Web服务、API服务、有健康检查接口的服务接口不能有鉴权不能有耗时逻辑TCP ConnectTCP连接成功建立Redis、MySQL、MQ等TCP服务只能证明端口通无法证明业务健康gRPC健康检查协议返回SERVINGgRPC微服务需要实现标准健康服务存在版本要求我的经验是业务服务优先选HTTP Get配合一个专门设计的健康检查接口中间件选TCP就够用如果容器里实在没有可探测的端点再用exec。方式没有绝对优劣关键是你清楚每种方式到底在探测什么别用一种“看似在检查健康、实则什么都没查”的姿态糊弄过去。2. 三类探针的分工协作与参数设计2.1 三类探针的原理与典型应用三类探针在Pod生命周期里的作用完全不同配置上也有明显差异。livenessProbe最典型的场景是检测“死锁”。Java服务在某种极端情况下线程池全部阻塞、GC频繁到接近FGC但进程还活着端口还开着。如果没有存活探针这个节点会一直卡着请求全部堆积。配上liveness之后连续几次探测失败kubelet直接把容器杀掉重新拉起一个虽然请求会断一下但至少服务能恢复到可用状态。readinessProbe最典型的场景是依赖服务抖动。比如你的服务A依赖RedisRedis短暂不可用服务A的连接池被打满新进来的请求无法获取连接。这时候如果直接杀掉服务A等于把Redis的故障扩散到自身而且重启过程中一堆Pod同时被拉起来很可能加剧雪崩。正确的做法是用readiness探针感知到“当前无法接收流量”先摘掉流量再等依赖恢复。等Redis恢复readiness探针重新探测成功Pod自动回到Endpoints里流量自动恢复。startupProbe典型的场景是启动慢的应用比如Java应用启动时要加载大量类、初始化数据库连接池、预热本地缓存在低配机器上可能40秒才对外提供服务。如果只配了liveness探针在启动阶段就会连续失败然后被重启陷入“永远启动不完”的死循环。startupProbe一旦配置在它成功之前其他探针全部暂停等应用真正初始化完成后再开始正常检查。2.2 参数怎么定initialDelaySeconds到failureThresholdKubernetes的探针参数不多但每个参数单独拿出来都影响巨大initialDelaySeconds容器启动后多久开始第一次探测。这段时间相当于给应用预留的启动缓冲期但它在startupProbe存在时主要影响第一次探测的时机。periodSeconds每隔多少秒探测一次。默认10秒最小1秒。这个值决定故障发现速度和探针开销的平衡。timeoutSeconds单次探测的超时时间。默认1秒如果你的健康检查接口本身需要2秒才能返回必须调大否则每次都会被判定失败。successThreshold连续成功多少次才认为状态健康。默认1readiness可以配大于1用于避免服务抖动造成的频繁摘挂。failureThreshold连续失败多少次才真正判定为不健康。默认3这个值和periodSeconds一起决定了故障发现的总耗时。还有一个容易被忽略的参数terminationGracePeriodSeconds。它定义的是Pod被终止时的宽限期默认30秒。当liveness探针判定失败触发重启时kubelet先发送SIGTERM给容器主进程如果超过terminationGracePeriodSeconds还没退出就会强制SIGKILL。所以如果你的应用优雅下线本身需要60秒这个参数就得调。2.3 一套可复制的参数设计思路参数设计很难给出一个“万能配方”但要有一个可以计算的方法。核心公式很简单故障发现时间 initialDelaySeconds periodSeconds × (failureThreshold - 1) timeoutSeconds。考虑一个具体场景某服务启动比较慢需要40秒完成初始化HTTP健康检查接口本身响应在300毫秒以内日常运行中我们希望在30秒内感知到服务不可用。startupProbeinitialDelaySeconds 5periodSeconds 5failureThreshold 10timeoutSeconds 3。在这里启动探针的总缓冲时间大约是5 5×9 3 53秒大于40秒的启动时间所以应用一定能顺利完成启动。readinessProbeinitialDelaySeconds 10periodSeconds 10failureThreshold 3timeoutSeconds 3。故障发现时间约10 10×2 3 33秒符合预期。livenessProbeinitialDelaySeconds 20periodSeconds 15failureThreshold 3timeoutSeconds 3。注意这里initialDelay必须超过startupProbe的预估成功时间避免startup还没完成就被liveness误杀。参数设计的关键是“先启动、后就绪、再存活”的时间轴逻辑。启动探针覆盖的时间范围必须大于应用真实启动时间就绪探针在启动完成后负责流量接入存活探针则要尽量晚一些介入给稳定运行期兜底。3. 完整实现以Java服务为例的探针配置实战3.1 从Docker层到Pod层的检查路径很多人以为配置探针就是往Dockerfile里加HEALTHCHECK这个理解在Kubernetes里是片面的。Dockerfile的HEALTHCHECK由节点上的容器运行时执行结果Docker能看到但Kubernetes的调度、Service负载均衡、滚动发布完全感知不到。Kubernetes的探针由kubelet执行它位于节点上定期向目标Pod的IP发起探测请求探测结果直接反映在Pod状态和Endpoints中。举个例子你的服务镜像里配置了EXPOSE 8080容器监听0.0.0.0:8080。HTTP探针配置为httpGet路径/actuator/health/live端口8080。kubelet会构造一个HTTP GET请求目标地址是Pod IP 8080 /actuator/health/live而不是通过Service也不会经过ingress。这意味着你的健康检查接口必须在容器内部直接能够访问不能被登录鉴权、IP白名单拦截监听地址也不能是127.0.0.1。我之前就踩过一个坑服务在开发环境监听localhost本地调试一切正常部署到Kubernetes之后探针一直失败。原因很简单kubelet请求的是Pod IP而服务只监听了127.0.0.1连接被拒绝。检查半天业务代码最后发现是监听地址的问题。所以容器里服务的监听地址一定要是0.0.0.0。3.2 一个完整Pod YAML拆解来看一个实际例子。某个Java微服务启动耗时约15到20秒端口8080依赖MySQL和Redis日常接口响应较快。我给它配的探针如下apiVersion: v1 kind: Pod metadata: name: order-service-pod namespace: production labels: app: order-service spec: containers: - name: order-service image: registry.internal/order-service:v2.3.1 ports: - containerPort: 8080 name: http startupProbe: httpGet: path: /actuator/health/live port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 10 readinessProbe: httpGet: path: /actuator/health/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health/live port: 8080 initialDelaySeconds: 30 periodSeconds: 15 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3 terminationGracePeriodSeconds: 60这个配置里三个探针分别指向不同的接口路径。startupProbe用/actuator/health/live因为启动阶段只关心进程和Spring容器是否初始化完毕不关心外部依赖。readinessProbe用/actuator/health/ready这个接口内部会检测MySQL和Redis连接池是否可用只有依赖正常才返回200。livenessProbe也用/actuator/health/live它只检查进程内部的核心状态避免因为依赖服务抖动导致容器被杀。注意到liveness的initialDelaySeconds是30秒比startupProbe完整缓冲时间5 5×9 3 53秒要短但这没关系因为在startupProbe成功之前liveness探针根本不会启动。等startup成功之后liveness再按自己的周期工作。我之所以把liveness的initialDelay设成30是为了在startup成功之后继续留一段缓冲避免服务刚就绪还没稳定就被存活检查误杀。3.3 探针接口怎么写才能不坑自己探针接口是整个方案的灵魂。写健康检查接口的时候我强烈建议把“存活”和“就绪”拆成两个端点不要混在一起。存活接口只检查进程内部状态比如Spring容器是否加载完成、核心线程池是否活着、本地缓存是否初始化不需要连接数据库也不需要调用外部API。这个接口要做到极快返回不能有IO等待。就绪接口则负责反映“当前是否具备流量接入能力”可以检查数据库连接池、Redis连接、消息队列连接等核心依赖。但这里有一个很重要的限定就绪接口检查的是“依赖的可用性”而不是“完整调用依赖”。比如检查数据库用SELECT 1比用一条复杂业务SQL更合适因为健康检查的目的是判断可用性不是验证业务逻辑。我见过有人把健康检查接口写得比业务接口还重内部遍历几百张表、调用多个下游服务结果探针本身成了性能瓶颈。这完全走偏了。健康检查接口的理想响应时间应该控制在100毫秒以内否则你调大timeoutSeconds也掩盖不了系统层面的问题。4. 运维视角探针常见故障与排查实录4.1 现象一Pod反复重启这是探针配置错误最典型的症状。一个Pod在Running和CrashLoopBackOff之间反复横跳kubectl get pods能看到RESTARTS列不断增长。第一反应看Pod事件kubectl describe pod pod-name拉到Events字段如果看到类似“Liveness probe failed”的记录基本就是liveness探针出了问题。继续往下看失败原因响应码是多少、超时还是连接拒绝。如果健康检查接口返回500那要分两种情况一是接口本身依赖了外部服务二是应用确实内部异常。如果返回503通常就是服务还没有真正就绪但liveness已经开始检查了。如果你是启动慢导致被杀优先检查startupProbe是否存在、参数是否覆盖了启动时间如果只是周期性的偶发失败检查periodSeconds和failureThreshold是不是太激进。4.2 现象二Pod Running但流量进不来Pod状态显示Running从Deployment看副本数也正常但Service访问不通。这种情况常见于readinessProbe失败——Pod还在跑但被从Endpoints里摘掉了。你可以用kubectl get endpoints service-name看后端的IP列表如果列表是空的说明readiness探针全部失败。再看kubectl describe pod会有Readiness probe failed的事件。这类问题最让我头疼的地方在于应用日志通常看不出明显异常因为readiness探针失败不会杀掉容器服务自己可能完全没感知。排查思路是手动请求readiness接口看看返回什么。比如就绪接口检查Redis连接池Redis连接池在某个时间点被打满接口直接超时但业务进程本身还挺正常。这时候要看的是依赖服务而不是应用本身。4.3 现象三启动慢被误杀另一种很隐蔽的误杀场景应用本身启动要40秒但startupProbe没配或参数没覆盖结果kubelet在启动阶段连续检查失败直接杀掉容器。最可怕的不是一次杀掉而是新容器又被杀掉形成无限重启。判断方法很直接看Pod的启动时间和RESTARTS规律。每次RESTARTS的增长间隔和periodSeconds×failureThreshold基本一致说明容器每次都活不过探针的检查窗口。看一眼Events里的“Startup probe failed”或者“Liveness probe failed”事件就能定位。解决方式我已经在前面说过了增加startupProbe给它充足的缓冲时间。startupProbe本身的设计哲学就是“宁愿它多活一会也不能在启动阶段误杀”。启动慢不是生产环境不支持而是一个需要被探测策略覆盖的事实。4.4 排查手段看Events、看kubelet日志探针排查最重要的是看对地方。kubectl describe pod是最快的上手方式但它的Events信息会被清理Pod反复重建几次之后历史事件可能已经不在了。这时候需要直接看kubelet日志。在节点上执行journalctl -u kubelet -f加上过滤关键字比如探针失败的URL和错误码。kubelet日志里会记录每一次探针探测的结果包括成功和失败。对大规模问题定位来说kubelet日志是比Pod Events更可靠的信息源。还有一个实用的命令组合# 查看Pod的详细状态和事件 kubectl describe pod pod-name -n namespace # 持续观察Pod状态变化 kubectl get pods -w # 在节点上查看kubelet日志中的探针记录 journalctl -u kubelet | grep pod-name排查者的认知顺序应该是先看Pod状态再看Events再手动请求健康检查接口最后才是查业务日志。很多人一上来就翻业务日志结果业务日志里只有正常请求问题出在探针路径、监听地址、依赖服务上业务层面根本看不到。5. 探针设计的几个坑千万不要踩5.1 不要在探针里做耗时活探针是kubelet周期调用的它本质上是集群对你服务的一种额外负载。健康检查接口如果内部调用下游服务、查询大表、执行复杂聚合那么每10秒就有一批这样的请求打进来。探针本身成了热点请求下游服务被高频请求打得直抖服务越抖探针越失败雪崩就是这么来的。经验是探针接口保持“浅层检查”。存活接口检查进程内部状态就绪接口最多做一次轻量的依赖连通性检查比如SELECT 1、Redis ping。探针接口绝对不应该调用业务链路中的关键接口。5.2 不要用探针做依赖健康检查这是个非常有争议但必须在团队里定调的规则。拿服务A依赖MySQL举例如果把“MySQL是否可用”塞进服务A的liveness探针MySQL抖动一下服务A直接被判死并重启重启过程中流量全挂在服务A上服务A的Pod被批量重建对数据库的连接风暴反而更猛。正确做法是把依赖检查放在readinessProbe里。依赖挂了服务先摘流量、保住进程等待依赖恢复依赖恢复之后readiness探针重新通过服务自动接流。如果依赖故障持续很久Liveness探针仍然只检查服务自身状态不让依赖故障扩大为容器重启。5.3 探针接口被网关截住导致误判有些服务前面挂了网关或权限组件开发的时候把健康检查接口也一起包了进去。结果kubelet请求这个接口被权限中间件拦截返回401探针判定失败容器无限重启。这个坑在工作里出现过不止一次。解决办法很简单给健康检查接口单独开白名单让它绕过鉴权、绕过限流、绕过一切业务中间件。健康检查接口是节点对容器的“内部通话”不应该被当作用户请求来对待。5.4 探针接口权限与网络监听的边界刚才说过监听地址的问题。服务如果监听在127.0.0.1只能本机访问kubelet从节点侧访问Pod IP时会失败。对于多容器Pod如果只有主容器负责探针响应sidecar不在其中也要确认主容器的健康检查接口是否真正暴露在容器网络的端口上。另外TCP探针有一些隐蔽问题端口通了不代表业务就绪。比如一个消息队列消费者端口正常监听但消费线程全部阻塞。TCP探针只能确认“三次握手成功”业务状态全凭想象。所以TCP探针只适合“只要端口在就是好”的服务一旦要求更高必须升到HTTP或gRPC探针。6. 进阶技巧与个人经验6.1 滚动发布时的探针联动探针不只是Pod稳定运行期的守护者它和Deployment的滚动策略是绑定的。滚动发布时新Pod的readinessProbe成功才允许继续增加新副本、减少旧副本。如果新版本的健康检查接口设计不良比如返回200但服务实际不可用滚动发布就会把流量切给一堆坏Pod造成生产事故。我的习惯是新版本上线前先手动跑一次健康检查接口用curl模拟kubelet的探测路径检查返回码、耗时和依赖状态。这一步成本极低但能挡住一半以上的发布故障。6.2 时间参数与资源消耗的关系探针看上去是轻量级的但在大规模集群里也要算账。假设你有1000个Pod每个Pod配3个探针periodSeconds按10秒算每分钟的探测请求数就是1000×3×6 18000次。如果健康检查接口响应都很慢这18000次请求会显著消耗容器CPU。为了测试探针一定不要让periodSeconds小于5秒除非你对这个服务有非常明确的高频检测需求。kubelet的探测频率还受节点上Pod数量的影响探针本身不消耗额外集群资源但每次探测都会在节点上发起一次连接。节点上Pod密度很高时过高的探测频率会放大节点CPU和网络的负载。合理设置failureThreshold、适当拉长periodSeconds反而是更“高可用”的做法。6.3 一些测试探针的实用小工具调试探针时我喜欢在容器里保留一个最小的调试入口。进入容器执行命令模拟探针# 模拟HTTP探针 curl -v http://127.0.0.1:8080/actuator/health/live # 模拟TCP探针 timeout 3 bash -c echo /dev/tcp/127.0.0.1/8080 echo tcp ok另外直接在Pod里配置一个临时的restartPolicy: Never的调试Pod把同样的探针配置放上去观察它的启动和就绪过程比修改线上服务的配置安全得多。6.4 个人经验探针配置要按服务画像去设计最后聊一点比较主观的经验。我给服务配探针前会先梳理这个服务的“画像”启动要多久、启动时依赖什么、运行时有什么外部依赖、能够接受的最长故障感知时间是多少、进程被杀重建的代价有多大。如果一个服务是无状态API依赖少、启动快探针可以配得激进一点故障发现控制在20秒内。如果一个服务是有状态任务工作者启动要加载大量本地数据重建代价高探针就配得保守重点是startup保护宁可故障发现慢一点也不要频繁重启。探针参数没有标准答案但它必须反映你对自己服务的理解。理解和参数配置的差距才是线上故障的真正来源。之前那个凌晨三点的假死事故后来我用startupProbe加了启动保护用readinessProbe把依赖检查从liveness里拆了出来再用liveness盯进程核心状态。从那以后同类故障再没有出现过。折腾探针的那几天让我对Kubernetes的“健康”有了更深刻的理解它不是一个静态状态而是集群、平台、应用三者之间持续的协商过程。探针就是这场协商的语言。早点把它的语法掌握好生产环境会平静很多。