欢迎光临
我们一直在努力

K8s 探针配置的 3 个反直觉行为:为什么 Pod 一直没就绪

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

我不会起名字322 · 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • K8s 探针配置的 3 个反直觉行为:为什么 Pod 一直没就绪
    • 一、先分清三种探针(很多人混用)
    • 二、反直觉行为 1:startupProbe 与 initialDelaySeconds 的优先级
    • 三、反直觉行为 2:什么时候被杀 = failureThreshold × periodSeconds
    • 四、反直觉行为 3:liveness 打到未就绪的 Pod → 重启风暴
    • 五、探针参数对照表(可带走物)
    • 六、排查没就绪的三步

K8s 探针配置的 3 个反直觉行为:为什么 Pod 一直没就绪

“应用日志明明写着 server started on :8080,为什么 Pod 一直是 0/1?”

这是个几乎每个上 K8s 的人都会遇到的困惑。你盯着 Pod 状态,READY 是 0/1,kubectl describe 里一堆 Readiness probe failed。而应用自己觉得一切正常。

问题几乎总是出在探针配置上。这篇先分清三种探针,再拆 3 个反直觉行为。

一、先分清三种探针(很多人混用)

探针作用失败后果失败的"严重度"
livenessProbe 判断容器活着吗 重启容器 高(会重启)
readinessProbe 判断容器能收流量吗 从 Service 摘除,不重启 中(不接流量)
startupProbe 判断容器启动完了吗 重启容器 高(会重启)

一、先分清三种探针(很多人混用)

最常见的错误:把 liveness 探针配得太激进去探业务接口,或者用 liveness 代替 readiness,导致"服务没就绪就被杀 → 重启 → 又没就绪"的重启风暴。

二、反直觉行为 1:startupProbe 与 initialDelaySeconds 的优先级

很多人以为配了 startupProbe 之后,livenessProbe.initialDelaySeconds 就无所谓了。基本正确,但有个坑:

  • 配了 startupProbe:在 startup 成功之前,liveness 和 readiness 都不会执行。这期间只有 startup 在起作用。
  • 没配 startupProbe:liveness 会立即按 initialDelaySeconds 开始计时。

反直觉点:initialDelaySeconds 不是"启动后多久开始探",而是"容器创建后多久开始第一次探测"。如果镜像拉取花了 40 秒,而 initialDelaySeconds = 10,那么第一次 liveness 探测发生在应用还没启动时——直接失败。

# ❌ 危险:初始延迟太短
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 5 # 应用 30 秒才启动完 → 前 5 次全失败
periodSeconds: 5
failureThreshold: 3 # 15 秒后就被杀 → 重启风暴

# ✅ 正确:用 startupProbe 单独兜启动期
startupProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 5
failureThreshold: 30 # 最多等 150 秒启动
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 3

记忆点:startupProbe 是给"启动慢的应用"用的,配了它,liveness 的 initialDelaySeconds 就可以设成 0。

三、反直觉行为 2:什么时候被杀 = failureThreshold × periodSeconds

这是最容易被误算的一个。决定"多久被杀"的不是 initialDelaySeconds,而是:

最长容忍时间 = failureThreshold × periodSeconds

举例:

livenessProbe:
initialDelaySeconds: 10 # 第一次探测在 10s 后
periodSeconds: 10 # 每 10s 探一次
failureThreshold: 3 # 连续失败 3 次就杀

从容器创建到被杀的最坏时间 = initialDelaySeconds + failureThreshold × periodSeconds = 10 + 30 = 40 秒。

反直觉点:timeoutSeconds 是每次探测的超时,它也算进周期。如果 timeoutSeconds = 5、periodSeconds = 10,那实际周期可能变成"探测 5 秒超时 + 等 5 秒",节奏被打乱。

# 完整的时间账
livenessProbe:
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5 # 单次探测超时
failureThreshold: 3
successThreshold: 1 # liveness 必须为 1
# 最坏:10 + 3×(5超时+10间隔) = 55 秒才被杀

readiness 的 successThreshold 可以 > 1(需要连续成功 N 次才算就绪),但 liveness 的 successThreshold 只能是 1(这是强制的)。

四、反直觉行为 3:liveness 打到未就绪的 Pod → 重启风暴

这是最惨烈的一种。典型配置错误:

# ❌ liveness 探了一个"依赖就绪"的接口
livenessProbe:
httpGet: { path: /api/health, port: 8080 } # 这个接口依赖下游 DB/Redis

问题链:

  • 服务启动,但下游 DB 还没连上(或者自己还没热身完);
  • /api/health 因为依赖未就绪返回 500;
  • liveness 判定失败 → 重启容器;
  • 重启后又没连上下游 → 又失败 → 又重启;
  • 重启风暴——Pod 永远起不来,还拖垮了下游。
  • 四、反直觉行为 3:liveness 打到未就绪的 Pod → 重启风暴

    正确原则:

    探针该探什么不该探什么
    liveness 进程是否卡死(一个不依赖任何外部组件的轻量端点) 业务接口、依赖下游的接口
    readiness 是否具备处理请求的能力(可以含依赖检查) 会触发重活的接口

    # ✅ liveness 只探进程健康
    livenessProbe:
    httpGet: { path: /healthz, port: 8080 } # 只返回 200,不查依赖
    # ✅ readiness 探业务就绪(可含依赖)
    readinessProbe:
    httpGet: { path: /readyz, port: 8080 } # 检查 DB 连接等

    Go 实现示例:

    // 健康端点分离
    mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    // liveness:只要进程活着就返回 200,不查任何依赖
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("ok"))
    })

    mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
    // readiness:检查下游依赖
    ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
    defer cancel()
    if err := db.PingContext(ctx); err != nil {
    w.WriteHeader(http.StatusServiceUnavailable)
    w.Write([]byte("db not ready"))
    return
    }
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("ready"))
    })

    五、探针参数对照表(可带走物)

    参数含义推荐值常见错误
    initialDelaySeconds 创建后多久首次探测 liveness: 配了 startup 可设 0 设太短→启动期被杀
    periodSeconds 探测间隔 10(readiness 可 5) 太小→频繁探测耗资源
    timeoutSeconds 单次超时 2~5 大于 period→节奏乱
    failureThreshold 连续失败几次算失败 liveness: 3;readiness: 3 设 1→一次抖动就摘
    successThreshold 连续成功几次算成功 readiness: 1~2;liveness: 必须 1 readiness 设太高→恢复慢

    六、排查没就绪的三步

    顺序动作命令
    1 看具体失败原因 kubectl describe pod <pod> 找 Probe failed 那行
    2 手动模拟探测 kubectl exec -it <pod> — curl -v localhost:8080/readyz
    3 核对端口和路径 探针的 port 是容器端口,不是 Service 端口

    第 3 条特别常错:探针的 port 必须写容器内监听的端口。如果写了 Service 的端口或 hostPort,探测必然失败。

    留个问题给你:你们的 liveness 探针是探 /healthz 这种纯进程健康端点,还是探业务接口?initialDelaySeconds 现在配的是多少?

    赞(0)
    未经允许不得转载:171主机测评 » K8s 探针配置的 3 个反直觉行为:为什么 Pod 一直没就绪
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址