Kubernetes 探针完全指南:从一次"健康的 Pod 被反复重启"看懂 liveness 与 readiness

2023 年初我们把核心订单服务迁上 Kubernetes,照教程给每个服务都配了 liveness probe——"探活嘛,容器死了 K8s 帮你重启"。大促那天流量上来,服务明明还在正常成交订单,Pod 却开始成片 CrashLoopBackOff:被反复 kill、反复重启,重启中的 Pod 不接流量,活着的扛更多压力变得更慢,它们的探针也开始超时被 kill,十分钟内整个服务雪崩。复盘才反应过来:liveness 探针 timeoutSeconds 设的是默认 1 秒,而高峰期一次 GC 停顿、一个慢查询就足以让健康检查超过 1 秒——K8s 不知道容器只是"忙",只看到探针失败,就忠实执行了我让它执行的动作:kill、重启。我亲手配的探针成了压垮服务的那只手。本文把三种探针从头梳理:liveness(失败重启容器)、readiness(失败摘流量不重启)、startup(成功前另两个探针不跑)失败后果天差地别;四种检查方式 httpGet/exec/tcpSocket/grpc 怎么选,tcpSocket 的"健康"含金量为何很低;五个时间参数怎么调,容忍窗口怎么算,timeoutSeconds 默认 1 秒为何必须调大;最关键一条原则——liveness 绝不能检查数据库等外部依赖,否则依赖一抖动所有 Pod 同时失败被集体重启、陷入永远好不了的重启风暴;readiness 怎么承接依赖检查并和 preStop 优雅下线串起来;最后覆盖慢启动用 startup probe、探针不能有副作用、滚动更新 maxUnavailable、CPU 限流导致探针超时四个工程坑。核心一句:探针不是"配了就有保障"的保险,它是你交给 K8s 的判断指令,指令下错了 K8s 会忠实地把你的服务搞垮。

2023 年初我们把核心订单服务迁上了 Kubernetes。迁完那阵子我特别有成就感:滚动更新、自动扩缩容、Pod 挂了自己拉起来,运维群里安静了不少。我照着教程给每个服务都配了 liveness probe——"探活嘛,容器死了 K8s 帮你重启,多省心"。直到那年的一次大促:流量上来后,订单服务明明还在正常处理请求,日志里订单一笔笔在成交,可监控上 Pod 却开始成片地 CrashLoopBackOff——被 K8s 反复 kill、反复重启。重启中的 Pod 不接流量,活着的 Pod 要扛更多压力、变得更慢,然后它们的探针也开始超时,又被 kill……整个服务在十分钟内雪崩。我盯着 kubectl describe 里那行 Liveness probe failed 看了很久才反应过来:我的 liveness 探针超时设的是 1 秒,而大促时一次 GC 停顿、一个慢查询,就足以让健康检查接口响应超过 1 秒。K8s 不知道这个容器只是"忙",它只看到探针失败,就老老实实地执行了我让它执行的动作:kill,重启。我亲手配的探针,成了压垮服务的那只手。那次之后我才认真搞懂:K8s 的探针不是"配了就有保障"的东西,它是一组你交给 K8s 的、关于"容器到底算不算健康"的指令——指令下错了,K8s 会忠实地把你的服务搞垮。这篇文章就把三种探针从头梳理一遍:它们各自管什么、怎么配、时间参数怎么调,以及那些配错了会反噬的坑。

问题背景

先把那次事故的现象和我的误判讲清楚,后面所有的配置建议,都是冲着纠正这个误判去的。

现象:订单服务部署在 K8s 上,每个 Pod 配了 liveness probe。大促流量高峰时,服务本身仍在正常处理请求,但 Pod 因 liveness 探针超时被 K8s 判定为"不健康",成片进入 CrashLoopBackOff 被反复重启;重启进一步减少了可用实例,加剧了剩余 Pod 的压力,最终雪崩。

我当时的错误认知:"liveness probe 就是探活,配上它,容器一出问题 K8s 就帮我重启,只有好处没有坏处。"

