freeCodeCamp.org

How to Debug a Stuck Kubernetes Rollout with a Hands-On Lab

7.1内容质量

TL;DR · AI 摘要

How to Debug a Stuck Kubernetes Rollout with a Hands-On Lab October 1, 2026 / Kubernetes Praneeth Kodumagulla You update...

核心要点

  • 主题聚焦:How to Debug a Stuck Kubernetes Rollout with a H
  • 来源:freeCodeCamp.org,建议结合原文判断细节。
  • AI 分析暂不可用,本条为保底评分与摘要。
#AI#编程#后端#云计算#安全
打开原文

如何通过动手实验调试卡住的 Kubernetes 滚动更新

2026年10月1日

/

#Kubernetes

Praneeth Kodumagulla

你更新了一个 Deployment,运行 kubectl rollout status 并等待。命令超时了。但当你向 Service 发送请求时,它仍然能正常响应。那么更新是否已经完成?如果尚未完成,哪些 Pod 正在处理你的流量?

你将在一个临时的 Kubernetes 实验环境中调查这种情况。从一个健康的应用程序开始,你将引入三个故障:一个无法拉取的镜像、一个永远无法通过的就绪探针,以及一个无法调度的 Pod。你将识别被阻塞的版本,检查其证据,并在继续之前验证恢复过程。

在记录的镜像失败案例中,部署报告了这两个条件。该表格源自其 JSON 快照:

| 条件 | 状态 | 原因 | |--------------|------------------|--------------------------| | Available | True | MinimumReplicasAvailable | | Progressing | False | ProgressDeadlineExceeded |

这些条件回答了不同的问题。以下实验展示了为什么应用程序在更新卡住时仍能响应请求。

目录

  • 如何搭建实验环境
  • 如何建立健康的滚动更新
  • 如何诊断镜像拉取失败
  • 如何区分两种超时
  • 如何恢复健康配置
  • 如何诊断就绪状态失败
  • 如何诊断放置约束
  • 如何清理环境
  • 结论

如何搭建实验环境

你应该熟悉容器、Deployment、ReplicaSet、Pod、Service 和基本的 kubectl 命令。实验使用 kind 在 Docker 容器内运行单个 Kubernetes 节点。

记录的环境配置如下:

| 组件 | 版本或配置 | |------------|----------------------------------------------------------------------------| | 主机 | macOS 15.7.4,Apple 芯片,16 GiB 内存 | | Colima | 0.10.3,Docker 运行时,4 核 CPU,Docker 可见内存约 5.77 GiB | | Docker | 客户端 29.4.3,服务端 29.2.1 | | kind | 0.33.0 | | kubectl | 1.36.0 | | Kubernetes 服务端 | 1.37.0 |

配套脚本需要 macOS ARM64 系统,且安装了 Colima 和 kind 0.33.0。我未在 Linux、Windows 和 Intel 机器上进行测试。这些内存数据描述的是测试机器配置,不是最低要求。

你应已安装 Docker CLI、Colima、kind 和 kubectl,并确保 Colima 正在运行且能访问 Docker Hub。

