原来是我的YAML — 三起故障实验笔记
目标
在真实的 k3s 中制造 selector 不匹配、错误的 readiness 路径和进程退出, 保存观测依据后以最小改动恢复。本实验面向了解 kubectl get/apply 与 YAML 基本结构的中级学习者,预计用时 50 分钟。
为什么重要
Running、Ready 和用户请求成功是彼此不同的事实。即使存在正常 Pod, Service selector 或端口错误时,客户仍会遇到失败。反之,进程可能仍然存活, 却因不满足就绪条件而被排除在流量目标之外。每次只修改一个字段并比较前后状态, 就能说明原因,而不是反复重启。区分三个事件后返回已确认的正常 revision, 即为本实验的范围。
安全范围
仅使用临时 VM 内的 API https://127.0.0.1:6443과 labhub-mystery 命名空间。 不要在生产集群或其他学员的环境中执行。所提供应用使用单个 replica 和 Recreate,避免原有正常 Pod 掩盖故障。这并不表示它是推荐的零停机运行配置。 应用使用 image digest 固定。会话结束后,VM 和观测文件都会消失。如有需要, 请在到期前延长时间并另行保存观测。观测文件是可编辑的实验笔记,并非不可伪造的证据。
步骤
- 使用 kubectl apply 应用所提供的 /opt/fixtures/k8s-mystery-app.json。查询 labhub-mystery 命名空间中的 parcel Deployment 和 Service,确认 Pod 与 Service 两侧的 8080 端口 / 路径都返回正文 labhub-mystery-v1,并将正常状态记录为 baseline。记录命令:
python3 /opt/fixtures/k8s_mystery.py observe baseline /root/k8s-mystery/01-baseline.json。 - 只把 parcel Service 的 selector 改为 app=missing,保持 Pod 标签和 Deployment 不变。此时 Pod 为 Ready 且可直接响应,但 Service 选中的 endpoint 为 0,请求失败;将该状态记录为 selector-broken。记录命令:
python3 /opt/fixtures/k8s_mystery.py observe selector-broken /root/k8s-mystery/02-selector.json。 - 将 Service selector 恢复为 app=parcel。确认有 1 个选中的 Ready endpoint,且 Pod 与 Service 两侧 HTTP 都正常,并记录为 selector-restored。不要删除并重新创建 Deployment。记录命令:
python3 /opt/fixtures/k8s_mystery.py observe selector-restored /root/k8s-mystery/03-selector-fixed.json。 - 只把 Deployment 中 parcel 容器的 readinessProbe.httpGet.path 改为 /missing。新 Pod 处于 Running,可直接响应 /,但 Ready=false、重启次数为 0,当前 Pod 有 404 事件,Ready endpoint 为 0;将该状态记录为 readiness-broken。记录命令:
python3 /opt/fixtures/k8s_mystery.py observe readiness-broken /root/k8s-mystery/04-readiness.json。 - 将同一个 readiness 路径恢复为 /。不要删除 probe,也不要替换成始终成功的 exec。确认 Pod 与 Service 的 HTTP 正常且有 1 个 Ready endpoint,并记录为 readiness-restored。记录命令:
python3 /opt/fixtures/k8s_mystery.py observe readiness-restored /root/k8s-mystery/05-readiness-fixed.json。 - 查询正常的 Deployment revision,并保存到 Deployment annotation labhub.io/known-good-revision。随后把 parcel 的 command 替换为 ["sh","-ec","echo intentional-exit-17 >&2; exit 17"]。确认 CrashLoopBackOff、上一次退出码为 17、重启次数至少为 1,且上一份日志包含 intentional-exit-17,并记录为 crash。记录命令:
python3 /opt/fixtures/k8s_mystery.py observe crash /root/k8s-mystery/06-crash.json。 - 查看 rollout history 和保存的 labhub.io/known-good-revision,然后 rollout undo 到该 revision。在保留现有 Deployment 的情况下,确认正常 command、probe /、Service selector app=parcel、targetPort 8080 以及两侧 HTTP 都已恢复,并记录为 rollback。记录命令:
python3 /opt/fixtures/k8s_mystery.py observe rollback /root/k8s-mystery/07-rollback.json。 - 保留前 7 个观测文件,并在 /root/k8s-mystery/report.json 中为 selector、readiness、crash 事件分别关联 cause、prevention、evidence。原因和预防代码见下方参考,evidence 使用发生故障时的文件名。当前 Service 也必须正常。最后点击完整评分,同时检查历史观测与当前恢复状态。
参考
观测工具的 observe 只执行查询和文件保存,不会代替你注入或恢复请求的故障。 如果 45 秒内未看到条件,请检查值和事件后再次观测。前 7 步的评分读取已保存记录, 因此恢复后也不会取消过去步骤。最终评分会两次确认同一 Deployment 的记录关联以及 当前 Pod、Service 的 HTTP。
- selector 事件:service-selector / compare-selector-labels —— 预先比较 selector 与 Pod 标签。
- probe 事件:readiness-path / probe-real-endpoint —— 检查实际提供的健康检查路径。
- 退出事件:process-command / smoke-test-command —— 对容器启动命令进行小型执行测试。
- 使用
kubectl -n labhub-mystery get pods,svc,endpointslices -o wide分别查看目标和状态。 - 将
kubectl -n labhub-mystery describe pod <이름>的事件与当前 Pod UID 关联。 kubectl -n labhub-mystery logs <이름> --previous是上一个容器的日志。- 删除 probe、重新创建 Deployment、凭记忆使用某个数字回滚,都是绕开问题契约的行为。
包裹正常送达的基线
使用 kubectl apply 应用所提供的 /opt/fixtures/k8s-mystery-app.json。查询 labhub-mystery 命名空间中的 parcel Deployment 和 Service,确认 Pod 与 Service 两侧的 8080 端口 / 路径都返回正文 labhub-mystery-v1,并将正常状态记录为 baseline。记录命令:python3 /opt/fixtures/k8s_mystery.py observe baseline /root/k8s-mystery/01-baseline.json。
不要只看 Running 标记,要区分 Ready、EndpointSlice 与实际 HTTP。观测工具不会创建期望状态。
配送车辆还活着,却没有目的地
只把 parcel Service 的 selector 改为 app=missing,保持 Pod 标签和 Deployment 不变。此时 Pod 为 Ready 且可直接响应,但 Service 选中的 endpoint 为 0,请求失败;将该状态记录为 selector-broken。记录命令:python3 /opt/fixtures/k8s_mystery.py observe selector-broken /root/k8s-mystery/02-selector.json。
比较 Service 的 selector 与 Pod 的 labels。更改选择条件并不会同时更改现有 Pod 标签。
只修正配送名单
将 Service selector 恢复为 app=parcel。确认有 1 个选中的 Ready endpoint,且 Pod 与 Service 两侧 HTTP 都正常,并记录为 selector-restored。不要删除并重新创建 Deployment。记录命令:python3 /opt/fixtures/k8s_mystery.py observe selector-restored /root/k8s-mystery/03-selector-fixed.json。
有重启应用的依据吗?只恢复被错误修改的对象字段,并通过实际请求确认效果。
因不存在的体检室而停止配送
只把 Deployment 中 parcel 容器的 readinessProbe.httpGet.path 改为 /missing。新 Pod 处于 Running,可直接响应 /,但 Ready=false、重启次数为 0,当前 Pod 有 404 事件,Ready endpoint 为 0;将该状态记录为 readiness-broken。记录命令:python3 /opt/fixtures/k8s_mystery.py observe readiness-broken /root/k8s-mystery/04-readiness.json。
修改的是 readiness 路径,而不是 liveness。还要检查事件目标 UID 是否与当前 Pod 相同。
让体检契约符合实际路径
将同一个 readiness 路径恢复为 /。不要删除 probe,也不要替换成始终成功的 exec。确认 Pod 与 Service 的 HTTP 正常且有 1 个 Ready endpoint,并记录为 readiness-restored。记录命令:python3 /opt/fixtures/k8s_mystery.py observe readiness-restored /root/k8s-mystery/05-readiness-fixed.json。
让实际请求与 probe 表达相同的健康状态。本案例是路径拼写错误,并非忽略真实依赖故障的案例。
配送员留下 17 号便签后下班
查询正常的 Deployment revision,并保存到 Deployment annotation labhub.io/known-good-revision。随后把 parcel 的 command 替换为 ["sh","-ec","echo intentional-exit-17 >&2; exit 17"]。确认 CrashLoopBackOff、上一次退出码为 17、重启次数至少为 1,且上一份日志包含 intentional-exit-17,并记录为 crash。记录命令:python3 /opt/fixtures/k8s_mystery.py observe crash /root/k8s-mystery/06-crash.json。
不要把 revision 记成 1 之类的固定数字。应在确认正常后立即保存,并通过 logs --previous 读取上一次退出的依据。
返回已确认的 revision,而不是依靠记忆
查看 rollout history 和保存的 labhub.io/known-good-revision,然后 rollout undo 到该 revision。在保留现有 Deployment 的情况下,确认正常 command、probe /、Service selector app=parcel、targetPort 8080 以及两侧 HTTP 都已恢复,并记录为 rollback。记录命令:python3 /opt/fixtures/k8s_mystery.py observe rollback /root/k8s-mystery/07-rollback.json。
Deployment 回滚不会修复 Service。请同时确认历史编号和实际请求。
依据证据而非绿灯结束事件
保留前 7 个观测文件,并在 /root/k8s-mystery/report.json 中为 selector、readiness、crash 事件分别关联 cause、prevention、evidence。原因和预防代码见下方参考,evidence 使用发生故障时的文件名。当前 Service 也必须正常。最后点击完整评分,同时检查历史观测与当前恢复状态。
区分 selector、probe 路径与进程命令。如果当前 Service 失败,只修改 JSON 也无法通过。