264 lines
15 KiB
Markdown
264 lines
15 KiB
Markdown
# 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 入口负责慢日志证据。
|