[在此处下载配套代码](#)。将其解压到新目录中,并在终端中打开该目录。启动 Bash(这是我使用的 shell),然后运行 setup:

bash
bash setup.sh

Setup 会创建 fcc-rollout-lab 集群和 rollout-lab 命名空间。它会将凭证写入 ./kubeconfig,通过不可变的 ConfigMap 挂载应用程序源代码,并创建 Deployment、Service 和一个 http-client Pod。不需要自定义镜像构建。

随后它会验证 v1 和 v2,将快照和 HTTP 响应保存到证据归档中。如果已有实验室集群或本地 kubeconfig,Setup 会拒绝覆盖。节点镜像和 Python 镜像在源代码中通过摘要固定。

设置成功后,从配套目录中定义此函数:

bash
LAB_ROOT="$PWD"
k() {
  kubectl --kubeconfig "$LAB_ROOT/kubeconfig" \
    --context kind-fcc-rollout-lab \
    --namespace rollout-lab "$@"
}

配套代码还包含 failures.sh,这是一个自动化重现案例和检查的脚本。按照以下命令进行手动操作。自动化运行器是另一种重现序列的方式。

这里,k 是一个 Bash 函数,它使用本实验的 kubeconfig、context 和 namespace 运行 kubectl。例如,k get pods 会列出实验环境中的 Pod。请在当前 Bash 终端中运行一次上述定义,并在整个操作过程中保持使用同一个终端和配套目录。

如何建立健康的滚动更新

在破坏任何内容之前,请先验证滚动更新路径和服务是否正常工作。设置过程通过仅更改 Pod 模板中的 APP_VERSION 环境变量来执行 v1 到 v2 的更新。

该应用是一个小型的 Python HTTP 服务器。GET / 会返回其配置的版本和 Pod 名称,/ready 返回 HTTP 200。所有其他路径均返回 HTTP 404。Pod 名称通过 downward API 从 metadata.name 获取。

以下是 app/server.py 中的路由逻辑:

code
if self.path == "/":
    status = 200
    payload = {
        "version": os.environ["APP_VERSION"],
        "pod": os.environ.get("POD_NAME", "local-test"),
    }
elif self.path == "/ready":
    status = 200
    payload = {"ready": True}
else:
    status = 404
    payload = {"error": "unknown path"}

就绪探针通过 8080 端口检查 /ready。这些配置来自 Deployment 的 spec,定义在 manifests/deployment.yaml 中。以下为部分内容,非完整清单:

code
replicas: 2
minReadySeconds: 5
progressDeadlineSeconds: 180
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1

通过这些滚动更新设置,控制器可以添加一个替换 Pod。它会在减少两个健康的旧副本之前等待替换 Pod 可用。新 Pod 必须保持 Ready 状态 5 秒才能被视为可用。进度截止时间是 180 秒。

maxUnavailable: 0 对此滚动更新设置了限制。它不会保护旧 Pod 免受其他故障影响。终止的 Pod 也可能在活动副本计数之外保持可见。这些设置是教学选择,而非生产可用性保证。

记录的控制均成功完成并返回了 10 次正确的服务响应。此表格源自两个 Deployment 快照:

| 控制 | 版本 | 更新 | 就绪 | 正确的 HTTP 响应 | |------|------|------|------|------------------| | v1 | 1 | 2 | 10/10| | v2 | | | |

例如,一个记录的 v2 请求解码后的响应体为:

code
{"version": "v2", "pod": "rollout-demo-5b4b45f68f-qrntz"}

您的 Pod 名称会有所不同。请检查当前状态:

code
k get deployment rollout-demo -o json
k get pods -l app=rollout-demo -o wide

继续之前,请等待直到恰好有两个应用 Pod 存在,且都处于 Ready 状态且没有终止。Deployment 应该有两个已更新、就绪且可用的副本,且 status.observedGeneration 与 metadata.generation 匹配。

所有后续故障都会保留 APP_VERSION=v2。这使得响应中的 Pod 名称至关重要:仅凭版本无法区分旧版本和新版本。

如何诊断镜像拉取失败

第一个故障仅更改镜像引用为公共 Python 仓库中故意缺失的标签:

code
k apply -f failure-lab/manifests/image.json
k rollout status deployment/rollout-demo --timeout=10s

在记录的运行中,短时间的监控以代码 1 退出并显示以下错误:

code
error: timed out waiting for the condition

这是本次练习的预期结果。认证、连接或其他错误需要不同的诊断方法。不要将每个非零退出码都视为预期结果。

查找被阻塞的版本

检查 Deployment 及其相关对象:

code
k get deployment rollout-demo
k get deployment rollout-demo -o json
k describe deployment rollout-demo
k get replicasets -l app=rollout-demo -o wide
k get pods -l app=rollout-demo -o wide

比较 metadata.generation 与 status.observedGeneration。如果后者较小,表示控制器尚未观察到当前期望状态。在解释其 rollout 状态前请重新检查。

记录的故障快照包含三个副本:一个已更新,两个就绪,两个可用。因此,看似健康的就绪数量可能完全来自上一版本。

kubectl describe deployment 会标识 NewReplicaSet。检查该 ReplicaSet 及其拥有的未就绪 Pod。将下方两个值替换为输出中的实际名称:

code
NEW_RS='replace-with-the-new-replicaset-name'
BAD_POD='replace-with-its-unready-pod-name'
k get replicaset "$NEW_RS" -o json
k get pod "$BAD_POD" -o json
k describe pod "$BAD_POD"

在 ReplicaSet 的 metadata.ownerReferences 中,controller: true 的条目必须指定 Deployment 的 UID。在 Pod 的 owner references 中,控制器 UID 必须与该 ReplicaSet 的 UID 匹配。新 ReplicaSet 的模板除生成的 template-hash 标签外,应与 Deployment 当前模板一致。其 revision 注解应与 Deployment 的 revision 一致。

标签可以缩小搜索范围。UID 所有权链确定了哪些对象属于同一组。有关底层关系的详细信息,请参阅 owner-reference 文档。

阅读失败信息

获取该 Pod 实例的 Events:

code
POD_UID="$(
  k get pod "$BAD_POD" -o jsonpath='{.metadata.uid}'
)"
k get events --field-selector "involvedObject.uid=$POD_UID" \
  --sort-by=.metadata.creationTimestamp -o yaml