真相:liveness 探针失败的后果是重启容器,这是一个不可小看的破坏性动作。探针"失败"不等于"容器真的死了"——它也可能只是容器在忙、在 GC、在等一个慢依赖。如果你的探针把"忙"误判成"死",K8s 就会去重启一个本来好好的容器;而在高负载下,这种误判会成批发生,重启反而成了雪崩的助推器。探针配置的本质,是你要精确地告诉 K8s:什么情况算"真的不行了",以及"不行了"之后该做什么。

要把探针用对,需要几块认知:

  • liveness、readiness、startup 三种探针各自管什么,失败后的动作天差地别;
  • 四种检查方式(httpGet / exec / tcpSocket / grpc)分别适合什么场景;
  • 探针的几个时间参数怎么调,调紧了会误杀、调松了会迟钝;
  • liveness 探针为什么绝不能检查外部依赖;
  • readiness 探针怎么和优雅下线、滚动更新配合。

一、三种探针:liveness、readiness、startup 各管什么

K8s 一共有三种探针,很多人(包括当年的我)只配了 liveness,这是出问题的根源。这三种探针探测的方式可以完全一样,但探测失败后 K8s 做的事情完全不同——搞混它们,后果就是上面那场雪崩。

liveness probe(存活探针):回答"这个容器还活着吗?需不需要重启?"。它失败到阈值后,K8s 会 kill 掉容器并重启。它用来兜底那些"进程还在、但已经卡死、重启才能恢复"的情况,比如死锁、线程池耗尽且无法自愈。

readiness probe(就绪探针):回答"这个容器现在能接收流量吗?"。它失败后,K8s 不会重启容器,而是把这个 Pod 从 Service 的 Endpoints 列表里摘掉——流量不再转发给它,但它继续运行。等它探针恢复成功,再自动加回去。它用来处理"容器活着,但暂时不该接流量"的情况,比如还在启动、依赖暂时不可用、正在下线。

startup probe(启动探针):回答"这个容器启动完成了吗?"。在它成功之前,liveness 和 readiness 探针都不会运行。它专门给启动慢的应用(比如冷启动要几十秒的 JVM 服务)用,避免应用还在启动就被 liveness 误杀。

下面先看一段事故现场:一个 Pod 进入 CrashLoopBackOff 后,kubectl 看到的样子。学会读这两段输出,是排查探针问题的第一步:

$ kubectl get pods
NAME                       READY   STATUS             RESTARTS   AGE
order-svc-7d9f8c-abc12     0/1     CrashLoopBackOff   7          12m

$ kubectl describe pod order-svc-7d9f8c-abc12
...
Events:
  Type     Reason     Age                  From     Message
  ----     ------     ----                 ----     -------
  Warning  Unhealthy  2m (x9 over 8m)      kubelet  Liveness probe failed: Get
           "http://10.1.2.3:8080/healthz": context deadline exceeded
  Normal   Killing    2m (x3 over 8m)      kubelet  Container order-svc failed
           liveness probe, will be restarted
  Normal   Pulled     1m (x4 over 12m)     kubelet  Container image already present
  Normal   Created    1m (x4 over 12m)     kubelet  Created container order-svc
  Normal   Started    1m (x4 over 12m)     kubelet  Started container order-svc

这段 Events 把因果链写得清清楚楚:Liveness probe failed ... context deadline exceeded(探针超时),紧接着 failed liveness probe, will be restarted(于是重启)。RESTARTS 7x9 over 8m 说明这不是偶发,而是反复发生。看到这种链路,第一反应就该是:不是容器真死了,是探针判断标准出了问题。

二、四种检查方式:httpGet、exec、tcpSocket、grpc

知道了三种探针"管什么",再看它们"怎么探"。每一种探针,都可以用下面四种方式之一来做实际的检查。检查方式选得合不合适,直接影响探针准不准。

httpGet:最常用。kubelet 向容器的某个 HTTP 路径发 GET 请求,返回状态码在 200–399 之间算成功,其他算失败。适合有 HTTP 接口的 Web 服务,你需要在应用里专门写一个健康检查端点。

