探针结果是控制循环使用的证据,不是平台对应用健康的直接理解。Kubernetes 不会从 healthz 这个路径名称推断你希望怎样恢复服务;真正决定下一步的是探针类型、结果、阈值与容器生命周期。把每次失败都描述成“重启 Pod”,会抹掉直接影响可用性的关键区别。

三个问题,对应不同动作

启动探针为初始化提供独立的检测政策。配置后,在它成功之前,存活和就绪探针都不会执行。启动探针成功后,在当前容器生命周期内便不再持续检查;如果容器重新启动,则需要再次通过启动阶段,不能把上一次成功沿用到新生命周期。

存活检查用于识别适合通过重启当前容器恢复的状态。达到失败阈值后,kubelet 终止受到影响的容器,并应用对应的重启政策。这不是保证重建整个 Pod、改变其身份或修复外部数据库,也不意味着容器之间的依赖与在途工作都能自动安全恢复。

就绪失败则让容器继续运行,并持续检查是否恢复。它影响通常按就绪状态选择目标的 Service 流量,但不是防火墙,也不会取消已经通过连接到达的请求。直接访问实例的客户端和现有连接,仍需要自己的生命周期处理,不能把就绪状态误读成网络阻断保证。

阈值统计连续观察结果

failureThreshold 为三,不是从启动以来累计失败三次就动作,而是连续失败达到三次;一次成功会打断这段失败序列。就绪恢复可以要求多次连续成功,而启动和存活探针的 successThreshold 必须为一。阅读结果轨迹之前,应先明确进入失败和恢复成功两个方向的阈值。

超时、初始延迟、执行周期和终止宽限期同样影响实际表现。阈值乘周期可以辅助估算,但不能当成端到端故障切换的精确秒表;调度、探针执行和路由传播都参与其中。容器尚未就绪时,就绪探针还可能比配置周期更频繁地执行,以便更快恢复就绪。

实验选择四次启动失败、三次存活失败、两次就绪失败,并要求连续两次就绪成功才能恢复。这组数字只是为了看清不同计数器的边界,不是生产推荐。真实门槛应结合启动时间、检查成本、错误恢复方式以及误判代价确定,不能照搬实验参数。

检查机制决定你拿到了什么证据

HTTP 检查使用文档定义的 200 至 399 成功范围。即使响应正文写着“内部错误”,返回 200 也不会因为人类读出了坏消息,就自动变成失败探针。应沿实际探测路径检查认证和重定向,而不是默认平台与交互式浏览器具有完全相同的访问结果。

TCP 探针证明的是连接可以打开,不是查询或事务能够完成。exec 检查则需要镜像里确实存在相应命令,并且退出状态有明确含义;频繁创建进程还会消耗节点资源。应该选择能够回答当前问题的低成本机制,而不是认为检查越复杂、调用越多就越可靠。

就绪成功并不是存活执行的前提。没有启动探针隔离时,初始化较长的应用可能还没来得及就绪,就因为存活失败反复被终止。相反,启动容忍窗口过大,也会推迟发现真正无法完成的启动。两边都需要依据实际启动分布和恢复目标权衡。

探针失败与 Unknown 结果不是同一种状态。Kubernetes 文档规定 Unknown 不改变容器状态,之后继续检查。本例只接受成功与失败两个值,故意没有第三种结果,所以无法预测传输错误会如何分类。实际排查应检查 kubelet 事件和部署中的探针处理机制,先确认错误属于哪类,再解释是否应计入连续失败。

坏掉的样子

把外部依赖可用性直接作为存活条件,可能在共享故障期间不断重启健康进程;另一个独立线程上始终绿色的处理器,又可能遗漏业务工作线程的死锁。重启并不天然便宜,它会中断在途工作、重复初始化,也可能进一步增加故障依赖的压力。

如果就绪涵盖每一项可选依赖,本来还能服务的容量也会被撤走;如果状态频繁抖动,负载会在副本之间来回转移,进一步恶化饱和。因此,既要测试明确故障能否被发现,也要测试误判和恢复行为,不能只看一次故障注入能不能把状态变红。

应对照探针结果、容器重启次数、事件记录、就绪变化和真实客户端流量,并确认部署中的配置与路由消费者。一个本地计数模型通过,不代表 kubelet、网络路径或负载均衡器已经正确执行同样规则。平台侧证据不能由应用侧自述替代。