镜像 Pod 已被调度:PodScheduled=True,nodeName 已设置。其容器等待状态为 ErrImagePull,随后在重试之间出现 ImagePullBackOff。Pod 的状态始终保持为 Pending。仓库错误以以下确切文本结束:

code
docker.io/library/python:fcc-rollout-missing-20260928: not found

该信息标识了缺失的标签。镜像拉取错误也可能由仓库凭证、限流、DNS 或网络问题导致。请诊断 Event 消息而非假设所有 ImagePullBackOff 都有相同原因。

同时区分 kubectl STATUS 显示与 Pod 的状态。ImagePullBackOff 是容器等待原因,而 Pending 是状态。Pod 生命周期参考解释了这一区别。

检查哪些 Pod 处理请求

检查 Service 的 EndpointSlices 并从集群内部通过 Service 发送请求:

code
k get endpointslices -l kubernetes.io/service-name=rollout-demo -o json
k exec --pod-running-timeout=30s --request-timeout=60s http-client -- \
  python -u /app/probe.py --url http://rollout-demo:8080/ \
  --expected-version v2 --count 10 --interval 0.2

该探针检查状态和版本并记录响应 Pod 的名称。将这些名称与之前健康 ReplicaSet 的 Pod 进行比较。同时比较 EndpointSlice 的 targetRef.uid 值与它们的 UID。

在记录的运行中,所有十次响应均来自两个上一版本的 Pod。它们的 EndpointSlice 条目处于就绪状态。被阻塞的镜像 Pod 也列在其中,但其端点的 ready: false 和 serving: false 标记为 false。

新 Pod 无法变为可用状态,因此控制器保留了两个健康的旧副本。Service samples 确认这些 Pod 响应了请求。十次成功的请求揭示了样本期间发生的情况,但无法证明两次测量之间的服务连续性。

如何区分两种超时

第一个超时来自你的终端。kubectl rollout status --timeout=10s 在客户端超时后停止等待,但并未取消更新或恢复之前的配置。命令参考文档描述了这种 watch 超时行为。

第二个超时属于 Deployment 控制器。等待其截止期限原因后,检查完整条件:

code
k wait --for=jsonpath='{.status.conditions[?(@.type=="Progressing")].reason}'=ProgressDeadlineExceeded \
  deployment/rollout-demo --timeout=240s
k get deployment rollout-demo -o json

240 秒的超时限制了这个独立的等待过程。确认 Progressing 为 False 且原因为 ProgressDeadlineExceeded。kubectl wait 参考文档记录了 JSONPath 等待机制。

在原始记录中,Progressing 条件的最后更新时间从 04:57:14Z 变为 05:00:15Z(181 秒),而配置的截止期限为 180 秒。控制器的计时并不能保证精确的时钟间隔。

Available=True 保持为真,因为旧 Pod 仍然提供所需的可用性。Progressing=False 报告了停滞的更新。相反,Progressing=True 在成功发布后也可能保持为真,因此始终要检查其原因和副本数量。

Deployment 控制器报告截止期限但不会自动回滚。在截止期限快照中,缺失的镜像仍然是期望的镜像。

现在重新执行 Service 检查。截止期限之后记录的十次请求仍然来自旧 Pod。在本教程中,只有镜像案例会等待控制器的截止期限。

如何恢复健康配置

在引入另一个故障之前现在进行恢复。将 BAD_POD 保留为刚刚检查的失败 Pod:

