Files
pikasTech-unidesk/.agents/skills/unidesk-cicd/references/incident-recovery.md
T
2026-07-21 07:26:51 +02:00

20 KiB
Raw Blame History

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. 读取节点摘要:

    bun scripts/cli.ts cicd status --node <NODE>
    
  2. 一次计算选中 consumer 的回退窗口与同 source authority 共同指纹:

    bun scripts/cli.ts platform-infra pipelines-as-code diagnose-regression \
      --target <NODE> \
      --consumer <CONSUMER>
    
    • ok 表示只读采集是否成功;
    • incident 表示最新终态是否处于失败连续段;
    • window.lastKnownGoodwindow.firstFailed 给出 source commit 边界;
    • commonFailure 只在共享 repositoryRef 的 lanes 内聚类;
    • state=read-failed 时先处理读取可见性,不得按健康解释;
    • 窗口不足时只执行输出中的有界 expand-window
  3. 仅在需要精确 TaskRun 或日志证据时,对首次失败执行输出中的 debug-first-failure

    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 合并后,先执行 release plan;范围准确时手动 trigger,再观察新 PaC 事件:
    • 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 时, 对象会在 specstatus 双份放大;
    • 运行期间的状态更新会重复写入大型对象,进一步放大 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 执行:

      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 迁移。

k3s VXLAN panic 后的 credential/datastore 启动闭锁

  • 出现以下连续指纹时,必须拆成两个故障阶段:
    • k3s 先在 flannel VXLAN 设备重建路径 panic,或因 inotify_init: too many open files 等 kubelet 启动错误异常退出;
    • systemd 随后持续自动重启,但每次都报 server/cred/<file> newer than datastore and could cause a cluster outage
    • 后一条 fatal 是重启闭锁,不等于最初触发 k3s 退出的根因。
  • 快速恢复前必须保存:
    • 首次退出前后的完整 panic 堆栈;
    • fatal 明确点名的单个 credential 文件路径;
    • 该文件的大小、权限、mtime、birth time 和 SHA-256
    • k3s unit 状态及 API、node、Service endpoint 当前状态。
    • 若存在 inotify 错误,同时记录 fs.inotify.max_user_instancesfs.inotify.max_user_watches、当前 inotify instance 数和遗留容器进程数。
  • 只有用户已授权恢复且 fatal 明确点名单个文件时,才允许:
    • server/cred 目录之外创建本次事故独立、权限为 0700 的恢复目录;
    • 将被点名文件移动到该目录,保留原名,不删除文件;
    • 禁止移动整个 credential 目录、删除 datastore、改写 token 或处理未点名文件;
    • 重启 k3s,让它从 datastore 重新生成该文件。
  • 重启后必须比对重新生成文件与备份的大小、权限和 SHA-256:
    • 一致时记录为“移除磁盘时间戳闭锁”,不能宣称凭据内容已修复;
    • 不一致时停止扩大操作,保留两份文件并进入 Secret/credential 专项调查。
  • inotify instance 已接近上限时,可在用户已授权的紧急恢复中临时提高 fs.inotify.max_user_instances
    • 先记录原值和当前使用量;
    • 只提高 instance 上限,不修改 fs.file-max 或广泛终止未知进程;
    • 临时值必须登记到 issue,后续由 owning YAML 决定持久值并调查 instance 泄漏;
    • 临时提高上限不能替代遗留 shim、runtime 生命周期或资源泄漏修复。
  • 验收必须覆盖:
    • k3s active 且不再 restart loop
    • node Ready、UniDesk workload Ready、backend dbReady=true
    • frontend/provider ingress 健康;
    • 事故前失败的原始 trans route 恢复。
  • 后续 issue 必须分别追踪:
    • flannel/k3s panic 的触发条件、版本和升级或规避方案;
    • 被点名 credential 文件的写入者与 source of truth
    • 受控 CLI 对该 fatal 的诊断、可恢复备份和验收能力。