exec:在容器内执行一条命令,退出码为 0 算成功,非 0 算失败。适合没有 HTTP 接口的服务(比如后台 worker),或者需要检查文件、进程等容器内部状态的场景。注意 exec 每次探测都要起一个进程,开销比 httpGet 大。

tcpSocket:kubelet 尝试和容器的某个端口建立 TCP 连接,连上了算成功。它只能证明"端口在监听",不能证明"服务逻辑正常"。适合纯 TCP 服务,或作为一种最轻量的兜底检查。

grpc:K8s 1.24+ 原生支持,kubelet 调用容器实现的标准 gRPC 健康检查协议(grpc.health.v1.Health)。适合 gRPC 服务,比之前用 grpc_health_probe 二进制配 exec 要干净得多。

下面用 YAML 把这四种方式各写一遍,放在同一个容器定义里对照看(实际中一个探针只选一种):

# 方式一:httpGet —— 最常用,访问一个 HTTP 健康检查路径
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
    httpHeaders:
      - name: X-Probe-Source
        value: kubelet

# 方式二:exec —— 在容器内执行命令,退出码 0 算成功
livenessProbe:
  exec:
    command:
      - sh
      - -c
      - "test -f /tmp/healthy"   # 文件在就算健康

# 方式三:tcpSocket —— 能建立 TCP 连接就算成功(只证明端口在听)
readinessProbe:
  tcpSocket:
    port: 8080

# 方式四:grpc —— K8s 1.24+ 原生支持标准 gRPC 健康检查协议
livenessProbe:
  grpc:
    port: 50051
    service: ""   # 留空表示检查整个服务的健康状态

这里要强调一个最常见的误用:tcpSocket 探活的"健康"含金量很低。端口在监听,只说明进程的网络层还在,完全不代表业务逻辑正常——一个线程池耗尽、所有请求都在排队的服务,它的端口照样连得上。所以对有 HTTP 接口的服务,优先用 httpGet 配一个真正能反映业务状态的端点,而不是图省事用 tcpSocket。

三、时间参数:调紧了误杀,调松了迟钝

开头那场雪崩,直接原因就是一个时间参数(timeoutSeconds)调得太紧。探针有五个时间相关参数,它们共同决定了"多快、多敏感地"做出判断。理解它们,才能在"反应及时"和"不误杀"之间找到平衡。

initialDelaySeconds:容器启动后,等待多少秒才开始第一次探测。如果应用要 30 秒才启动完,这个值设小了,探针会在应用还没起来时就判失败。(更好的做法是用 startup probe,见第六节。)

periodSeconds:探测的间隔,默认 10 秒。多久探一次。

timeoutSeconds:单次探测的超时,默认 1 秒。这就是坑我的那个参数——默认 1 秒对很多服务都太紧了,高负载下一次正常但稍慢的响应就会超时。

failureThreshold:连续失败多少次,才最终判定探针失败,默认 3 次。它是"误杀"的最后一道缓冲——把它和 periodSeconds 一起看:failureThreshold × periodSeconds 就是"从开始不正常,到 K8s 动手"的总时间。

successThreshold:连续成功多少次才判定为成功,默认 1 次。对 readiness 探针有意义(避免服务在好坏之间抖动时频繁进出 Endpoints);liveness 和 startup 这个值必须是 1。

下面这段 YAML 把每个参数都标上注释,并算出这套配置的实际"容忍窗口":

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 20   # 容器启动后,先等 20s 再开始第一次探测
  periodSeconds: 10         # 之后每 10s 探一次
  timeoutSeconds: 5         # 单次探测最多等 5s(默认 1s 太紧,务必调大)
  failureThreshold: 3       # 连续失败 3 次才判定失败
  successThreshold: 1       # liveness 必须为 1

# 这套配置的实际效果:
#   - 容器从"开始不正常"到"被 kill 重启",最坏要经过:
#     timeoutSeconds + periodSeconds × (failureThreshold - 1)
#     = 5 + 10 × 2 = 25 秒
#   - 也就是说,一次持续不到 25 秒的卡顿,不会触发重启
#   - 这个窗口要大于你服务最长的可接受 GC 停顿 / 慢请求耗时