快速验证

保存为 probe_policy_check.py,用 Python 3.10 以上版本执行 python3 probe_policy_check.py,本轮已在 Python 3.12.14 上重新运行。这个确定性模型接收成功或失败两个值,不部署 Pod,不调用 Kubernetes,不安排真实探测时间,也不会重启操作系统进程。

class ProbePolicy:
    # (consecutive successes, consecutive failures), chosen for this experiment.
    thresholds = {"startup": (1, 4), "liveness": (1, 3), "readiness": (2, 2)}

    def __init__(self):
        self.restart_requests = 0
        self.start_container()

    def start_container(self):
        self.started = self.ready = self.restart_requested = False
        self.successes = dict.fromkeys(self.thresholds, 0)
        self.failures = dict.fromkeys(self.thresholds, 0)

    def sample(self, kind, passed):
        if self.restart_requested:
            return "awaiting-restart"
        if kind != "startup" and not self.started:
            return "suppressed"
        if kind == "startup" and self.started:
            return "disabled"
        success_limit, failure_limit = self.thresholds[kind]
        self.successes[kind] = self.successes[kind] + 1 if passed else 0
        self.failures[kind] = 0 if passed else self.failures[kind] + 1
        if passed and self.successes[kind] >= success_limit:
            if kind == "startup":
                self.started = True
            elif kind == "readiness":
                self.ready = True
        if not passed and self.failures[kind] >= failure_limit:
            self.ready = False
            if kind != "readiness":
                self.restart_requested = True
                self.restart_requests += 1
                return "restart-requested"  # A model output, not a kubelet or OS action.
        return "sampled"


policy = ProbePolicy()
for _ in range(3):
    assert policy.sample("startup", False) == "sampled"
    assert policy.sample("liveness", False) == "suppressed"
    assert policy.sample("readiness", True) == "suppressed"
assert policy.restart_requests == 0 and not policy.ready
assert policy.failures["liveness"] == 0 and policy.successes["readiness"] == 0
policy.sample("startup", True)
assert policy.started and not policy.ready
assert policy.sample("startup", False) == "disabled"
print("three startup failures do not trigger liveness; startup success enables the other probes")

policy.sample("readiness", True)
assert not policy.ready
policy.sample("readiness", True)
assert policy.ready
policy.sample("readiness", False)
assert policy.ready
policy.sample("readiness", True)  # Breaks the consecutive-failure run.
policy.sample("readiness", False)
assert policy.ready
policy.sample("readiness", False)
assert not policy.ready and policy.restart_requests == 0
for result in (True, False, True):
    policy.sample("readiness", result)
assert not policy.ready
policy.sample("readiness", True)
assert policy.ready
print("two readiness failures remove readiness without restart; two consecutive successes restore it")

for result in (False, False, True, False, False):
    policy.sample("liveness", result)
assert policy.restart_requests == 0
assert policy.sample("liveness", False) == "restart-requested"
assert policy.restart_requests == 1 and not policy.ready
assert policy.sample("liveness", False) == "awaiting-restart"
assert policy.restart_requests == 1
policy.start_container()  # Explicitly model the beginning of another container lifetime.
assert not policy.started and not policy.ready
for _ in range(4):
    policy.sample("startup", False)
assert policy.restart_requests == 2
print("three consecutive liveness failures request a restart; a fresh lifetime runs startup checks again")
print("four startup failures request another restart; no real container, clock, or traffic route was changed")

前三次启动失败尚未达到实验阈值,期间试图输入的存活、就绪观察都会被抑制,不增加它们的计数。启动成功后,其他检查才开始生效,但不会因此自动变成就绪,仍然需要满足就绪自己的成功条件。

连续两次就绪失败撤下就绪状态,却不申请重启;恢复需要连续两次成功,中间插入相反结果会重置相应的连续计数。连续三次存活失败才产生一次重启请求;模型处于等待重启阶段时,继续输入失败不会重复增加请求数量。

显式开始一个新的容器生命周期后,启动状态被清空,连续四次启动失败又会申请重启。Unknown 结果、传输错误分类、真实计时、重启政策执行、终止宽限和端点传播都没有在此验证;依赖完整平台行为之前,需要在目标集群上补充对应验收。

延伸阅读

来源