code
k apply -f failure-lab/manifests/healthy.json
k rollout status deployment/rollout-demo --timeout=300s
k wait --for=delete "pod/$BAD_POD" --timeout=120s
k get deployment rollout-demo -o json
k get replicasets -l app=rollout-demo -o wide
k get pods -l app=rollout-demo -o wide
k get endpointslices -l kubernetes.io/service-name=rollout-demo -o json
k exec --pod-running-timeout=30s --request-timeout=60s http-client -- \
  python -u /app/probe.py --url http://rollout-demo:8080/ \
  --expected-version v2 --count 10 --interval 0.2

检查整个恢复过程,而不仅仅是 rollout 命令的成功状态。期望的规范应与 healthy.json 匹配。控制器必须已观察到该状态,副本数、已更新副本数、就绪副本数和可用副本数都应等于 2。

应恰好保留两个应用 Pod,均由当前 ReplicaSet 所有,处于就绪状态且未终止。它们的 UID 应该是就绪的 Service 端点,HTTP 样本应从这些 Pod 名称返回 v2。

在原始实验中,恢复过程复用了现有的健康 ReplicaSet 和相同的两个 Pod。它们的模板已经与恢复后的配置匹配。失败的 ReplicaSet 已扩展到零。

即使这些健康的 Pod 仍然存在,修订注解也发生了变化。这些值来自记录的运行:

| 情况 | 故障前 | 故障修订 | 恢复后 | |--------------|--------|----------|--------| | 镜像 | 3 | 4 | 5 | | 就绪性 | 5 | 6 | 6 | | 调度 | 7 | 8 | 8 |

您的数字可能有所不同。应恢复已知的健康规范,而不是依赖这些示例修订号。

如何诊断就绪检查失败

验证恢复后,应用第二个故障:

code
k apply -f failure-lab/manifests/readiness.json
k rollout status deployment/rollout-demo --timeout=10s

这仅修改了 readinessProbe.httpGet.path 字段,从 /ready 改为 /not-ready。短时监视器再次超时。

重复镜像案例中的检查流程。识别新的 ReplicaSet,将 NEW_RS 和 BAD_POD 重置为本案例的名称,并使用新 Pod 的 UID 查询 Events。这次容器已运行,但 Pod 未就绪。

以下值来自记录的 Pod 快照:

字段

值

阶段

运行中

PodScheduled

容器重启次数

0

Pod 的事件信息解释了原因:

code
就绪检查失败:HTTP 检查失败,状态码:404

应用程序没有处理 /not-ready 的端点。HTTP 检查接受 200 到 399 的响应码,因此这个 404 导致就绪检查失败。失败的就绪检查会使容器继续运行,但标记为未就绪并持续检查。详见探针文档。

记录中还短暂出现过启动时连接被拒绝的事件。这些事件单独不足以确定此故障:它们可能在健康服务器开始监听前发生。后续的 404 证明服务器已响应请求。结合单个修改字段和已知路由,可确认错误的探针路径。

就绪检查失败并未重启容器。捕获的重启次数为零。这种情况不是崩溃循环。

重复 EndpointSlice 和 Service 检查。新 Pod 在记录的 EndpointSlice 中仍显示 ready: false、serving: false 和 terminating: false。两个旧 Pod 有就绪端点,所有十次响应都来自它们。

此 Service 未启用 publishNotReadyAddresses。端点就绪和终止设置非常重要,因此不要将此观察结果推广到所有 Service 配置。EndpointSlice 文档描述了这些条件和例外情况。

关于相关背景,freeCodeCamp 的 Kubernetes 自我修复教程也探讨了就绪检查。在此案例中,失败的探针解释了为什么新的发布无法完成。

再次运行完整的恢复流程,使用此就绪 Pod 作为 BAD_POD。在继续之前验证健康的 v2。

如何诊断放置约束故障

第三个故障向 Pod 模板添加了以下选择器:

code
nodeSelector:
  rollout-lab.example.com/placement: no-matching-node

检查该选择器是否匹配任何节点,然后应用它:

code
k get nodes -l rollout-lab.example.com/placement=no-matching-node
k apply -f failure-lab/manifests/scheduling.json
k rollout status deployment/rollout-demo --timeout=10s

如果第一条命令列出节点,说明预期的未匹配选择器场景未成立。继续之前请检查标签配置。