调参的核心思路是:liveness 探针宁可"迟钝"一点,也不要"敏感"。它的失败动作是重启,代价高、还会引发连锁反应,所以它的容忍窗口(上面算出的 25 秒)应该明显大于你服务在正常压力下可能出现的最长停顿。而 readiness 探针可以相对敏感一些——它失败只是"暂时不接流量",代价小,反应快一点反而能更快把流量从有问题的实例上挪走。

四、liveness 探针的头号坑:它绝不能检查外部依赖

前面讲的都是"怎么配",这一节讲一个"配什么"的原则性错误——它是 K8s 探针领域最经典、也最致命的坑:不要让 liveness 探针去检查数据库、下游服务、缓存这些外部依赖。

想清楚为什么。假设你的 /healthz 端点里写了"先 ping 一下数据库,ping 不通就返回失败"。某一刻数据库抖动了几秒——注意,数据库是被所有 Pod 共享的。于是所有 Pod 的 liveness 探针在同一时刻一起失败。K8s 看到的是:整个 Deployment 的容器全都"死了",于是它忠实地把它们全部 kill、全部重启。可问题来了:重启一个应用容器,根本治不好数据库的抖动。容器重启完,数据库可能还没好,新起来的容器 liveness 立刻又失败,又被重启……你的整个服务陷入了一个由"外部依赖抖动"驱动的、自己永远好不了的重启风暴。本来只是数据库抖了 5 秒、服务短暂不可用,结果被你的 liveness 配置放大成了全军覆没。

正确的划分是:liveness 只回答"我这个进程自己是不是卡死了、必须重启才能恢复"——它应该只检查进程内部的状态(主循环还转不转、有没有死锁),绝不碰任何网络外部依赖。依赖能不能用,是 readiness 的职责(下一节讲)。下面是一个"正确"的 liveness 端点,它只看进程自身,一行都不碰数据库:

package main

import (
	"net/http"
	"sync/atomic"
	"time"
)

// liveness 只回答一个问题:这个进程自己是不是卡死了?
// 它绝不检查数据库、下游服务等任何外部依赖 —— 那是 readiness 的事。

// 后台主循环每跑完一圈,就把当前时间戳写进来
var lastLoopTick atomic.Int64

// healthzHandler:liveness 端点。只看进程内部的"心跳",不碰外部依赖。
func healthzHandler(w http.ResponseWriter, r *http.Request) {
	last := lastLoopTick.Load()
	// 主循环超过 60s 没有更新心跳,说明它是真的卡死了 —— 这才该重启
	if time.Now().Unix()-last > 60 {
		w.WriteHeader(http.StatusServiceUnavailable)
		w.Write([]byte("main loop stuck, restart needed"))
		return
	}
	// 进程自身在转,就算健康 —— 哪怕此刻数据库连不上,也返回 200
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("ok"))
}

注意这个端点的关键:数据库连不上的时候,它照样返回 200。因为数据库连不上,重启这个容器是没用的——所以 liveness 就不该因此失败。它只在"主循环真的卡死"这种"重启确实能救"的情况下才返回失败。这就是 liveness 探针该有的样子:它的每一次失败,都应该对应一个"重启确实有用"的判断。

五、readiness 探针:把"暂时不行"和优雅下线串起来

上一节把外部依赖检查从 liveness 里赶了出来,那它该去哪?答案是 readiness。readiness 探针回答的是"我现在能不能接流量",它失败的后果只是被摘出 Endpoints、不再收新流量,但容器继续运行——所以它天生就适合处理一切"容器活着、但暂时不该接流量"的情况。

readiness 该检查什么?所有"没有它就没法正常服务"的关键依赖:数据库连不上、必需的下游服务挂了、本地缓存还没预热完——这些情况下,容器是活的,但它处理请求只会返回错误,那就不如先别给它流量。数据库抖动时,readiness 失败把 Pod 摘掉,数据库恢复后 readiness 自动成功、Pod 自动加回来,全程容器不重启,平滑得很。这正是上一节那个雪崩场景的正确解法。

