K8s 探针配置错了,服务为什么会反复重启?聊聊 4 个常见误区

服务明明可以正常启动,容器却不断重启;Pod 看起来是 Running,流量却一直打不进去;一次滚动发布,旧实例已经开始退出,新实例还没有准备好。

很多 Kubernetes 上的"应用故障",最后都会指向同一组配置:

yaml 复制代码
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5

readinessProbe:
  httpGet:
    path: /health
    port: 8080
  periodSeconds: 5

看起来配置很简单,但它背后至少涉及四个问题:

  • 启动探针负责什么?
  • 就绪探针什么时候应该失败?
  • 存活探针失败后应该发生什么?
  • 优雅退出时,Kubernetes 和应用如何协作?

四个概念只要混在一起,就可能出现"越检查越不稳定"的结果。

先分清三种探针的职责

Kubernetes 常见的三类探针经常被合成一个 /health 接口,但它们回答的问题完全不同。

Startup Probe:应用是否已经完成启动

适合启动慢、需要加载配置、预热模型或建立数据库连接的应用。

它在启动阶段检查应用是否完成初始化。对于启动时间波动较大的服务,可以避免 Liveness Probe 过早杀死应用。

Readiness Probe:应用是否准备好接收流量

它决定 Pod 是否应该进入 Service 的 Endpoint 列表。

失败不一定意味着进程坏了。它可能表示:

  • 依赖暂时不可用;
  • 正在加载缓存;
  • 队列积压过重;
  • 应用处于维护或降级状态;
  • 正在等待数据库迁移完成。

Liveness Probe:应用是否仍然可以继续运行

它应该判断进程是否陷入不可恢复的状态,例如永久死锁、事件循环阻塞、内部状态损坏。

Liveness 失败通常会导致容器重启。这个动作代价很高,所以它不应该被用来判断数据库、Redis 或某个下游服务是否健康。

一句话总结:

text 复制代码
Startup:能不能完成启动
Readiness:能不能接流量
Liveness:是否需要重启

三者混用时,最常见的结果就是:下游抖了一下,应用其实可以降级运行,却被 Liveness 判定为死亡,所有实例同时重启,故障进一步放大。

误区一:Liveness Probe 检查所有依赖

很多团队把 Liveness 接口写成这样:

ts 复制代码
app.get("/health", async () => {
  await database.ping();
  await redis.ping();
  await paymentService.ping();
  return { status: "ok" };
});

这个接口用来做 Readiness 都有点激进,用来做 Liveness 则非常危险。

假设支付服务发生网络抖动:

text 复制代码
支付服务不可用
  -> 所有 Pod 的 Liveness 同时失败
  -> Kubernetes 开始重启 Pod
  -> 系统出现大量连接重建
  -> 数据库和消息队列压力上升
  -> 更多依赖失败
  -> 更多 Pod 被重启

原本只是一个下游不可用,最后演变成整个服务不可用。

更合理的划分是:

text 复制代码
/health/live
  只检查进程是否可以响应、事件循环是否被长期阻塞

/health/ready
  检查是否可以接收当前流量
  可以包含关键依赖状态和降级判断

/health/startup
  检查初始化流程是否完成

Liveness 最好使用尽可能少的本地检查,不要做外部网络调用。

如果应用必须依赖外部服务才能工作,可以让 Readiness 失败并停止接流量,而不是让 Liveness 重启所有实例。

误区二:Readiness 通过得太早,也可能太晚失败

Readiness 不只是"启动完成后返回 200"。

它还应该覆盖应用在运行过程中暂时无法服务的状态。

例如:

  • 本地缓存为空,直接处理请求会产生大量回源;
  • 数据库连接池正在重建;
  • 服务正在滚动升级;
  • 队列消费者已经落后太久,继续接流量没有意义;
  • 应用处于只读降级模式,不满足核心请求要求。

一个典型的启动配置可以是:

yaml 复制代码
startupProbe:
  httpGet:
    path: /health/startup
    port: 8080
  failureThreshold: 30
  periodSeconds: 2

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 3

livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

这些数字不是通用答案。关键是它们必须和应用的实际行为匹配:

text 复制代码
启动最长需要多久?
一次探针响应通常需要多久?
依赖恢复需要多久?
故障时应该停止接流量,还是重启进程?

如果 periodSeconds、timeoutSeconds 和 failureThreshold 完全不匹配,就会出现:

  • 应用还在启动,已经被 Liveness 杀死;
  • 短暂网络抖动导致 Readiness 反复抖动;
  • 探针超时比业务请求还短,正常负载被误判;
  • 失败阈值太大,故障长时间无法被识别。

探针的参数应该来自观测数据,而不是复制别人的 YAML。

误区三:只有探针,没有优雅退出

滚动发布时,Pod 不会在收到终止信号后瞬间消失。

一个更合理的生命周期是:

text 复制代码
Pod 被标记为 Terminating
  -> 从 Service Endpoint 中摘除
  -> 应用收到 SIGTERM
  -> 停止接收新请求
  -> 等待正在处理的请求结束
  -> 关闭连接池和后台任务
  -> 进程退出

如果应用收到 SIGTERM 后立刻退出,正在处理的请求可能失败;如果进程不退出,Kubernetes 最终会在 terminationGracePeriodSeconds 到期后强制结束。