PaC/Tekton controller 假健康与 Pending 队列恢复

  • 适用指纹:
    • PaC watcher 或 Tekton controller Deployment 显示 Ready,但日志停止推进;
    • 多个 consumer 的 PipelineRun 长期停在 PipelineRunPending,没有 TaskRun
    • watcher 访问集群内 Gitea Service 超时,而 Gitea Pod、Service endpoint 和同节点探针正常;
    • watcher 位于已 cordon 或 Pod 网段异常的 worker,Gitea 位于控制面节点。
    • PipelineRun 已 Running,但 step 只完成 entrypoint 解码并持续等待 /tekton/downward/ready,同时 Tekton controller 日志早于该 TaskRun 停止。
  • 先区分公共控制面与具体 Provider:
    • watcher、Tekton controller、Gitea、Service 网络和共享 PaC 队列属于公共 CI/CD;
    • AgentRun runner、HWLAB node、业务 Deployment 和 Provider 数据通道不在恢复范围;
    • 用户只授权公共运维时,不得借机重启、迁移或修改具体 Provider。
  • 快速恢复必须按依赖顺序执行:
    • 先确认 node、Gitea Pod、Service endpoint 和 watcher placement
    • 公共无状态 controller 卡在不可调度且跨节点网络失效的 worker 时, 只删除该精确 Pod,让 Deployment 在 YAML 允许的健康节点重建;
    • watcher 重新取得 leader 且能访问 Gitea 后,再处理 Pending 队列;
    • Tekton controller 未观察新 TaskRun 时,只重启该精确 Deployment 让现有 PipelineRun 和 TaskRun 由 controller 重新调和;
    • 不得先批量删除 PipelineRun,也不得用业务重跑掩盖公共依赖故障。
  • watcher 停机后可能只重建内存队列,无法补发已经完成的前序事件:
    • execution-order、source commit、spec.status、TaskRun 数量和终态条件 确认卡住的事件组;
    • 当前权威 source commit 只从 GitHub source branch 获取;
    • 仅取消同一 Repository 中仍为 Pending 且已被权威 commit 取代的运行;
    • 使用 Tekton 合法的 spec.status: Cancelled,保留 PipelineRun 历史对象;
    • 保护权威 commit 的完整执行组、active run、latest success 和审计证据;
    • 没有明确公共运维授权时,只报告候选,不执行队列 mutation。
  • 临时恢复不能作为完成证据:
    • Pod 重建、精确 Pending 取消和 controller 重新选主只证明公共通道恢复;
    • 长期调度归属必须回写 owning YAML,避免公共 controller 再落到隔离 worker
    • 最终必须由新的正常 GitHub source event 自动经过 Gitea、PaC、Tekton 和 public-edge reconcile
    • 公网入口验收同时要求受控 public-edge status 无 unresolved/probe failure、 desired/current fingerprint 一致、provenance source commit 对齐, 并以绕过本机代理的公网 IP HTTPS probe 确认 TLS 和 readiness。

长期防复发

  • 从 renderer/source 消除重复内联:
    • 同构多服务构建优先使用 Tekton matrix,只在 PipelineRun 中保留一份 task 实现;
    • 跨 Pipeline 共享的稳定职责使用 reusable Task,并确保 Task reconciler 是正式 owner
    • 没有 Task reconciler 时不得以手工 apply 维持 TaskRef,改用 matrix 或 source 中的原生脚本文件;
    • 禁止用压缩字符串、编码载荷、第二 Pipeline 或 fallback 隐藏对象膨胀。
  • 为 source artifact 和 renderer 增加结构验证:
    • 统计对象总大小、specstatus.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 合并后的 plan 与手动 webhook 事件验收:
    • 新 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 或自动交付失败。
  • 直接根因:
    • 80ca187cbootstrap_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 上同一条有界日志读取实测需要 18s38.5s,不能作为 status 同步必需项。
  • 公共面修复:
    • status 只读取一次 TaskRun JSON,并在 task summary 与 artifact terminal record 间复用;
    • status 使用 taskrun-status-only,不再同步读取 Pod 日志;
    • history collector 收敛为一次 selector 聚合,保留失败日志下钻;
    • 目标侧输出无敏感值的 pac-status-progressCLI 在 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 #225625000ms 总预算按 /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.7s11.3s 完成且保持 ok=true
  • 验收结论:
    • status --target NC01 --consumer selfmedia-nc01 --json 稳定在 YAML 25000ms 预算内返回合法 JSON
    • 输出 ok=trueremoteStage=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 入口负责慢日志证据。