readiness 还有一个极其重要、却常被忽略的用途:优雅下线。一个 Pod 要被删除时,K8s 会同时做两件事——给容器发终止信号、把 Pod 从 Endpoints 摘除——但这两件事不保证谁先谁后。如果容器先收到信号、关掉了服务,而 Endpoints 还没更新,这段时间里进来的流量就会打到一个正在关闭的容器上,直接报错。解决办法是配一个 preStop 钩子:收到终止信号后先 sleep 一小段时间,这几秒里 readiness 已经转为失败、Pod 已被摘除,新流量不再进来,老请求也处理完了,再真正关闭。下面是 readiness 端点 + 优雅下线的完整配合:

// readiness 回答:我现在能正常服务请求吗?
// 它可以、也应该检查关键依赖 —— 但失败只会被摘流量,不会重启容器。

var shuttingDown atomic.Bool   // 进入优雅下线流程时置为 true

func readyzHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// 已进入下线流程:主动报未就绪,让 K8s 尽快把自己摘出 Endpoints
		if shuttingDown.Load() {
			w.WriteHeader(http.StatusServiceUnavailable)
			w.Write([]byte("shutting down"))
			return
		}
		// 关键依赖:数据库 ping 不通就别接流量(但容器不会因此重启)
		ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
		defer cancel()
		if err := db.PingContext(ctx); err != nil {
			w.WriteHeader(http.StatusServiceUnavailable)
			w.Write([]byte("db not reachable"))
			return
		}
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("ready"))
	}
}

应用侧准备好了,部署侧再配上 preStop 和一个相对敏感的 readiness 探针,优雅下线的闭环就成了:

# readiness 探针:周期短、失败阈值小 —— 它失败代价低,可以敏感些
readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  periodSeconds: 5         # 每 5s 探一次,变化能很快被发现
  timeoutSeconds: 3
  failureThreshold: 2      # 连续 2 次失败就摘流量

# preStop 钩子:收到终止信号后,先等 15s 再让进程真正退出
lifecycle:
  preStop:
    exec:
      # 这 15s 里:Pod 已从 Endpoints 摘除 -> 没有新流量进来;
      #            在途的老请求继续处理完 -> 不会被打断。
      command: ["sh", "-c", "sleep 15"]

六、工程坑:慢启动、探针副作用、滚动更新、资源限流

把探针真正用稳,还有几个绕不开的工程细节。它们不属于"探针原理",但每一个都实实在在地坑过人。

坑 1:启动慢的应用,用 startup probe,别去硬调 initialDelaySeconds。一个冷启动要 60 秒的 JVM 服务,如果靠 liveness 的 initialDelaySeconds 来等,这个值要嘛设太大(启动完之后探针迟钝)、要嘛设太小(启动期间被误杀)。正确做法是配 startup probe:它成功之前,liveness 和 readiness 都不运行,等于给慢启动单独开了一个不会被误杀的"保护期"。

坑 2:探针本身不能"重",更不能有副作用。探针每隔几秒就被调一次,如果你的健康检查端点里做了复杂查询、写了日志表、甚至触发了某些业务逻辑,这些副作用会被放大几百上千倍。健康检查端点必须轻量、只读、幂等

坑 3:滚动更新时,readiness 决定了"新 Pod 什么时候开始接流量"。K8s 滚动更新时,只有新 Pod 的 readiness 成功了,才会把流量切过去、再去删老 Pod。如果新 Pod 没配 readiness,K8s 会以为它一启动就能服务,可能在它还没就绪时就把老 Pod 删了,造成更新瞬间的请求失败。配合 maxUnavailable: 0 可以保证更新全程可用实例数不下降。

坑 4:CPU 被限流(throttling),会让探针莫名其妙超时。如果容器的 CPU limits 设得很低,在负载高时容器会被 CFS 限流,导致连健康检查这种轻量请求都响应变慢、探针超时——现象和"服务卡死"一模一样,但根因是资源配额。排查探针超时时,一定要顺带看一眼 CPU throttling 指标。