重复所有权和事件检查,更新此案例的 NEW_RS、BAD_POD 和 POD_UID。新 Pod 处于 Pending 状态,与镜像故障时相同。但 Pending 状态涵盖等待调度和等待容器设置(包括镜像下载)两种情况。

记录的差异如下:

证据

镜像故障

调度故障

False,原因 Unschedulable

nodeName

实验室节点已分配

缺失

containerStatuses

等待容器

诊断事件

镜像拉取失败

FailedScheduling

调度事件显示:

code
0/1 个节点可用:1 个节点未满足 Pod 的节点亲和性/选择器。抢占:0/1 个节点可用:1 抢占对调度无帮助。

没有节点满足选择器,因此 Pod 从未被分配到 kubelet。镜像检索尚未开始。Kubernetes 要求节点必须匹配所有指定的 nodeSelector 标签,如节点选择文档所述。

重复检查服务。此未调度的 Pod 没有 Pod IP 或 EndpointSlice 条目。旧 Pod 提供了所有十个记录的响应。

再次应用完整的恢复序列,将此调度 Pod 作为 BAD_POD。确认健康版本 v2 和正确的服务响应。该实验测试的是选择器不匹配,而非资源耗尽或所有可能的调度失败。

如何清理

保存证据后,删除临时实验室环境:

code
bash cleanup.sh

脚本会选择 Colima 的 Docker 上下文并删除 fcc-rollout-lab,显式传递实验室的 kubeconfig。它会记录结果并检查其他 kind 集群名称是否保持不变。这将删除实验室工作负载。

证据归档保留在本地配套目录中。要再次运行教程,请在清理后使用全新解压文件;设置过程故意拒绝重复使用旧的 kubeconfig。

结论

三个不同的故障阻止了新版本的发布,而旧版本继续响应记录的请求:

停止位置

最强证据

成功故障状态样本

容器镜像检索

已调度的 Pod;缺少标签错误

两批共 20/20

就绪检查

运行中的 Pod;HTTP 404;Ready=False

节点选择

PodScheduled=False;选择器不匹配

对于下一次停滞的发布,请从 Deployment 追踪到其 ReplicaSet 和 Pod 的所有权。检查 Pod 是否已调度、容器是否已启动以及是否变为 Ready。使用其条件和事件定位故障,然后使用服务响应确定实际响应的 Pod。

最后,区分客户端监视超时与控制器进度截止时间,并在得出结论前验证完整恢复。这些结果来自单节点本地实验室和有限的 HTTP 样本,它们不能证明生产环境行为或持续可用性。

合作伙伴推荐课程

(opens in a new tab)

ADVERTISEMENT

  • Udemy Microsoft Azure DevOps:从零开始自动化应用部署 Ajeet Khan 4.2(满分5星)(2,169 条评价)4小时15分钟 中级 证书 查看课程 (opens in a new tab)
  • Udemy Microsoft Intune 培训 | MDM MAM - Azure 端点管理 Ajeet Khan 和 Firoj Khan 4.4(满分5星)(1,752 条评价)4小时7分钟 中级 证书 查看课程 (opens in a new tab)
  • Coursera 理解和结合 GKE 自动扩展策略 Google Cloud 培训 1小时30分钟 中级 证书 查看课程 (opens in a new tab)
  • Coursera Kubernetes 基础 LearnKartS 4(满分5星)(28 条评价)4周,每周1小时 初级 证书 查看课程 (opens in a new tab)

更多 Kubernetes 课程请访问 Class Central

→

code

我是Praneeth Kodumagulla,现为汇丰银行(HSBC)全栈平台工程师,同时也是一位独立研究者,专注于AI代理和自动化领域。我的工程工作涵盖云原生应用、Kubernetes、CI/CD、基础设施自动化和可观测性,重点聚焦金融服务领域可靠系统的构建。在这些工作之外,我致力于构建代理原型,并研究指令如何塑造自主系统的行为。我从平台工程师的视角探索AI领域:我们如何测试这些系统?如何理解它们的故障?如何让它们的行为更加可靠?我通过实践教程、可复现的实验和实用代码分享我的学习成果。

如果这篇文章对你有帮助,请分享它。

免费学习编程。freeCodeCamp的开源课程已帮助超过40,000人成为开发者。立即开始学习