因此,优雅退出至少要考虑:

  • 先停止接收新流量,再处理存量请求;
  • HTTP 服务停止监听;
  • 消息消费者停止拉取新消息,等待当前消息处理完成;
  • 数据库事务和外部调用有明确超时;
  • 后台任务可以被取消或安全中断;
  • 退出总时间小于 terminationGracePeriodSeconds。

一个 Node.js 服务的简化示例:

ts 复制代码
let shuttingDown = false;

server.on("request", (req, res) => {
  if (shuttingDown && req.url !== "/health/live") {
    res.statusCode = 503;
    res.end("shutting down");
    return;
  }

  handleRequest(req, res);
});

process.on("SIGTERM", async () => {
  shuttingDown = true;

  await new Promise(resolve => server.close(resolve));
  await consumer.stop();
  await database.close();

  process.exit(0);
});

真实实现还要处理强制退出兜底、连接超时和任务补偿,但方向是明确的:

探针决定 Pod 什么时候开始接流量,优雅退出决定 Pod 什么时候停止接流量。

缺少退出流程时,即使探针配置正确,滚动发布仍会出现 502 或请求中断。

误区四:把探针当成业务监控

探针只服务于 Pod 生命周期和流量管理,不能替代业务监控。

例如:

  • 探针只能告诉你"某个实例能不能接流量";
  • 监控可以告诉你"用户下单成功率是否下降";
  • 探针失败会触发重启或摘流量;
  • 业务监控失败可以触发告警、限流或降级。

如果把所有业务状态都塞进 Liveness,Kubernetes 会频繁重启容器;如果把所有外部依赖都塞进 Readiness,短暂抖动又会让整个服务从 Endpoint 中消失。

更合理的分工是:

text 复制代码
Startup:启动成功即可
Readiness:当前是否有能力服务
Liveness:进程是否需要重启
Metrics:系统正在发生什么
Alert:人是否需要介入

探针应该回答"要不要采取 Kubernetes 动作",而不是"业务是否完全健康"。

一套更稳妥的落地方式

1. 先定义状态

写探针之前,先明确:

text 复制代码
未启动:不能接流量,不能重启
启动中:不能接流量,不能重启
已就绪:可以接流量
暂时不可用:停止接流量,但不要重启
永久不可用:可以重启
正在退出:停止接新流量,处理存量请求

2. 让三种探针各司其职

yaml 复制代码
startupProbe:
  httpGet: { path: /health/startup, port: 8080 }

readinessProbe:
  httpGet: { path: /health/ready, port: 8080 }

livenessProbe:
  httpGet: { path: /health/live, port: 8080 }

不要让三个接口返回完全相同的结果。

3. 用真实数据调整参数

至少采集:

  • 应用启动耗时分布;
  • 探针 P95 响应时间;
  • 依赖恢复时间;
  • 重启次数;
  • Endpoint 摘除和恢复时间;
  • 滚动发布期间的错误率。

4. 验证故障场景

只验证"正常时探针返回 200"远远不够。还应该验证:

  • 启动过程中 Liveness 不会误杀;
  • 下游不可用时 Readiness 能正确摘流量;
  • 进程阻塞时 Liveness 能触发重启;
  • 发布过程中存量请求不会被中断;
  • 探针恢复后实例能重新进入 Endpoint。

最后检查这 8 项

  • Startup、Readiness、Liveness 是否职责分离
  • Liveness 是否避免检查外部依赖
  • Readiness 是否反映真实接流能力
  • 探针超时和阈值是否来自真实数据
  • 慢启动服务是否使用 Startup Probe
  • 应用收到 SIGTERM 后是否优雅退出
  • 退出时间是否小于 terminationGracePeriodSeconds
  • 是否用故障演练验证过摘流量和重启行为

K8s 探针不是"配置得越多越安全"。

它真正决定的是:什么时候接流量、什么时候停流量、什么时候重启进程。把这三个动作设计清楚,比单纯调整 failureThreshold 更重要。

你们的存活探针会检查数据库吗?有没有遇到过下游故障导致 Pod 集体重启?欢迎在评论区聊聊。

如果你也在优化 Kubernetes 上的服务稳定性,也欢迎在微信公众号搜索「长安米粒贵」继续交流。

相关推荐
小陈运维3 小时前
使用kubeadm初始化IPV4/IPV6集群
linux·docker·云原生·容器·kubernetes
笑洋仟4 小时前
k8s的Ingress配置问题
云原生·容器·kubernetes
垚垚学技术_聚焦云原生4 小时前
K8s OpenEBS LVM 动态存储 PVC Pending 排查记
云原生·容器·kubernetes
我不是阵雨16 小时前
微服务幂等性深度解剖
微服务·云原生·架构
code_slave(码畜)18 小时前
微服务架构落地:消息队列架构设计(上篇)——异步解耦、削峰填谷,看懂业务事件流转本质
spring boot·spring cloud·微服务·云原生·架构
code_slave(码畜)1 天前
微服务架构落地:消息队列架构设计(下篇)——消息堆积、死信治理与集群监控告警实战
spring boot·spring cloud·微服务·云原生·中间件·架构
sbjdhjd1 天前
云安全 | 从特权容器到旧版 runC:Docker 宿主机访问与运行时缺陷复盘
运维·网络安全·docker·云原生·kubernetes·云计算·云安全
蜡笔婧萱2 天前
Docker介绍及其基础简介(一)
云原生·eureka