下面先把"坑 1"的 startup probe 落地——给一个慢启动服务配上启动保护期:

# 一个冷启动要 60-100 秒的 JVM 服务
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10
  failureThreshold: 12      # 最多容忍 10 × 12 = 120 秒的启动时间
  # startup 成功之前,下面两个探针完全不运行

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3
  # 不再需要靠 initialDelaySeconds 硬等 —— startup 已经兜底了启动期

把前面所有原则汇成一份完整的、可直接参考的 Deployment——三种探针各司其职、滚动更新不掉实例、带优雅下线:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-svc
spec:
  replicas: 4
  strategy:
    rollingUpdate:
      maxUnavailable: 0      # 滚动更新全程不减少可用实例
      maxSurge: 1
  selector:
    matchLabels: { app: order-svc }
  template:
    metadata:
      labels: { app: order-svc }
    spec:
      terminationGracePeriodSeconds: 40   # 要大于 preStop sleep 时间
      containers:
        - name: order-svc
          image: registry.local/order-svc:1.8.0
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: "500m", memory: "512Mi" }
            limits:   { cpu: "2",    memory: "1Gi" }   # 别设太低,防限流
          # 探针 1) startup:给慢启动兜底,成功前另外两个探针不跑
          startupProbe:
            httpGet: { path: /healthz, port: 8080 }
            periodSeconds: 10
            failureThreshold: 12
          # 探针 2) liveness:只查进程自身,窗口放宽,失败才重启
          livenessProbe:
            httpGet: { path: /healthz, port: 8080 }
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          # 探针 3) readiness:查依赖,失败只摘流量不重启
          readinessProbe:
            httpGet: { path: /readyz, port: 8080 }
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 2
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 15"]

最后是排查探针问题时最常敲的几条命令——出问题时照着这个顺序走,基本能定位:

# 1. 看 Pod 状态和重启次数:RESTARTS 高 + CrashLoopBackOff = 探针在作怪
kubectl get pods -o wide

# 2. 看 Events,探针失败的具体原因(超时?连接拒绝?)都在这里
kubectl describe pod <pod-name>

# 3. 看上一次被 kill 掉的那个容器留下的日志 —— 关键是 --previous
kubectl logs <pod-name> --previous

# 4. 进容器里手动跑一次探针请求,验证健康端点本身对不对
kubectl exec -it <pod-name> -- curl -sv http://localhost:8080/healthz

# 5. 看是不是 CPU 被限流导致探针超时(坑 4)
kubectl top pod <pod-name>

# 6. 实时观察 Pod,确认改完探针配置后是否不再反复重启
kubectl get pod <pod-name> -w

关键概念速查

探针 / 参数 说明
liveness probe 探活;失败到阈值则 kill 并重启容器;只该查进程自身
readiness probe 探就绪;失败则从 Endpoints 摘除、不重启;可查外部依赖
startup probe 探启动完成;成功前 liveness/readiness 都不运行
httpGet 访问 HTTP 路径,状态码 200–399 算成功,最常用
exec 容器内执行命令,退出码 0 算成功,开销较大
tcpSocket 能建立 TCP 连接算成功,只证明端口在听
grpc K8s 1.24+ 原生,调标准 gRPC 健康检查协议
initialDelaySeconds 容器启动后等多久开始第一次探测
periodSeconds 两次探测的间隔,默认 10s
timeoutSeconds 单次探测超时,默认 1s——通常太紧,务必调大
failureThreshold 连续失败几次才判定失败,默认 3
preStop 容器终止前执行的钩子,配 sleep 实现优雅下线

