Files
pikasTech-unidesk/.agents/skills/unidesk-cicd/references/incident-recovery.md
T
2026-07-18 17:52:32 +02:00

264 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CI/CD 生产事故恢复
## 适用边界
- 本流程适用于 PaC、Tekton、GitOps、Argo、git-mirror 或 runtime rollout 连续失败,以及多个 consumer 出现共同失败指纹的生产事故。
- 本流程不扩大写入授权:
- consumer 私有配置可以在原任务范围内修复;
- CI/CD 公共服务面默认只读;
- 修改公共服务面必须取得用户明确授权。
- 节点、consumer、PipelineRun、仓库、分支和提交均为变化项,必须从 owning YAML 与受控 CLI 解析,禁止复制本案例取值。
## 公共服务面与 consumer 私有面
- 公共服务面包括:
- 共享 PaC controller/release 与 admission
- Gitea webhook bridge、durable inbox 和共享 source authority
- 共享 git-mirror read/write Deployment、Service、PVC 和仓库 ref
- 共享 Tekton/Argo bootstrap、renderer、模板和 native helper
- 公共 edge、FRPC、Caddy、Secret 和 RBAC。
- consumer 私有面只包括:
- owning YAML 中由 consumer id 唯一定位的独立配置片段;
- 独立 lane、namespace、Pipeline、source artifact 和 runtime 参数;
- consumer 自己仓库内由 owning YAML 声明的 `.tekton` 制品。
- 任一对象存在多个 consumer owner、共享 `repositoryRef`、共享 PVC、共享 Deployment 或共享网络入口时,按公共服务面处理。
## 最短定位路径
按以下顺序执行,前三步足以完成运行面分层,不再从裸 Kubernetes 大对象开始试探:
1. 读取节点摘要:
```bash
bun scripts/cli.ts cicd status --node <NODE>
```
2. 一次计算选中 consumer 的回退窗口与同 source authority 共同指纹:
```bash
bun scripts/cli.ts platform-infra pipelines-as-code diagnose-regression \
--target <NODE> \
--consumer <CONSUMER>
```
- `ok` 表示只读采集是否成功;
- `incident` 表示最新终态是否处于失败连续段;
- `window.lastKnownGood` 与 `window.firstFailed` 给出 source commit 边界;
- `commonFailure` 只在共享 `repositoryRef` 的 lanes 内聚类;
- `state=read-failed` 时先处理读取可见性,不得按健康解释;
- 窗口不足时只执行输出中的有界 `expand-window`。
3. 仅在需要精确 TaskRun 或日志证据时,对首次失败执行输出中的 `debug-first-failure`
```bash
bun scripts/cli.ts platform-infra pipelines-as-code debug-step \
--target <NODE> \
--consumer <CONSUMER> \
--id <PIPELINERUN>
```
4. 使用命令输出的 `sourceRange.compareUrl` 比较最后成功 source commit 与首次失败 source commit
- 只检查两者之间的提交;
- 优先寻找与共同失败指纹直接相关的最近变更;
- 记录致因文件、提交、影响 consumer 和可回退性;
- 不在此阶段设计前向重构。
## 回退决策
- 满足以下条件时直接选择回退:
- 失败从某个提交后稳定出现;
- 多条 PipelineRun 具有相同首个断点;
- 变更与断点存在直接因果关系;
- 回退不造成不可逆数据或外部状态损坏。
- 回退 PR 只包含致因变更的精确反向语义:
- 不夹带前向修复;
- 不夹带重构;
- 不夹带其他 consumer 或公共服务清理;
- 不夹带 unrelated PR。
- 致因位于公共服务面但用户没有明确授权时:
- 停止写入;
- 提供致因提交和精确回退候选;
- 说明受影响 consumer 与预计恢复证据;
- 等待用户授权公共服务面回退。
## 恢复验收
- 回退 PR 合并后,只观察该合并产生的新正常自动事件:
- webhook/Gitea delivery 已提交;
- PaC 外层 PipelineRun 成功;
- Tekton terminal roles 成功;
- GitOps revision 与 source commit 对齐;
- Argo `Synced/Healthy`
- runtime ready 且 `/health` 成功。
- 不得通过人工 mirror sync、直接 Gitea push、人工 PipelineRun、Argo sync、bootstrap、apply 或运行面热补补齐验收。
- 回退事件恢复健康后,才允许从最新健康基线创建独立前向修复 PR。
## Tekton 大对象与 Kine/SQLite 控制面退化
### 现象分层
- 先区分自动链传播延迟与节点控制面事故:
- PipelineRun 已成功并生成 GitOps commit、Argo 仍停在旧 revision
但 node、API、lease 和 runtime workload 健康时,按传播延迟处理;
- 只读记录 PipelineRun completion、GitOps commit、Argo reconcile/operation、
Deployment rollout 和 runtime ready 时间,等待自动链自行收敛;
- host 无法解析 `*.svc.cluster.local` 只说明查询平面错误,
不能证明集群内 git mirror 或 Argo repo-server 不可用;
- node 进入 `NotReady`、Service endpoint 消失,并同时出现 API handler timeout、
node lease 延迟、kubelet housekeeping timeout 或持续 Kine Slow SQL 时,
才按节点控制面事故处理。
- 不得只凭 Pod Pending、Argo `Synced/Healthy` 或单条 Slow SQL 下结论:
- `Synced/Healthy` 必须同时核对实际 revision,避免把旧 revision 健康误判为新交付完成;
- 周期性 compaction 或孤立 Slow SQL 只记录为余压;
- 同窗口出现 lease、API、kubelet 和业务 endpoint 退化,才构成同类故障段。
### 根因判定
- 对疑似大型 PipelineRun,在目标 k8s 内计算短摘要:
- 对象总字节数;
- `spec` 字节数;
- `status.pipelineSpec` 字节数;
- Pipeline task、matrix TaskRun 和普通 TaskRun 数量;
- 最大字段及其 renderer/source 路径。
- Tekton 会把解析后的 Pipeline spec 持久化到 `status.pipelineSpec`
- renderer 为每个服务重复展开相同大型 inline `taskSpec.steps[].script` 时,
对象会在 `spec` 与 `status` 双份放大;
- 运行期间的状态更新会重复写入大型对象,进一步放大 Kine/SQLite 单写者压力;
- TaskRun results 只有在字节证据成立时才可判为主因,禁止按字段名称猜测。
- 将对象证据与同窗口控制面证据关联:
- Kine compaction 耗时和 Slow SQL
- API handler timeout 与 stale resource-version
- node/peer lease 写入延迟;
- kubelet housekeeping 和 pod lifecycle 延迟;
- k3s cgroup task、内存、swap 与数据库/WAL 压力。
- 对象大小、耗时和留存阈值必须由 owning YAML 控制:
- 状态与 OTel 只输出 `blocking=false` warning
- 禁止把可见性阈值升级成阻断 PipelineRun、GitOps、Argo 或业务 `/health` 的门禁。
### 快速恢复
按以下顺序恢复用户入口,禁止跳过取证后直接重启:
1. 保存最小证据:
- 执行 `cicd status --node <NODE>`
- 对选中 consumer 执行 `pipelines-as-code status` 和 id-specific `history`
- 记录 node condition、受影响 Service endpoint、异常 Pod owner、
Kine/API/lease/kubelet 的有界日志计数;
- 所有 Kubernetes 大对象在目标侧计算大小和摘要,不把完整 JSON 拉回本机。
2. 判断是否仍可由 Kubernetes 自行调和:
- API 与 kubelet 可用时,先等待 Deployment/StatefulSet controller 创建替代 Pod
- 只处理已被驱逐、终态、卡在删除态且已不属于 Service endpoint 的旧 Pod
- 删除前确认 owner、替代副本和 endpoint,禁止 broad selector、全 namespace 强删或触碰有状态单例。
3. 只在节点控制面已退化且 Pod 级调和无效时重启 k3s:
- 该动作属于 `$unidesk-daddev` P2 紧急恢复,必须有用户授权;
- 单节点目标通过 host 受控 route 执行:
```bash
trans <NODE>:/root/unidesk systemctl restart k3s
trans <NODE>:k3s kubectl get nodes
```
- 禁止使用 Compose 替代 YAML 选中的 k8s 运行面;
- API 恢复后立即复查 node Ready、controller 调和和业务 endpoint
healthy 后不再扰动 backend-core 或其他已恢复 workload。
4. 控制面稳定后释放终态 Tekton 压力:
- 先对 owning cleanup 入口执行 dry-run
- 再以 `--confirm --wait` 有界删除符合 YAML retention 的终态 PipelineRun
- 必须保留 active run、最新成功证据、业务 PVC、Secret、runtime workload 和 GitOps desired state。
5. 恢复只以用户原入口结束:
- node `Ready`
- workload ready 且无持续重启;
- Service endpoint 存在;
- backend/provider/public `/health` 返回成功;
- 恢复动作、根因假设和待持久化修复进入 issue 与 TaskTree;遗留 MDTODO 按 `$unidesk-tasktree` 迁移。
### 长期防复发
- 从 renderer/source 消除重复内联:
- 同构多服务构建优先使用 Tekton matrix,只在 PipelineRun 中保留一份 task 实现;
- 跨 Pipeline 共享的稳定职责使用 reusable Task,并确保 Task reconciler 是正式 owner
- 没有 Task reconciler 时不得以手工 apply 维持 TaskRef,改用 matrix 或 source 中的原生脚本文件;
- 禁止用压缩字符串、编码载荷、第二 Pipeline 或 fallback 隐藏对象膨胀。
- 为 source artifact 和 renderer 增加结构验证:
- 统计对象总大小、`spec` 和 `status.pipelineSpec`
- 检查重复 inline script、task 数量和 matrix 展开语义;
- 保证每个服务仍有独立 TaskRun、结果收集和失败归属;
- 结构检查只形成有界 warning,真正缺少渲染必需输入时才 fail-closed。
- 保持 YAML-owned retention
- 每个 consumer 声明终态 PipelineRun/TaskRun 留存策略;
- cleanup 必须保护 active、latest success 和审计证据;
- 定期清理降低对象 churn,但不能替代 renderer 减载。
- 补齐非阻塞可观测性:
- status/history 输出对象字节、Kine/WAL、compaction、API、lease、kubelet 和 node pressure 摘要;
- CI/CD OTel span/event 记录 source commit、PipelineRun、GitOps commit、Argo revision、
runtime digest、阶段时间和对象压力 warning;
- exporter 或 provenance 漂移只标记 evidence gap,禁止改变业务成功终态。
- 长效修复必须由正常 PR 自动事件验收:
- 新 PipelineRun 成功且对象显著减小;
- GitOps commit、Argo revision、runtime digest 与 source identity 对齐;
- PipelineRun 执行和后续 compaction 窗口内 node、API、lease、kubelet 与业务 endpoint 保持健康;
- 不得用 k3s restart、Pod 删除、人工 PipelineRun、Argo sync 或 mirror flush 作为最终修复证据。
## 案例:删除工作目录引发 `getcwd()` 失败
- 共同指纹:多个 PaC consumer 的后续 step 在业务逻辑执行前统一报 `getcwd()`。
- 致因:前一步删除 `/workspace/source`,后续步骤仍把该目录作为工作目录。
- 正确动作:精确回退致因提交,先恢复自动链,再重做共享 Repository 多分支与 release 支持。
- 错误动作:
- 在故障基线上继续叠加共享架构改造;
- 同时维护多个不完整 PR
- 误合并无关 PR
- 用人工补链制造“已恢复”证据。
- 长期结论:
- 先恢复最后已知健康状态;
- 再做独立前向修复;
- 每个 PR 必须单一职责、可独立验证、可独立回退。
## 案例:PaC status 公共 renderer 生成非法 JSON
- 现象分层:
- SelfMedia PR `#82` 合并后 `14s` 自动触发 PipelineRun
- PipelineRun 对 source commit `8c03bcca554457315bc16bad01e5a96764276472` 执行成功,耗时 `256s`
- Argo 与 runtime 正常,`pac-read-parse-failure` 只发生在共享 status 观察器;
- 因此这是公共观察面事故,不是 SelfMedia 私有 YAML 或自动交付失败。
- 直接根因:
- `80ca187c` 中 `bootstrap_ready` 的赋值原本正确;
- `1f3283ff` 在回退 admission 业务阻断时删除该赋值;
- `2b385f08` 恢复 consumer bootstrap fail-closed 引用,却没有恢复赋值;
- shell 最终拼出非法 `"ok":,`,影响所有复用共享 renderer 的 PaC consumer。
- 回退决策:
- 不整体回退 `2b385f08`,因为整体回退会再次丢失正确的 bootstrap fail-closed 语义;
- 只从最后已知正确实现恢复 `bootstrap_ready` 赋值,并实际执行三种 bootstrap fixture 后 `JSON.parse` 输出;
- 这是对致因遗漏的最小语义回退,不是在错误基础上继续扩展 admission 设计。
- 恢复期间暴露的第二断点:
- status 在 `25000ms` YAML 预算内超时,阶段标记锁定在 `artifact-evidence`
- 共享 collector 对同一 TaskRun 重复读取,并为成功 run 同步串行执行 `kubectl logs`
- NC01 上同一条有界日志读取实测需要 `18s` 到 `38.5s`,不能作为 status 同步必需项。
- 公共面修复:
- status 只读取一次 TaskRun JSON,并在 task summary 与 artifact terminal record 间复用;
- status 使用 `taskrun-status-only`,不再同步读取 Pod 日志;
- history collector 收敛为一次 selector 聚合,保留失败日志下钻;
- 目标侧输出无敏感值的 `pac-status-progress`CLI 在 timeout observation 中显示 `remoteStage`
- PipelineRun 成功、Argo `Synced/Healthy` 且 runtime ready 时,缺少同步日志证据降级为 `pac-artifact-log-evidence-deferred` 非阻断告警。
- 首次合并后的补充恢复:
- 首个修复 PR 合并后,canary 又在 `remoteStage=argo` 间歇超过 `25000ms`
- 给 PipelineRun 查询直接设置过短的单次上限会返回空列表并制造 `pipeline-missing` false negative,不能以“命令不再超时”冒充恢复;
- 最终按 Repository CR label 在服务端筛选 PipelineRun,同时保留 consumer classifier 区分共享 repository lanes
- PR `#2256` 把 `25000ms` 总预算按 `/8000` 派生为 `3s` 单次上限,合并后再次把超时的 PipelineRun 查询改写为空数组并误报 `pipeline-missing`
- 同时,admission provenance 为 default ServiceAccount 串行执行十次 `kubectl auth can-i`,累计占用总预算;
- 恢复时把十次权限探测收敛为一次 `kubectl auth can-i --list`,并在目标侧聚合 PipelineRun/TaskRun mutation verbs
- 精确回退 `/8000` 错误切分,单次读取上限恢复为 YAML 总预算本身,整次 status 仍由调用侧 `25000ms` 硬上限约束;
- PipelineRun 读取失败不再改写为空数组,而是返回 `pac-pipelinerun-read-failed`,明确表示缺失尚未成立;
- Argo 原始 JSON 仍只读一次并由 Argo 摘要与 runtime 共同复用;阶段事件继续输出 elapsed 与 step timeout
- 最终补丁连续两轮 canary 分别在 `15.7s` 与 `11.3s` 完成且保持 `ok=true`。
- 验收结论:
- 原 `status --target NC01 --consumer selfmedia-nc01 --json` 稳定在 YAML `25000ms` 预算内返回合法 JSON
- 输出 `ok=true`、`remoteStage=complete`、PipelineRun `Succeeded`、Argo `Synced/Healthy`、runtime `1/1`
- 全程未修改 SelfMedia 私有 YAML,未人工执行 PipelineRun、mirror、trigger、sync、flush、Argo sync 或 runtime patch。
- 长期结论:
- 先区分 delivery failure 与 observer failure
- 修复共享 renderer 必须取得公共服务面授权;
- 先恢复最后已知正确语义,再删除重复、无界和同步日志读取;
- 总预算不得按假定调用数切成会制造 false negative 的局部超时,读取失败也不得伪装成对象不存在;
- status 负责快速结构化摘要,id-specific 入口负责慢日志证据。