避坑清单

  1. 探针不是"配了就有保障",它是你交给 K8s 的判断指令;指令下错了,K8s 会忠实地把你的服务搞垮。
  2. 三种探针失败后果完全不同:liveness 失败重启容器,readiness 失败摘流量不重启,startup 失败前另两个探针不跑。
  3. liveness 探针绝不能检查数据库等外部依赖——依赖一抖动会导致所有 Pod 同时失败、被集体重启、陷入重启风暴。
  4. liveness 只该检查进程自身状态(主循环、死锁),它的每次失败都应对应"重启确实有用"的情况。
  5. 外部依赖检查放进 readiness:依赖抖动时摘流量、恢复后自动加回,全程不重启,平滑无感。
  6. timeoutSeconds 默认 1 秒对多数服务太紧,高负载下一次稍慢的正常响应就会超时误杀,务必调大。
  7. liveness 的容忍窗口(timeoutSeconds + periodSeconds×(failureThreshold-1))要大于服务最长可接受停顿。
  8. 启动慢的应用用 startup probe,别靠硬调 initialDelaySeconds,后者要嘛迟钝要嘛误杀。
  9. 健康检查端点必须轻量、只读、幂等;探针每几秒调一次,任何副作用都会被放大成千上万倍。
  10. 配 preStop sleep + readiness 实现优雅下线;CPU limits 别设太低,否则限流会让探针莫名超时。

总结

回头看那场大促雪崩,最该记住的不是"timeoutSeconds 要调大"这个具体结论,而是我上线前那个想当然的判断——"配上 liveness 只有好处没有坏处"。探针给人的第一印象太温和了:健康检查嘛,容器坏了帮你重启,听起来纯粹是保险。但 liveness 探针手里握着的是"重启容器"这个破坏性动作,而它做判断的依据,完全是你写的那个端点、你配的那几个数字。你把判断标准写松了,它迟钝;写紧了,它误杀;让它去查不该查的东西,它就成了把局部故障放大成全局雪崩的那个开关。探针不是保险,它是一个你必须认真对待的、有杀伤力的控制器。

所以用好探针,核心是想清楚一件事:每一种探针失败时,K8s 会做什么,而那个动作到底能不能解决问题。liveness 失败 → 重启,所以它只该在"重启确实有用"时失败——也就是进程自己卡死了;readiness 失败 → 摘流量,所以一切"容器还活着、但这会儿不该接流量"的情况都归它——依赖不可用、还在启动、正在下线。把这条线想清楚,你就不会再把数据库检查塞进 liveness,也不会再指望一个探针包打天下。三种探针不是三个可选项,而是三种各司其职的角色:startup 负责"启动期别被打扰",liveness 负责"真死了才重启",readiness 负责"没准备好就别给流量"。

这篇文章的几节,其实是顺着"配一个探针要做的决定"展开的:先分清三种探针各管什么、失败动作是什么;再选检查方式(httpGet / exec / tcpSocket / grpc);再调时间参数,在"反应及时"和"不误杀"之间找平衡;然后是最关键的一条原则——liveness 不查依赖;接着把依赖检查和优雅下线交给 readiness;最后收尾几个工程坑。你会发现,探针配置和写代码里的"异常处理"特别像:你不能笼统地 catch 一切然后做同一个动作,你得分清这个异常是该重试、该降级、还是该直接抛出——探针也一样,你得分清这次"不健康"是该重启、该摘流量、还是该再等等。

最后想说,K8s 的自愈能力确实强大,但"自愈"的前提是你得准确地告诉它"什么算病、该吃什么药"。诊断标准定错了,自愈就变成了自残——一个健康的容器被反复重启,一次依赖抖动被放大成全面雪崩,都是这么来的。所以下次给服务配探针前,不妨对着每一个探针问自己三句话:它失败的时候,K8s 会做什么?那个动作,真的能解决我担心的那种故障吗?如果它在最不该失败的时候失败了(比如高峰期、依赖抖动时),会发生什么?能从容回答这三句,你的探针才是真正的保险,而不是埋在 YAML 里的一颗雷。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

AI Agent 工具调用完全指南:从一次"模型乱调退款接口"看懂 Function Calling

2026-5-21 18:07:53

技术教程

大模型上下文窗口完全指南:从一次"AI 聊着聊着就失忆"看懂 token 与上下文管理

2026-5-21 18:15:14

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索