From 26c77e6091688c04fa9fdd2e4a674c2e3e32135e Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 22 Oct 2026 16:24:27 +0800 Subject: [PATCH 1/6] =?UTF-8?q?docs:=20=E8=A1=A5=20CI=20workflow=20?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E8=A7=82=E6=B5=8B=E7=9A=84=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 接入指南 5.1 原来只有标题。补上方案:结果必须从 GitHub Actions API 取 (ci_runner_job 是 Pushgateway 全量替换、pod-history-api 是 Pod 生命周期、 EphemeralRunner.status 没有 conclusion),给出端点、水位线、指标与告警设计、 以及 Deployment + /metrics 的部署形态;5.2 说明 job 日志在 GitHub 侧、 集群 LTS 只到 runner Pod 这一层。 Co-Authored-By: Claude Code --- ...45\345\205\245\346\214\207\345\215\227.md" | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" index e69668e..0e76c38 100644 --- "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" +++ "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" @@ -516,8 +516,138 @@ Service 端口名命中白名单即可,无需写 job。以 oauth2-proxy 为例 ### 5.1 指标接入 +本节是方案,尚未落地(当前 CI workflow 只有 runner 维度的在跑信息,见[CI 可观测现状 第 8 章](CI可观测现状.md))。 + +#### 5.1.1 为什么结果必须从 GitHub 取 + +现在能拿到的东西都回答不了「这次 workflow 成功了没有」: + +1. **`ci_runner_job`** 是 Pushgateway 上的 info 型指标,runner 一结束就被下一次 push 全量替换掉——它只描述"此刻谁在跑",不留下任何结果。 +2. **pod-history-api** 有 `status`(`released` / `rejected` / `expired`)、`duration`、`wait_duration`,但那是 **Pod 生命周期**,不是 job 结论:runner Pod 正常退出不代表 job 成功;而且它只有查询 API,没有 Prometheus 指标。 +3. **定制 ARC 的 `EphemeralRunner.status`** 有 `jobRepositoryName` / `workflowRunId` / `jobWorkflowRef` / `jobDisplayName`,**没有 conclusion**——controller 不关心 job 成功与否。 + +结论只在 GitHub 侧:`run` 和 `job` 都有 `conclusion` 字段。所以结果观测的源是 GitHub Actions API(或 webhook),集群侧数据只做补充。 + +#### 5.1.2 数据来源 + +只读 GitHub Actions REST API,不动仓库、不装 App 到 workflow: + +| 端点 | 取什么 | 频率 | +| --- | --- | --- | +| `/repos/{owner}/{repo}/actions/runs?status=completed&per_page=100` | 已结束的 run:`id`、`name`、`conclusion`、`run_attempt`、`run_started_at`、`updated_at`、`event` | 60s | +| `/repos/{owner}/{repo}/actions/runs/{run_id}/jobs?per_page=100` | 该 run 下每个 job:`name`、`conclusion`、`started_at`、`completed_at`、`labels`、`runner_name` | 只对新增的 run | +| `/repos/{owner}/{repo}/actions/runs?status=in_progress` | 当前在跑的 run 数 | 60s | + +五条约定: + +1. **认证用 GitHub App 或服务账号的 fine-grained PAT**,权限只要 `Actions: read` + `Metadata: read`;token 进 Secret,不进配置文件。401/403 必须单独告警——它会让采集**静默归零**。 + 当前 21 个仓库分属 **12 个 owner**(含个人仓库 `hiyouga/LlamaFactory`):fine-grained PAT 只覆盖 token 所有者有权限的仓库, + 跨 owner 要么用 GitHub App 逐个 org 安装,要么用有全量权限的服务账号,别指望一个 PAT 通吃。 +2. **水位线是 `(run_id, run_attempt)` 而不是 `run_id`**:re-run 会让 `run_attempt` 递增,只按 `run_id` 去重会把重跑的结果丢掉。 +3. 每个仓库独立超时,**单仓失败不能拖垮整轮**,失败时保留上一轮的值。 +4. **算清速率预算**:认证后 5,000 次/小时。按当前规模(**11 个集群 / 21 个仓库 / 279 个 runner label**, + 见 `ascend-gha-runners` 的 Cluster Map,按 ArgoCD 配置自动生成):21 个仓库 × 60 轮 = **1,260 次/小时**拉 run 列表; + 新增 run 的 jobs 调用按天算只有几百次,可忽略。`in_progress` 那条不参与失败率计算,放宽到 5 分钟一轮(252 次/小时), + 合计约 1,500 次/小时,占额度 30%。仓库数涨到 60 以上再放宽周期或按仓库分片。 + 首屏 100 条要确认够不够——单仓每分钟超过 100 个 run 才需要翻页。 +5. **首次启动只建基线**:`last_success_timestamp` 用现有历史回填,否则上线当天满屏"24 小时无成功"。 + +仓库清单不用手工维护:从中心 Prometheus 查 `count by (repo) (ci_runner_job)` 取近 7 天出现过的仓库,每日生成一次配置。 +上线前用 `ascend-gha-runners` 的 Cluster Map 对一遍——那是按 ArgoCD 配置自动生成的,比指标更全 +(当前 21 个仓库,其中 9 个跨多个集群,`vllm-project/vllm-ascend` 一个仓库就跑在 6 个集群上)。 + +#### 5.1.3 指标设计 + +| 指标 | 类型 | 标签 | 用途 | +| --- | --- | --- | --- | +| `ci_workflow_run_result_total` | counter | `repo`、`workflow`、`conclusion`、`event`、`cluster` | 失败率主指标 | +| `ci_workflow_job_result_total` | counter | `repo`、`workflow`、`job`、`conclusion` | 定位是哪个 job 挂了 | +| `ci_workflow_run_duration_seconds` | histogram | `repo`、`workflow`、`conclusion` | 时长分布与变慢趋势 | +| `ci_workflow_consecutive_failures` | gauge | `repo`、`workflow` | 连续失败告警 | +| `ci_workflow_last_success_timestamp_seconds` | gauge | `repo`、`workflow` | "多久没成功过" | +| `ci_workflow_runs_in_progress` | gauge | `repo` | 大盘 | +| `ci_workflow_last_sync_timestamp_seconds` / `ci_workflow_api_errors_total` / `ci_workflow_rate_limit_remaining` | gauge / counter / gauge | — | 自监控 | + +表里的 `workflow` 和 `job` 是 GitHub Actions 的两层对象,**都不是 id,也都不是文件名**: + +| GitHub 对象 | 是什么 | 标识 | 本方案 | +| --- | --- | --- | --- | +| 工作流 workflow | `.github/workflows/*.yaml` 一个文件;一次运行里的 `run.name` 取文件里的 `name:`(如 `CI`),文件没写 `name:` 时 GitHub 回退成文件路径 | 仓库内唯一;`workflow_id` 是数值 id | `workflow` 标签取 `run.name` | +| 运行 run | 工作流的一次执行(push / PR / 定时触发) | `run_id` + `run_attempt` | **只做水位线与去重,不做标签** | +| 作业 job | YAML 里 `jobs:` 下的一个条目,最终落到某个 runner 上执行 | `job_id`,只在本次 run 内唯一 | `job` 标签取 `job.name` | + +三个坑: + +- **`workflow` 不是文件名**:标签取 `name:` 而不是 `path`。`name:` 改了、或被删掉回退成路径,都会换一个 series,历史曲线断开——改名时要在变更记录里写一句。 +- **矩阵 job 会展开**:`strategy.matrix` 的 job 会变成多个 `job.name` 带参数后缀(`build (aarch64, py3.10)`)。默认保留全名; + 只有某个矩阵维度取值很多(>10)时,才在第一个 `(` 处截断成 `build`,代价是分不清具体矩阵项。 +- **`job_id` / `run_id` 禁止做标签**:数值 id 每次运行都变,等于一次运行一个 series,直接撑爆时序。 + +标签纪律: + +- `repo` 用 `owner/name`;`workflow` 用 run 的 `name`(人可读),不要用 `.github/workflows/xxx.yaml` 路径——路径会因为改文件名而换一个 series。 +- `conclusion` 归一成小写:`success` / `failure` / `cancelled` / `skipped` / `timed_out` / `action_required`。 + **`cancelled`(人取消)和 `skipped` 不算失败**,混进失败率会长期误报——与 6.1.3 是同一口径。 +- `cluster` 从 job 的 `labels` 里匹配 scale set 名反查(如 `linux-aarch64-a3-2-gy004` → `guiyang-004`),**匹配不到就留空,不允许猜**。 +- **禁止把 `run_id` / `head_sha` 做成标签**,高基数会撑爆时序。需要定位单次运行的点进去看 GitHub,或用 `run_id` 去和 `ci_runner_job` 在 Grafana 里关联。 + +#### 5.1.4 告警规则 + +| 名称 | 条件 | 级别 | 时延 | +| --- | --- | --- | --- | +| `CIWorkflowBulkFailure` | 10 分钟失败率 > 30% 且样本 ≥ 20 | critical | ~15 min | +| `CIWorkflowFailing` | `consecutive_failures >= 5` | warning | ~30 min | +| `CIWorkflowNoSuccess` | `time() - last_success_timestamp > 86400` | warning | 1 天 | +| `CIWorkflowExporterStale` | 距上次成功同步 > 5 分钟,或 `up == 0` | critical | 5 min | + +```promql +# 批量失败:分母是全部结论,不是只有成功和失败两种 +sum(increase(ci_workflow_run_result_total{conclusion="failure"}[10m])) + / +sum(increase(ci_workflow_run_result_total[10m])) > 0.3 +and +sum(increase(ci_workflow_run_result_total[10m])) >= 20 +``` + +三条注意: + +1. **样本量下限(≥ 20)不能省**:夜间只有零星几次运行,挂 1 次就是 50%。 +2. 单次失败**故意不告警**,只认"批量"和"连续",否则正常偶发失败会淹没通知。 +3. `CIWorkflowNoSuccess` 要**排除低频仓库**(白名单或"近 7 天运行数 ≥ N"才纳入),否则一周跑一次的仓库会天天报。 + +结果指标真正的价值是**区分责任边界**,判读规则(不是告警): + +- 同一时间段**多个仓库一起失败**,且 `github_probe_success == 0` → 我方链路问题(DNS / 代理 / 白名单 / 限流); +- **只有单个仓库失败** → 该仓库代码或 workflow 自身问题; +- **多个仓库同一 job 名失败** → runner 镜像或共享缓存的问题,去看 `runner_version_update_available` 与缓存组件。 + +#### 5.1.5 部署与运维 + +- **形态用 Deployment,不用 CronJob + Pushgateway**:Pushgateway 本身能存 counter,问题出在推送方—— + CronJob 是短生命周期进程,计数器每轮从 0 开始,推上去的值反复归零,`increase()` 只会看到重置。 + 要用就得自己持久化累计值,另外还要处理 Pushgateway 不做 TTL(进程停了指标停在最后一个值, + 现有探针靠 `push_time_seconds` + `*ProbeStale` 兜底)和多副本写同一个 group 互相覆盖。 + Deployment 单副本暴露 `/metrics:8080` 由中心 Prometheus 抓取,这些都由抓取模型天然解决: + 计数器重置 `rate()` 自己认,target 没了看 `up`。 +- **部署位置**:中心集群的监控命名空间,与现有中心 Prometheus 同网段,能出网访问 `api.github.com`; + 走代理的出口要确认代理放行 GitHub API(`github-probe` 的 `path="proxy"` 就是这条链路)。 + **只部署这一份**:结果只存在于 GitHub、与哪个集群执行无关,每集群一份会重复拉同一批 run(API 额度 ×14) + 还要把 token 分发到 14 个集群;`cluster` 标签是从 job 的 `labels` 反查出来的,不依赖部署位置。 + 代价是单点——它挂了就全量失明,所以下面的自监控是必需的,不是可选项。 +- **与本仓其他数据源的分工**:结果看 GitHub(本章);排队与调度看 `RunnerPodPendingTooLong`、`ci_runner_active_jobs`; + Pod 的等待 / 运行时长看 pod-history-api。三者不互相替代。 + ### 5.2 日志接入 +CI workflow 的日志有两条,来源不同,不要混: + +- **job 的步骤输出在 GitHub**(`/actions/runs/{run_id}/logs`,打包 zip),不进集群 LTS。GitHub 侧自己有保留期, + 不建议再往 LTS 灌一份——量大、且和 GitHub 的保留策略对不上。 +- **集群侧只有 runner Pod 自身的 stdout**(分配 job、拉镜像、连不上 GitHub 等),已经在 LTS 里, + 适合排查"runner 起不来"这类问题,看不到构建过程。 + +所以 CI workflow 的日志接入**只做到 runner Pod 这一层就够**,构建失败的具体原因回 GitHub 看。 + ## 6. Jenkins 任务 Jenkins 任务用于执行 src-openeuler 社区的 CI。 From 72a4556df8a608695845137296ead66e20f677bc Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 22 Oct 2026 17:44:18 +0800 Subject: [PATCH 2/6] =?UTF-8?q?docs:=20=E8=AF=B4=E6=98=8E=20CI=20workflow?= =?UTF-8?q?=20=E8=A7=82=E6=B5=8B=E9=80=89=E8=BD=AE=E8=AF=A2=E8=80=8C?= =?UTF-8?q?=E9=9D=9E=20webhook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补一句触发方式的取舍:轮询幂等、可回填、不怕丢事件,端到端延迟 约 12–15 分钟;webhook 能到秒级但要公网接收端与签名校验,事件丢失 只能手工 redelivery,对当前告警粒度不值。 Co-Authored-By: Claude Code --- ...5\213\346\216\245\345\205\245\346\214\207\345\215\227.md" | 5 +++++ 1 file changed, 5 insertions(+) diff --git "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" index 0e76c38..977dcd5 100644 --- "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" +++ "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" @@ -528,6 +528,11 @@ Service 端口名命中白名单即可,无需写 job。以 oauth2-proxy 为例 结论只在 GitHub 侧:`run` 和 `job` 都有 `conclusion` 字段。所以结果观测的源是 GitHub Actions API(或 webhook),集群侧数据只做补充。 +**触发方式选的是轮询,不是 webhook。** 轮询幂等、能回填历史、不怕丢事件,配额也可控(见 5.1.2); +代价是端到端延迟约 12–15 分钟(60s 轮询 + 60s 抓取 + 10 分钟告警窗口)。webhook(`workflow_run` 的 `completed` 事件) +能做到秒级,但要在公网提供带 TLS 与签名校验的接收端,事件丢了只能靠 GitHub 的 redelivery 手工补——对这个场景 +收益不值。以后若要做「失败即刻通知 PR 作者」,再上 webhook 做实时触发,轮询继续留在原地做对账。 + #### 5.1.2 数据来源 只读 GitHub Actions REST API,不动仓库、不装 App 到 workflow: From 32e014c025505e1243349d6e74d922de94f497b5 Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 22 Oct 2026 17:46:19 +0800 Subject: [PATCH 3/6] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=20CI=20workflow?= =?UTF-8?q?=20=E8=B7=A8=E8=BD=AE=E8=AF=A2=E7=AA=97=E5=8F=A3=E7=9A=84?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- ...45\345\205\245\346\214\207\345\215\227.md" | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" index 977dcd5..9bc8b50 100644 --- "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" +++ "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" @@ -548,7 +548,7 @@ Service 端口名命中白名单即可,无需写 job。以 oauth2-proxy 为例 1. **认证用 GitHub App 或服务账号的 fine-grained PAT**,权限只要 `Actions: read` + `Metadata: read`;token 进 Secret,不进配置文件。401/403 必须单独告警——它会让采集**静默归零**。 当前 21 个仓库分属 **12 个 owner**(含个人仓库 `hiyouga/LlamaFactory`):fine-grained PAT 只覆盖 token 所有者有权限的仓库, 跨 owner 要么用 GitHub App 逐个 org 安装,要么用有全量权限的服务账号,别指望一个 PAT 通吃。 -2. **水位线是 `(run_id, run_attempt)` 而不是 `run_id`**:re-run 会让 `run_attempt` 递增,只按 `run_id` 去重会把重跑的结果丢掉。 +2. **去重键是 `(run_id, run_attempt)` 而不是 `run_id`**:re-run 会让 `run_attempt` 递增,只按 `run_id` 去重会把重跑的结果丢掉。具体机制见下面「跨窗口去重」。 3. 每个仓库独立超时,**单仓失败不能拖垮整轮**,失败时保留上一轮的值。 4. **算清速率预算**:认证后 5,000 次/小时。按当前规模(**11 个集群 / 21 个仓库 / 279 个 runner label**, 见 `ascend-gha-runners` 的 Cluster Map,按 ArgoCD 配置自动生成):21 个仓库 × 60 轮 = **1,260 次/小时**拉 run 列表; @@ -561,6 +561,24 @@ Service 端口名命中白名单即可,无需写 job。以 oauth2-proxy 为例 上线前用 `ascend-gha-runners` 的 Cluster Map 对一遍——那是按 ArgoCD 配置自动生成的,比指标更全 (当前 21 个仓库,其中 9 个跨多个集群,`vllm-project/vllm-ascend` 一个仓库就跑在 6 个集群上)。 +#### 跨窗口去重 + +同一条已结束的 run 会在连续多轮轮询里反复出现在第一页,这是正常的——列表按 `created_at` 倒序, +一条 run 要过一段时间才会掉出首页。去重必须做在 exporter 内部,PromQL 侧补不回来: +同一结论被 `+1` 两次,失败率只会被放大,而且看不出异常。 + +- **不能用单条水位线 `max(run_id)` 推进**。`run_id` 随创建时间递增,但**完成顺序和创建顺序不一致**: + 一个创建早、跑了 40 分钟的长任务完成时,水位线早已被后创建的短 run 越过去了,它会被永久跳过。 +- **用「已计数集合」**:每个仓库维护 `{ ":": }`。每轮拉第一页(100 条), + 对 `status=completed` 且键不在集合里的条目各 `+1`,并把键写进集合。同一条 run 被拉到 10 轮也只加 1 次。 +- **按首屏边界剪枝**:记本轮首屏最老条目的 `created_at` 为 `horizon`,集合里 `created_at < horizon` 的键直接删。 + 列表稳定按创建时间倒序,掉出首页的 run 不会再回来,所以剪完既不会重复计数,集合也不会无限增长(每仓 ≤ 100 条)。 +- **状态落盘**:集合与 `last_success_timestamp` 写到 PVC 上的 JSON 文件。进程重启后**把当前首屏整体当基线写入、不回填**—— + 宁可缺最前面那几十秒,也不要重复计数;计数器归零交给 `rate()`。 +- **已知边界**:① re-run 只在原 run 仍处首屏范围内时才看得到,更早的 run 重跑后到不了第一页; + ② 单仓一个轮询周期内新增超过 100 个 run 需要翻页(当前 21 个仓库远达不到,翻页逻辑先把接口留出来); + ③ **只能单副本**——多副本各自维护集合,必然重复计数。 + #### 5.1.3 指标设计 | 指标 | 类型 | 标签 | 用途 | @@ -634,6 +652,9 @@ sum(increase(ci_workflow_run_result_total[10m])) >= 20 现有探针靠 `push_time_seconds` + `*ProbeStale` 兜底)和多副本写同一个 group 互相覆盖。 Deployment 单副本暴露 `/metrics:8080` 由中心 Prometheus 抓取,这些都由抓取模型天然解决: 计数器重置 `rate()` 自己认,target 没了看 `up`。 +- **状态存 PVC**:已计数集合与每仓 `last_success_timestamp` 落在 `/data/state.json`,PVC 用 ReadWriteOnce、 + `strategy: Recreate`——单副本的前提下,滚动升级不会出现两个 Pod 同时写同一份去重状态。 + 这个文件的完整生命周期见 5.1.2「跨窗口去重」。 - **部署位置**:中心集群的监控命名空间,与现有中心 Prometheus 同网段,能出网访问 `api.github.com`; 走代理的出口要确认代理放行 GitHub API(`github-probe` 的 `path="proxy"` 就是这条链路)。 **只部署这一份**:结果只存在于 GitHub、与哪个集群执行无关,每集群一份会重复拉同一批 run(API 额度 ×14) From e1eb27a6cbbaf2982b6a67301afc5ac16bad0360 Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 22 Oct 2026 19:44:57 +0800 Subject: [PATCH 4/6] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=20CI=20workflow?= =?UTF-8?q?=20=E8=BD=AE=E8=AF=A2=E5=BB=B6=E8=BF=9F=E4=BC=B0=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- ...\216\245\345\205\245\346\214\207\345\215\227.md" | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" index 9bc8b50..57e4453 100644 --- "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" +++ "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" @@ -529,7 +529,10 @@ Service 端口名命中白名单即可,无需写 job。以 oauth2-proxy 为例 结论只在 GitHub 侧:`run` 和 `job` 都有 `conclusion` 字段。所以结果观测的源是 GitHub Actions API(或 webhook),集群侧数据只做补充。 **触发方式选的是轮询,不是 webhook。** 轮询幂等、能回填历史、不怕丢事件,配额也可控(见 5.1.2); -代价是端到端延迟约 12–15 分钟(60s 轮询 + 60s 抓取 + 10 分钟告警窗口)。webhook(`workflow_run` 的 `completed` 事件) +代价是延迟:exporter 本身最多落后 60s(最坏一整轮),到 Prometheus 里可见再加一次抓取(60s), +到告警发出再加一次规则评估(默认 60s)与 `group_wait`(10s),**端到端约 3 分钟(中位 1.5 分钟)**。 +若为了抑制抖动给规则加 `for: 10m`,则变成 12–13 分钟——这 10 分钟是**确认期**,不是轮询造成的。 +webhook(`workflow_run` 的 `completed` 事件) 能做到秒级,但要在公网提供带 TLS 与签名校验的接收端,事件丢了只能靠 GitHub 的 redelivery 手工补——对这个场景 收益不值。以后若要做「失败即刻通知 PR 作者」,再上 webhook 做实时触发,轮询继续留在原地做对账。 @@ -618,10 +621,10 @@ Service 端口名命中白名单即可,无需写 job。以 oauth2-proxy 为例 | 名称 | 条件 | 级别 | 时延 | | --- | --- | --- | --- | -| `CIWorkflowBulkFailure` | 10 分钟失败率 > 30% 且样本 ≥ 20 | critical | ~15 min | -| `CIWorkflowFailing` | `consecutive_failures >= 5` | warning | ~30 min | -| `CIWorkflowNoSuccess` | `time() - last_success_timestamp > 86400` | warning | 1 天 | -| `CIWorkflowExporterStale` | 距上次成功同步 > 5 分钟,或 `up == 0` | critical | 5 min | +| `CIWorkflowBulkFailure` | 10 分钟失败率 > 30% 且样本 ≥ 20 | critical | ~3 min(另需窗口内攒够 20 个样本) | +| `CIWorkflowFailing` | `consecutive_failures >= 5` | warning | 取决于运行频次(5 次连续失败)+ ~3 min | +| `CIWorkflowNoSuccess` | `time() - last_success_timestamp > 86400` | warning | 1 天 + ~3 min | +| `CIWorkflowExporterStale` | 距上次成功同步 > 5 分钟,或 `up == 0` | critical | ~7 min(5 min 判定 + 抓取与评估) | ```promql # 批量失败:分母是全部结论,不是只有成功和失败两种 From 2908766de34e57f5ba02867d3b8e6bf43b1e12f9 Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Thu, 22 Oct 2026 19:52:11 +0800 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=20CI=20workflow?= =?UTF-8?q?=20=E5=BB=B6=E8=BF=9F=E6=9E=84=E6=88=90=E4=B8=8E=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E9=A9=B1=E5=8A=A8=E5=8F=AF=E4=BC=98=E5=8C=96=E8=8C=83?= =?UTF-8?q?=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- ...45\345\205\245\346\214\207\345\215\227.md" | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" index 57e4453..e159bea 100644 --- "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" +++ "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" @@ -528,13 +528,36 @@ Service 端口名命中白名单即可,无需写 job。以 oauth2-proxy 为例 结论只在 GitHub 侧:`run` 和 `job` 都有 `conclusion` 字段。所以结果观测的源是 GitHub Actions API(或 webhook),集群侧数据只做补充。 -**触发方式选的是轮询,不是 webhook。** 轮询幂等、能回填历史、不怕丢事件,配额也可控(见 5.1.2); -代价是延迟:exporter 本身最多落后 60s(最坏一整轮),到 Prometheus 里可见再加一次抓取(60s), -到告警发出再加一次规则评估(默认 60s)与 `group_wait`(10s),**端到端约 3 分钟(中位 1.5 分钟)**。 +**触发方式选的是轮询,不是 webhook。** 轮询幂等、能回填历史、不怕丢事件,配额也可控(见 5.1.2)。 +代价是延迟。按一次 run 结束起算,端到端是四段叠起来的: + +| 阶段 | 最坏 | 中位 | 说明 | +| --- | --- | --- | --- | +| exporter 下一次轮询 | 60s | 30s | 轮询间隔 60s,均匀分布 | +| Prometheus 抓取 | 60s | 30s | 中心集群 `scrapeInterval: 60s` | +| 规则评估 | 60s | 30s | 未配 `evaluationInterval`,取 Prometheus 默认 1m | +| `group_wait` | 10s | 10s | Alertmanager 配置 | +| **合计** | **~3.2 min** | **~1.6 min** | 不含 `for:`(当前规则没写) | + 若为了抑制抖动给规则加 `for: 10m`,则变成 12–13 分钟——这 10 分钟是**确认期**,不是轮询造成的。 -webhook(`workflow_run` 的 `completed` 事件) -能做到秒级,但要在公网提供带 TLS 与签名校验的接收端,事件丢了只能靠 GitHub 的 redelivery 手工补——对这个场景 -收益不值。以后若要做「失败即刻通知 PR 作者」,再上 webhook 做实时触发,轮询继续留在原地做对账。 + +四段里只有第一段是「在等一个事件」,能被事件驱动替掉;后三段都不认事件: + +| 阶段 | 事件驱动能消吗 | 手段与代价 | +| --- | --- | --- | +| 轮询 60s | ✅ 能降到秒级 | 用 GitHub webhook 或 K8s watch(`EphemeralRunner` / runner Pod 终态)作**触发**,事件到了立刻查一次该 run | +| 抓取 60s | ❌ | 只能改 `scrapeInterval`(降到 15–30s,抓取量 ×2~4),或让 exporter 主动 remote-write(丢掉 `up` 与抓取模型) | +| 规则评估 60s | ❌ | 只能调 `evaluationInterval`(全局,所有规则一起变快也一起变重);Prometheus 没有「数据到达即评估」的钩子 | +| `group_wait` 10s | ❌ | 方向相反,它在故意聚合;设 0 只会把同批告警拆成多封邮件 | + +**推荐路径**:事件只作触发、不作数据源——结论仍然逐条问 GitHub(每个 run 一次,比每 60s 扫 21 个仓库更省额度), +60s 轮询保留做对账:事件丢了、或 Pod 已终止但 GitHub 还没写完 conclusion,都要靠它兜。 +再配上抓取与评估各降到 30s,告警端到端可到 **1 分钟上下**;要再快只能绕过 Alertmanager 直发通知, +会丢掉分组、静默、去重,一期不做。 + +**为什么不用 webhook 直接取数**:`workflow_run` 的 `completed` 事件确实秒级,但要在公网提供带 TLS 与签名校验的接收端, +事件丢了只能靠 GitHub 的 redelivery 手工补——对这个场景收益不值。以后若要做「失败即刻通知 PR 作者」, +再上 webhook 做实时触发,轮询继续留在原地做对账。 #### 5.1.2 数据来源 From 746b727e590845a16bac30b9a1c12ffa6eeee35e Mon Sep 17 00:00:00 2001 From: TangJia025 <574451426@qq.com> Date: Fri, 23 Oct 2026 18:07:51 +0800 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20=E9=87=8D=E5=86=99=20CI=20workflow?= =?UTF-8?q?=20=E8=A7=82=E6=B5=8B=E7=AB=A0=E8=8A=82=EF=BC=8C=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=E5=8F=8C=E5=B9=B3=E5=8F=B0=E4=B8=8E=E5=88=86=E6=9C=9F?= =?UTF-8?q?=E8=A7=84=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 第 5 章整体重写:整体架构图(CI 集群在上、中心集群在下, 区分事件上报与 remote_write 两条通路)、指标清单、分期规划、详细设计 - 一期同时覆盖 GitHub 与 GitCode:platform 标签、Source 适配器、 状态/结论归一表、排序键抽象 - ghalistener 归入二期、event agent 归入三期;明确 ② 与 ③ 在 ARC 指标里拆不开,二期 B 只买到②+③合计 - 轮询路径改用「终态轮询 / 在途轮询 / job 轮询」表述 Co-Authored-By: Claude Code --- ...45\345\205\245\346\214\207\345\215\227.md" | 607 +++++++++++++----- 1 file changed, 455 insertions(+), 152 deletions(-) diff --git "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" index e159bea..d457920 100644 --- "a/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" +++ "b/docs/\345\217\257\350\247\202\346\265\213\346\216\245\345\205\245\346\214\207\345\215\227.md" @@ -514,191 +514,494 @@ Service 端口名命中白名单即可,无需写 job。以 oauth2-proxy 为例 ## 5. CI workflow -### 5.1 指标接入 +本章解决「CI 跑的结果怎么样、为什么慢、卡在哪」,覆盖两类业务: -本节是方案,尚未落地(当前 CI workflow 只有 runner 维度的在跑信息,见[CI 可观测现状 第 8 章](CI可观测现状.md))。 +- **参与型**:仓库在 GitHub,跑 GitHub Actions; +- **主导型**:仓库在 GitCode,跑 GitCode Actions。 -#### 5.1.1 为什么结果必须从 GitHub 取 +两者共用同一套指标契约,靠 `platform` 标签(`github` / `gitcode`)区分,不做两套大盘、两套告警。 -现在能拿到的东西都回答不了「这次 workflow 成功了没有」: +当前只有 runner 维度的在跑信息,见[CI 可观测现状 第 8 章](CI可观测现状.md)。本章是方案,尚未落地。 -1. **`ci_runner_job`** 是 Pushgateway 上的 info 型指标,runner 一结束就被下一次 push 全量替换掉——它只描述"此刻谁在跑",不留下任何结果。 -2. **pod-history-api** 有 `status`(`released` / `rejected` / `expired`)、`duration`、`wait_duration`,但那是 **Pod 生命周期**,不是 job 结论:runner Pod 正常退出不代表 job 成功;而且它只有查询 API,没有 Prometheus 指标。 -3. **定制 ARC 的 `EphemeralRunner.status`** 有 `jobRepositoryName` / `workflowRunId` / `jobWorkflowRef` / `jobDisplayName`,**没有 conclusion**——controller 不关心 job 成功与否。 +### 5.1 整体架构 -结论只在 GitHub 侧:`run` 和 `job` 都有 `conclusion` 字段。所以结果观测的源是 GitHub Actions API(或 webhook),集群侧数据只做补充。 +#### 5.1.1 三个组件与两条既有通路 -**触发方式选的是轮询,不是 webhook。** 轮询幂等、能回填历史、不怕丢事件,配额也可控(见 5.1.2)。 -代价是延迟。按一次 run 结束起算,端到端是四段叠起来的: +``` +┌──────────── 每个 CI 集群(11 个) ───────────────────────────────────────────────────────────────────────┐ +│ │ +│ arc-systems: ghalistener × N(已有) │ +│ └─ :8080/metrics ──scrape──► Prometheus agent(每集群已有) │ +│ │ +│ ci-event-agent(三期):watch EphemeralRunner │ +└───────────────────────┬─────────────────────────────────────────────────────┬────────────────────────────┘ + │ 事件上报(三期) │ remote_write + ▼ ▼ +┌──────────────── 中心集群 ────────────────────────────────────────────────────────────────────────────────┐ +│ │ +│ ┌──────────────────────────────────────────┐ ┌────────────────────────────────────────┐ │ +│ │ ci-workflow-aggregator(单副本) │ │ 中心 Prometheus(唯一汇聚点) │ │ +│ │ ├ Source: github/gitcode 适配器 │ /metrics │ ├ 抓取 aggregator 的 /metrics │ │ +│ │ ├ 终态轮询 A:已结束的 run(60s) │──────────►│ └ 告警规则 ──► Alertmanager │ │ +│ │ ├ 在途轮询 B:排队 / 执行中(5min) │ │ └─► 通知(邮件 / IM / SMN) │ │ +│ │ ├ 在途轮询 B+:在途 run 的 job(5min) │ │ └─► 值班人 │ │ +│ │ ├ 事件接收 POST /ingest(三期) │ │ │ │ +│ │ └ 状态: 1Gi PVC + state.json │ │ │ │ +│ └──────────────────────────────────────────┘ └────────────────────────────────────────┘ │ +│ │ +│ ▲ 轮询 A / B / B+ │ +│ │ │ +│ GitHub Actions REST API · GitCode v8 API │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +``` -| 阶段 | 最坏 | 中位 | 说明 | -| --- | --- | --- | --- | -| exporter 下一次轮询 | 60s | 30s | 轮询间隔 60s,均匀分布 | -| Prometheus 抓取 | 60s | 30s | 中心集群 `scrapeInterval: 60s` | -| 规则评估 | 60s | 30s | 未配 `evaluationInterval`,取 Prometheus 默认 1m | -| `group_wait` | 10s | 10s | Alertmanager 配置 | -| **合计** | **~3.2 min** | **~1.6 min** | 不含 `for:`(当前规则没写) | +| 组件 | 位置 | 数量 | 数据源 | 产出 | +| --- | --- | --- | --- | --- | +| `ci-workflow-aggregator` | 中心集群 | **1 个**(单例) | GitHub / GitCode REST API | run/job 级结果、耗时、在途状态 | +| `ghalistener` | 各 CI 集群 `arc-systems` | 每 scale set 1 个(已有 387 个) | GitHub Actions 消息通道 | 容量 gauge、②③④ 阶段耗时(分段定义见 5.1.4) | +| `ci-event-agent` | 各 CI 集群 | 每集群 1 个(三期) | `EphemeralRunner` / Pod | run 身份、精确完成事件、重跑 | + +**event agent 只上报 job 级事实,run 级结论只在 aggregator 产生。** 这里有**两层重复**,成因不同: -若为了抑制抖动给规则加 `for: 10m`,则变成 12–13 分钟——这 10 分钟是**确认期**,不是轮询造成的。 +- **跨集群 agent 之间**:一个 job 只在一个集群执行,各 agent 看到的 job 互不相交,本身不重复。但如果让每个 agent 各自算 run 级结论,同一条 run 就会在它涉及的每个集群各被记一次,而且每一份都是残缺的——agent 只知道"我这个集群的这几个 job 结束了",不知道整条 run 是否结束、结论是什么。一条 run 跨 6 个集群就会记 6 次。 +- **轮询与事件之间**:同一条 run 的终态会被两条路径各发现一次,这是真重复,由 aggregator 的幂等键 `:::` 合并。 -四段里只有第一段是「在等一个事件」,能被事件驱动替掉;后三段都不认事件: +#### 5.1.2 为什么必须三种采集并存 -| 阶段 | 事件驱动能消吗 | 手段与代价 | +| 观测问题 | 谁回答 | 其他源为什么答不了 | | --- | --- | --- | -| 轮询 60s | ✅ 能降到秒级 | 用 GitHub webhook 或 K8s watch(`EphemeralRunner` / runner Pod 终态)作**触发**,事件到了立刻查一次该 run | -| 抓取 60s | ❌ | 只能改 `scrapeInterval`(降到 15–30s,抓取量 ×2~4),或让 exporter 主动 remote-write(丢掉 `up` 与抓取模型) | -| 规则评估 60s | ❌ | 只能调 `evaluationInterval`(全局,所有规则一起变快也一起变重);Prometheus 没有「数据到达即评估」的钩子 | -| `group_wait` 10s | ❌ | 方向相反,它在故意聚合;设 0 只会把同批告警拆成多封邮件 | +| 这次 run 成功了没有 | aggregator(轮询 API) | 集群侧没有 `conclusion`,runner 正常退出≠job 成功 | +| 哪个 job 挂了 | aggregator(`/jobs`) | 同上 | +| 现在有多少 run 在排队、排了多久 | aggregator(路径 B) | listener 只有 scale set 视角,没有 run 身份 | +| 池子够不够、扩缩容卡在哪 | ghalistener(8 个 gauge) | API 视角看不到 runner 注册数、`min/max/desired` 这些意图值 | +| 慢在 GitHub 派发还是我方扩缩容 | aggregator + ghalistener 相减 | 单看任一方只有合并值 | +| 哪个 PR、要不要重跑 | event agent(三期) | API 列表按 `created_at` 排序,重跑旧 run 落不到首页 | -**推荐路径**:事件只作触发、不作数据源——结论仍然逐条问 GitHub(每个 run 一次,比每 60s 扫 21 个仓库更省额度), -60s 轮询保留做对账:事件丢了、或 Pod 已终止但 GitHub 还没写完 conclusion,都要靠它兜。 -再配上抓取与评估各降到 30s,告警端到端可到 **1 分钟上下**;要再快只能绕过 Alertmanager 直发通知, -会丢掉分组、静默、去重,一期不做。 +#### 5.1.3 能力矩阵(平台 × 能力) -**为什么不用 webhook 直接取数**:`workflow_run` 的 `completed` 事件确实秒级,但要在公网提供带 TLS 与签名校验的接收端, -事件丢了只能靠 GitHub 的 redelivery 手工补——对这个场景收益不值。以后若要做「失败即刻通知 PR 作者」, -再上 webhook 做实时触发,轮询继续留在原地做对账。 +| 能力 | GitHub | GitCode | +| --- | --- | --- | +| run/job 结果与耗时(轮询) | ✅ | ✅ 换适配器即可 | +| 在途排队/执行 gauge | ✅ | ⚠️ run 级排队状态待实测 | +| 容量 gauge、②+③ 启动耗时与 ④ 执行耗时(ghalistener) | ✅ | ❌ ARC 是 GitHub Actions 专用 | +| run 身份、重跑补齐(event agent) | ✅ | ❌ 无 `EphemeralRunner` 对应物 | -#### 5.1.2 数据来源 +两边观测深度**天然不对称**:GitHub 半边能看到「扩容慢还是平台派发慢」,GitCode 半边只有 API 视角。大盘上必须显式标注,否则「没数据」会被误读成「没跑任务」。 -只读 GitHub Actions REST API,不动仓库、不装 App 到 workflow: +#### 5.1.4 阶段划分与数据源对应 -| 端点 | 取什么 | 频率 | -| --- | --- | --- | -| `/repos/{owner}/{repo}/actions/runs?status=completed&per_page=100` | 已结束的 run:`id`、`name`、`conclusion`、`run_attempt`、`run_started_at`、`updated_at`、`event` | 60s | -| `/repos/{owner}/{repo}/actions/runs/{run_id}/jobs?per_page=100` | 该 run 下每个 job:`name`、`conclusion`、`started_at`、`completed_at`、`labels`、`runner_name` | 只对新增的 run | -| `/repos/{owner}/{repo}/actions/runs?status=in_progress` | 当前在跑的 run 数 | 60s | - -五条约定: - -1. **认证用 GitHub App 或服务账号的 fine-grained PAT**,权限只要 `Actions: read` + `Metadata: read`;token 进 Secret,不进配置文件。401/403 必须单独告警——它会让采集**静默归零**。 - 当前 21 个仓库分属 **12 个 owner**(含个人仓库 `hiyouga/LlamaFactory`):fine-grained PAT 只覆盖 token 所有者有权限的仓库, - 跨 owner 要么用 GitHub App 逐个 org 安装,要么用有全量权限的服务账号,别指望一个 PAT 通吃。 -2. **去重键是 `(run_id, run_attempt)` 而不是 `run_id`**:re-run 会让 `run_attempt` 递增,只按 `run_id` 去重会把重跑的结果丢掉。具体机制见下面「跨窗口去重」。 -3. 每个仓库独立超时,**单仓失败不能拖垮整轮**,失败时保留上一轮的值。 -4. **算清速率预算**:认证后 5,000 次/小时。按当前规模(**11 个集群 / 21 个仓库 / 279 个 runner label**, - 见 `ascend-gha-runners` 的 Cluster Map,按 ArgoCD 配置自动生成):21 个仓库 × 60 轮 = **1,260 次/小时**拉 run 列表; - 新增 run 的 jobs 调用按天算只有几百次,可忽略。`in_progress` 那条不参与失败率计算,放宽到 5 分钟一轮(252 次/小时), - 合计约 1,500 次/小时,占额度 30%。仓库数涨到 60 以上再放宽周期或按仓库分片。 - 首屏 100 条要确认够不够——单仓每分钟超过 100 个 run 才需要翻页。 -5. **首次启动只建基线**:`last_success_timestamp` 用现有历史回填,否则上线当天满屏"24 小时无成功"。 - -仓库清单不用手工维护:从中心 Prometheus 查 `count by (repo) (ci_runner_job)` 取近 7 天出现过的仓库,每日生成一次配置。 -上线前用 `ascend-gha-runners` 的 Cluster Map 对一遍——那是按 ArgoCD 配置自动生成的,比指标更全 -(当前 21 个仓库,其中 9 个跨多个集群,`vllm-project/vllm-ascend` 一个仓库就跑在 6 个集群上)。 - -#### 跨窗口去重 - -同一条已结束的 run 会在连续多轮轮询里反复出现在第一页,这是正常的——列表按 `created_at` 倒序, -一条 run 要过一段时间才会掉出首页。去重必须做在 exporter 内部,PromQL 侧补不回来: -同一结论被 `+1` 两次,失败率只会被放大,而且看不出异常。 - -- **不能用单条水位线 `max(run_id)` 推进**。`run_id` 随创建时间递增,但**完成顺序和创建顺序不一致**: - 一个创建早、跑了 40 分钟的长任务完成时,水位线早已被后创建的短 run 越过去了,它会被永久跳过。 -- **用「已计数集合」**:每个仓库维护 `{ ":": }`。每轮拉第一页(100 条), - 对 `status=completed` 且键不在集合里的条目各 `+1`,并把键写进集合。同一条 run 被拉到 10 轮也只加 1 次。 -- **按首屏边界剪枝**:记本轮首屏最老条目的 `created_at` 为 `horizon`,集合里 `created_at < horizon` 的键直接删。 - 列表稳定按创建时间倒序,掉出首页的 run 不会再回来,所以剪完既不会重复计数,集合也不会无限增长(每仓 ≤ 100 条)。 -- **状态落盘**:集合与 `last_success_timestamp` 写到 PVC 上的 JSON 文件。进程重启后**把当前首屏整体当基线写入、不回填**—— - 宁可缺最前面那几十秒,也不要重复计数;计数器归零交给 `rate()`。 -- **已知边界**:① re-run 只在原 run 仍处首屏范围内时才看得到,更早的 run 重跑后到不了第一页; - ② 单仓一个轮询周期内新增超过 100 个 run 需要翻页(当前 21 个仓库远达不到,翻页逻辑先把接口留出来); - ③ **只能单副本**——多副本各自维护集合,必然重复计数。 - -#### 5.1.3 指标设计 +一条 job 从被触发到跑完,按时间轴切成四段。分段是为了**分诊**:慢在 ① 去找平台侧,慢在 ② 是扩容或节点池资源问题,慢在 ③ 是调度与镜像问题,慢在 ④ 才是任务本身慢。 + +| 段 | 名称 | 从哪到哪 | 这一段在等什么 | GitHub 侧来源 | GitCode 侧来源 | +| --- | --- | --- | --- | --- | --- | +| ① | 平台排队 | job 创建 → 被某个 scale set 领取 | GitHub 队列积压、仓库并发上限 | 无直接指标;用 API `job.started_at − job.created_at` 减去 startup 直方图反推 | 无(只能拿到 ①②③ 合计) | +| ② | 扩容 | scale set 领取 → runner Pod 创建出来 | 扩缩容策略、节点池有没有可用卡 | 与 ③ 合并计时,拆不开 | 无(含在下面的合计里) | +| ③ | 启动 | Pod 创建 → runner 注册完成并拿到 job | 调度等待、镜像拉取、注册握手 | `gha_job_startup_duration_seconds`(= `RunnerAssignTime − ScaleSetAssignTime`,含 ②+③) | `apply_time → start_time`,是 ①②③ 的合计 | +| ④ | 执行 | job 开始执行 → job 结束 | 任务本身(编译、测试、构建镜像) | `gha_job_execution_duration_seconds`,或 API `job.completed_at − job.started_at` | API `end_time − start_time` | + +两个约束要写清楚,否则容易在盘上做出量不出来的图: + +- **② 和 ③ 在 ARC 的指标里拆不开**。listener 只暴露两条耗时直方图,startup 的起点是 scale set 领取 job 的时刻、终点是 runner 注册完成,中间囊括扩容、调度、拉镜像、注册四件事,没有指标能把它们分开。真要拆只能由三期 event agent 记下 Pod 的创建/Ready 时刻,本期不做。 +- **GitCode 侧只有 ①②③ 合计和 ④**。它的 job 只有 `apply_time` / `start_time` / `end_time` 三个时间戳,排队与启动是一整块,分段能力天然比 GitHub 弱——这也是 5.1.3 说两边不对称的具体体现。 + +### 5.2 指标清单 + +所有指标都带 `platform` 标签,取值 `github` / `gitcode`。 + +#### 5.2.1 结果与耗时(aggregator) + +| 指标 | 类型 | 标签 | 用途 | 期次 | +| --- | --- | --- | --- | --- | +| `ci_workflow_run_result_total` | counter | `platform` `repo` `workflow` `conclusion` `event` | 失败率主指标 | 一期 | +| `ci_workflow_job_result_total` | counter | `platform` `repo` `workflow` `job` `conclusion` | 定位哪个 job 挂了 | 一期 | +| `ci_workflow_run_duration_seconds` | histogram | `platform` `repo` `workflow` `conclusion` | run 时长分布 | 一期 | +| `ci_workflow_job_duration_seconds` | histogram | `platform` `repo` `workflow` `job` `conclusion` | job 执行时长 | 一期 | +| `ci_workflow_job_queue_duration_seconds` | histogram | `platform` `repo` `workflow` `job` | job 排队时长(job 轮询) | 一期 | +| `ci_workflow_consecutive_failures` | gauge | `platform` `repo` `workflow` | 连续失败告警 | 一期 | +| `ci_workflow_last_success_timestamp_seconds` | gauge | `platform` `repo` `workflow` | 多久没成功过 | 一期 | + +#### 5.2.2 在途状态(aggregator,路径 B) + +| 指标 | 类型 | 标签 | 用途 | 期次 | +| --- | --- | --- | --- | --- | +| `ci_workflow_runs_queued` | gauge | `platform` `repo` | 当前排队数 | 一期 | +| `ci_workflow_runs_in_progress` | gauge | `platform` `repo` | 当前在跑数 | 一期 | +| `ci_workflow_oldest_queued_age_seconds` | gauge | `platform` `repo` | 最久排队了多久 | 一期 | +| `ci_workflow_oldest_running_age_seconds` | gauge | `platform` `repo` | 最久执行了多久 | 一期 | +| `ci_workflow_oldest_job_queue_age_seconds` | gauge | `platform` `repo` | 最久卡住的单个 job | 一期 | + +在途指标是**瞬时值**,告警直接用阈值比较,不走 `increase`,因此没有「窗口内攒样本」的延迟。 + +#### 5.2.3 自监控(aggregator) | 指标 | 类型 | 标签 | 用途 | | --- | --- | --- | --- | -| `ci_workflow_run_result_total` | counter | `repo`、`workflow`、`conclusion`、`event`、`cluster` | 失败率主指标 | -| `ci_workflow_job_result_total` | counter | `repo`、`workflow`、`job`、`conclusion` | 定位是哪个 job 挂了 | -| `ci_workflow_run_duration_seconds` | histogram | `repo`、`workflow`、`conclusion` | 时长分布与变慢趋势 | -| `ci_workflow_consecutive_failures` | gauge | `repo`、`workflow` | 连续失败告警 | -| `ci_workflow_last_success_timestamp_seconds` | gauge | `repo`、`workflow` | "多久没成功过" | -| `ci_workflow_runs_in_progress` | gauge | `repo` | 大盘 | -| `ci_workflow_last_sync_timestamp_seconds` / `ci_workflow_api_errors_total` / `ci_workflow_rate_limit_remaining` | gauge / counter / gauge | — | 自监控 | +| `ci_workflow_last_sync_timestamp_seconds` | gauge | `platform` | 采集活性 | +| `ci_workflow_api_errors_total` | counter | `platform` `repo` `kind` | 401/403/429/超时分类计数 | +| `ci_workflow_rate_limit_remaining` | gauge | `platform` | 配额余量 | +| `ci_workflow_runs_finalized_total` | counter | `platform` `repo` `source` | 按来源(轮询/事件)计终态条数 | +| `ci_workflow_unresolved_runs` | gauge | `platform` `repo` | 进入在途但迟迟没有终态的 run 数 | +| `ci_workflow_list_truncated_total` | counter | `platform` `repo` `path` | 首页满页(可能截断)次数 | +| `ci_workflow_completed_page_overflow_total` | counter | `platform` `repo` | 一轮内新增数占满整页(可能漏采) | -表里的 `workflow` 和 `job` 是 GitHub Actions 的两层对象,**都不是 id,也都不是文件名**: +#### 5.2.4 集群侧容量与阶段(ghalistener,二期) -| GitHub 对象 | 是什么 | 标识 | 本方案 | +| 指标 | 类型 | 对应字段 | 用途 | | --- | --- | --- | --- | -| 工作流 workflow | `.github/workflows/*.yaml` 一个文件;一次运行里的 `run.name` 取文件里的 `name:`(如 `CI`),文件没写 `name:` 时 GitHub 回退成文件路径 | 仓库内唯一;`workflow_id` 是数值 id | `workflow` 标签取 `run.name` | -| 运行 run | 工作流的一次执行(push / PR / 定时触发) | `run_id` + `run_attempt` | **只做水位线与去重,不做标签** | -| 作业 job | YAML 里 `jobs:` 下的一个条目,最终落到某个 runner 上执行 | `job_id`,只在本次 run 内唯一 | `job` 标签取 `job.name` | +| `gha_job_startup_duration_seconds` | histogram | `RunnerAssignTime − ScaleSetAssignTime` | ②+③ | +| `gha_job_execution_duration_seconds` | histogram | `FinishTime − RunnerAssignTime` | ④ | +| `gha_started_jobs_total` / `gha_completed_jobs_total` | counter | — | 与轮询结果交叉校验 | +| `gha_assigned_jobs` / `gha_running_jobs` | gauge | — | 当前派单/在跑 | +| `gha_registered_runners` / `gha_busy_runners` / `gha_idle_runners` | gauge | — | 已注册/占用/空转 | +| `gha_min_runners` / `gha_max_runners` / `gha_desired_runners` | gauge | — | 扩缩容意图值 | -三个坑: +`gha_*` 没有 `platform` 标签——它只由 GitHub 侧产出,`platform` 恒为 `github`,不构成维度。 -- **`workflow` 不是文件名**:标签取 `name:` 而不是 `path`。`name:` 改了、或被删掉回退成路径,都会换一个 series,历史曲线断开——改名时要在变更记录里写一句。 -- **矩阵 job 会展开**:`strategy.matrix` 的 job 会变成多个 `job.name` 带参数后缀(`build (aarch64, py3.10)`)。默认保留全名; - 只有某个矩阵维度取值很多(>10)时,才在第一个 `(` 处截断成 `build`,代价是分不清具体矩阵项。 -- **`job_id` / `run_id` 禁止做标签**:数值 id 每次运行都变,等于一次运行一个 series,直接撑爆时序。 +#### 5.2.5 事件侧(ci-event-agent,三期) -标签纪律: +三期**不新增业务指标**。事件路径不产出另一套指标,它做的是让 `ci_workflow_*` 更快、更全地拿到同一批事实:重跑补齐、精确完成时刻。入账仍在 aggregator,按幂等键合并,两条路径的产出用 `ci_workflow_runs_finalized_total{source}` 分开计数。agent 自身只需存活类自监控,不单列。 + +### 5.3 分期规划 -- `repo` 用 `owner/name`;`workflow` 用 run 的 `name`(人可读),不要用 `.github/workflows/xxx.yaml` 路径——路径会因为改文件名而换一个 series。 -- `conclusion` 归一成小写:`success` / `failure` / `cancelled` / `skipped` / `timed_out` / `action_required`。 - **`cancelled`(人取消)和 `skipped` 不算失败**,混进失败率会长期误报——与 6.1.3 是同一口径。 -- `cluster` 从 job 的 `labels` 里匹配 scale set 名反查(如 `linux-aarch64-a3-2-gy004` → `guiyang-004`),**匹配不到就留空,不允许猜**。 -- **禁止把 `run_id` / `head_sha` 做成标签**,高基数会撑爆时序。需要定位单次运行的点进去看 GitHub,或用 `run_id` 去和 `ci_runner_job` 在 Grafana 里关联。 +| 期次 | 组件 | 观测能力 | 工期 | 主要风险 | +| --- | --- | --- | --- | --- | +| **一期** | aggregator(GitHub + GitCode 两个 `Source` 适配器、终态轮询 / 在途轮询 / job 轮询、截断守卫、指标、告警、大盘) | run/job 结果与耗时、在途排队与执行、失败率与连续失败 | | 单点;凭据失效会静默归零;GitCode 限流规则未公开,接入前要实测 | +| **二期** | 各集群 Prometheus agent 加 scrape job(A 步);536 个 scale set 的 `listenerMetrics` 声明(B 步) | A 步先拿 8 个容量 gauge(池子够不够、扩缩容意图与实际差多少);B 步再补 ②+③ 启动耗时与 ④ 执行耗时直方图、job 级排队 | | A 步不重启任何组件;B 步改 values 会重建 listener pod,需分批 | +| **三期** | 各集群 `ci-event-agent` | run 身份、重跑补齐、精确完成事件 | | 取决于 GitCode 侧 runner 形态 | -#### 5.1.4 告警规则 +一期的两个适配器同步落地:GitHub 先行,GitCode 在 `Source` 接口定稿后并行接入。**契约与接口必须一次到位**——`platform` 标签、枚举归一、排序键抽象晚加等于全部序列换名。GitCode 接口实测若有阻塞,允许它的适配器在一期后段交付,但不允许先只做 GitHub 再回头改契约。 -| 名称 | 条件 | 级别 | 时延 | +### 5.4 详细设计 + +#### 5.4.1 中心 aggregator + +**内部结构** + +``` +┌──────────────── ci-workflow-aggregator ────────────────┐ +│ scheduler ──┬─ A completed 首页 每 60s │ +│ ├─ B queued + in_progress 每 5min │ +│ ├─ B+ 活跃 run 的 /jobs 每 5min │ +│ └─ 状态落盘 / 剪枝 每 30s 或 N 条 │ +│ │ +│ Source 接口 ── githubAdapter / gitcodeAdapter │ +│ 状态层 ── seen(去重)/ inflight(在途)/ 每仓统计 │ +│ 出口 ── /metrics(Prometheus 抓取) │ +│ ── /ingest(三期接收 agent 事件,带 token) │ +└─────────────────────────────────────────────────────────┘ +``` + +**归一数据模型** + +```go +type Run struct { + Platform string // github | gitcode + Repo string // owner/name + ID string // GitHub 数字 id / GitCode 32 位 hex + Attempt int + Workflow string + Status string // 归一后:queued|running|completed + Conclusion string // 归一后:success|failure|cancelled|skipped|... + Event string + OrderKey time.Time // GitHub created_at / GitCode start_time + StartedAt *time.Time + FinishedAt *time.Time +} +``` + +`Source` 只做三件事:`ListRuns(status, since, page)`、`GetRun(id)`、`ListJobs(id)`。去重、状态机、指标、告警全部平台无关。 + +**状态归一(适配器最核心的一条)**:GitHub 的 run 有 `status`(阶段:`queued` / `in_progress` / `completed`)和 `conclusion`(结论)**两个**字段;GitCode 只有**一个** `status`,阶段和结论混在同一个值里。适配器必须拆开:`RUNNING` → `Status=running`;`COMPLETED` / `FAILED` / `CANCELED` / `IGNORED` → `Status=completed`,结论按下表映射。`PAUSED` / `SUPPEND` 的结论语义未验证,先各自保留成独立取值,不并入成功或失败。拆完之后,上层的状态机只认 `Status` / `Conclusion` 两个字段,不再关心平台。 + +| 含义 | 归一值 | GitHub | GitCode | | --- | --- | --- | --- | -| `CIWorkflowBulkFailure` | 10 分钟失败率 > 30% 且样本 ≥ 20 | critical | ~3 min(另需窗口内攒够 20 个样本) | -| `CIWorkflowFailing` | `consecutive_failures >= 5` | warning | 取决于运行频次(5 次连续失败)+ ~3 min | -| `CIWorkflowNoSuccess` | `time() - last_success_timestamp > 86400` | warning | 1 天 + ~3 min | -| `CIWorkflowExporterStale` | 距上次成功同步 > 5 分钟,或 `up == 0` | critical | ~7 min(5 min 判定 + 抓取与评估) | +| 成功 | `success` | `success` | `COMPLETED` | +| 失败 | `failure` | `failure` | `FAILED`(可能带后缀,取 `split("(")[0]`) | +| 取消 | `cancelled` | `cancelled` | `CANCELED` | +| 跳过 | `skipped` | `skipped` | `IGNORED` | +| 未定 | 原样保留 | — | `PAUSED` / `SUPPEND`,不并入成功/失败 | -```promql -# 批量失败:分母是全部结论,不是只有成功和失败两种 -sum(increase(ci_workflow_run_result_total{conclusion="failure"}[10m])) - / -sum(increase(ci_workflow_run_result_total[10m])) > 0.3 -and -sum(increase(ci_workflow_run_result_total[10m])) >= 20 +`event` 同理归一:`MR` → `pull_request`、`Push` → `push`、`Manual` → `workflow_dispatch`;`Note` 语义未验证,先原样保留。 + +**标签取值**(指标 label 直接取自归一后的字段): + +- `repo` 用 `owner/name`;`workflow` 取 run 的可读名(GitHub `run.name`、GitCode `workflow_name`),**不用文件路径**——改文件名会换 series。 +- `job` 取 job 名;矩阵 job 默认保留全名(`build (aarch64, py3.10)`),单维度取值超过 10 个时才截断到第一个 `(`。 +- **禁止**把 `run_id` / `job_id` / `head_sha` / `job_workflow_ref` 做成标签,高基数会撑爆时序。`status` 不进标签,只驱动状态机。 + +**三条轮询路径** + +| 路径 | 频率 | 请求 | 职责 | +| --- | --- | --- | --- | +| A | 60s | `status=completed&per_page=100` | 短 run 的终态;本轮新 run 逐个加计数 | +| B | 5min | `status=queued` + `status=in_progress` 各一页 | 在途 gauge;run 消失后按 ID 核实终态 | +| B+ | 5min | 对在途 run 取 `/jobs` | job 级排队与执行时长,定位具体 job | + +B+ 的调用量随活跃 run 数走(20~40 个 × 12 次/小时 ≈ 240~480 次/小时)。 + +**去重** + +- 键:`:::`。re-run 会让 `run_attempt` 递增,只按 `run_id` 会丢重跑。 +- **不能用单条水位线 `max(run_id)` 推进**:完成顺序和创建顺序不一致,长任务会被水位线越过而永久跳过。 +- 每次成功计数后刷新 `processed_at`。 + +**剪枝(两条同时满足才删)** + +1. `order_key < 本轮 A 首页最老条目` —— 已经掉出首页,不会再被 A 看到; +2. `now − processed_at > 对账窗口`(默认 2 小时)—— 深扫不会再覆盖到它。 + +只按 TTL 剪会出问题:低频仓库首页可能覆盖几十天,按固定 TTL 剪掉的键会重新出现并被二次计数。 + +**终态判定** + +在途 run 从 B 的列表里消失时,**不连续等两轮**,而是: + +- 本轮两条列表请求都成功(HTTP 200 且解析成功); +- 按 ID 取一次 `GetRun`; +- **以返回的 `status` 为准**:`completed` 才入账,仍是 `running`/`queued` 就留在 registry。 + +连续两轮的代价是长任务完成时又多等一个周期,而它本来就是为了防整页请求失败——用「请求成功校验」能直接覆盖,不需要多等。 + +**失败与降级** + +- 每个仓库独立超时,**单仓失败不拖垮整轮**,失败时保留上一轮的值,只加 `api_errors_total`。 +- 401/403 单独告警:它会让采集静默归零。 +- 进程重启后把当前首页整体当基线写入、**不回填**——宁可缺最前面几十秒,也不要重复计数。 + +**守卫(替代原全局对账)** + +| 守卫 | 判据 | 覆盖的风险 | +| --- | --- | --- | +| 列表截断 | B 的响应 `len(items) == per_page` | 排 100+ 时最久的 run 从没被看见 | +| 首页溢出 | A 本轮新 run 数 == `per_page` | 一分钟内完成超过 100 个,老的没看就被挤下去 | +| 未收敛 | registry 条目超过 48h 仍无终态 | B/B+ 失效 | +| 来源计数 | `runs_finalized_total{source}` 长期为 0 | 某条路径静默停止工作 | + +注意判据用 `per_page` 而不是 `total_count`:`total_count` 会封顶在 2500,小规模下也与列表不符(实测 `queued` 为 32 vs 27),只能当上下文写进日志,不能参与判定。 + +**速率预算(GitHub,认证后 5,000 次/小时)** + +| 路径 | 计算 | 次/小时 | +| --- | --- | --- | +| A | 21 仓 × 60 | 1,260 | +| B | 21 仓 × 2 条列表 × 12 | 504 | +| B+ | 20~40 个活跃 run × 12 | 240~480 | +| 合计 | | **约 2,000~2,250(45%)** | + +GitCode 的限流规则未公开,接入前要实测;适配器里要有独立的配额余量指标。 + +**两个适配器的差异** + +| 项 | GitHub | GitCode | +| --- | --- | --- | +| 状态字段 | `status`(阶段)+ `conclusion`(结论)两个字段 | 只有一个 `status`,阶段与结论混在一起,需拆成两个归一字段(见上文「状态归一」) | +| 状态过滤 | `completed` 是**分组**,一个参数覆盖多种结论 | 每个取值单独,需多次调用或在本地过滤 | +| 排序键 | `created_at` | 无该字段,用 `start_time`(列表实测按 `run_number` 倒序) | +| 时间单位 | ISO8601 | 毫秒时间戳 | +| job 结构 | 平铺 `/jobs` | 嵌套 `stages[].jobs[]` | +| job 排队起点 | `created_at` | `apply_time`(部分为 null,需兜底) | +| 鉴权 | `Authorization: Bearer` | 同样用 Bearer;**不要用 `access_token` 查询参数**,会进访问日志 | + +**部署形态** + +Deployment 单副本,`/metrics:8080`;状态落 `/data/state.json`,PVC 1Gi `ReadWriteOnce` + `strategy: Recreate`(单副本前提下滚动升级不会出现两个 Pod 同时写)。不用 CronJob + Pushgateway:短生命周期进程的计数器每轮从 0 开始,`increase()` 只会看到重置。 + +状态文件写入用 tmp + fsync + rename 原子替换,每 30 秒或累积 200 条事件刷一次。 + +#### 5.4.2 ghalistener + +**它是什么** + +ARC 每个 scale set 自带一个 listener,直连 GitHub Actions 的消息通道做派单,同时暴露 Prometheus 指标。**它是 ARC 的组成部分,不能停、不能改**,metrics 端点由 controller 以 `--listener-metrics-addr` 下发(参数在 Secret 里的 config.json)。 + +**现状(2026-10-23 实测)** + +| 项 | 状态 | +| --- | --- | +| 二进制 | 全集群在跑,`arc-systems` 下 7 个集群合计 387 个 pod | +| metrics 端口 | pod 声明 `metrics:8080`,controller values 的 `metrics` 段已打开 | +| 指标声明 | 536 个 scale set 中 **500 个**声明了 `listenerMetrics`,36 个为空(`installMetrics` 只注册声明过的名字,不报错) | +| 中心采集 | **完全没有**,中心 Prometheus 里 `gha_*` 序列数为 0 | + +**二期 A:只加抓取(零改动)** + +在各集群 `prometheus-agent-configmap-patch.yaml` 增加一个 job: + +```yaml + - job_name: arc-listener + kubernetes_sd_configs: + - role: pod + api_server: https://kubernetes.default.svc + authorization: + type: Bearer + credentials_file: /etc/prometheus/sa-token/token + tls_config: + insecure_skip_verify: true + relabel_configs: + - source_labels: [__meta_kubernetes_namespace] + regex: arc-systems + action: keep + - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_component] + regex: runner-scale-set-listener + action: keep + - source_labels: [__meta_kubernetes_pod_label_actions_github_com_repository] + target_label: repository + - source_labels: [__meta_kubernetes_pod_label_actions_github_com_scale_set_name] + target_label: scale_set + - target_label: cluster + replacement: <集群名> + metric_relabel_configs: + - source_labels: [__name__] + regex: "gha_(assigned|running)_jobs|gha_(registered|busy|min|max|desired|idle)_runners" + action: keep ``` -三条注意: +三件事不用改: + +- **RBAC**:`base/monitoring-clusterrole.yaml` 已有全局 `pods: get/list/watch`; +- **remote_write**:现有丢弃规则按 namespace 匹配 `ascend-gha-runners`,listener 在 `arc-systems`,不受影响; +- **补充声明**:二期 A 只抓 gauge,36 个未声明的 scale set 里 gauge 也是空的,可先接受。 + +产出约 8 × 387 ≈ 3,000 条序列。 + +**二期 B:打开时长指标(需要改 values)** + +必须**从 `listenerMetrics` 声明里去掉 `job_workflow_ref`**,不能在抓取侧 `labeldrop`: + +- 该标签值形如 `owner/repo/.github/workflows/x.yml@refs/pull/123/merge`,ref 段无界; +- 抓取侧删标签会让同一 job 的 main 分支和 PR 分支变成两条同标签序列,Prometheus 报重复样本并丢弃其中一条,**counter 静默少算**; +- 从声明里去掉,listener 自己在源头就不按 ref 拆维度,counter 正确相加。 + +顺带把 46 个 bucket 精简到约 10 个。基数从约 1,960 条/listener 降到约 500 条/listener。 + +**风险:改 values 会重建 listener pod** + +`listenerMetrics` 写进 listener 的 config Secret,改它等于换配置。listener 重建期间该 scale set 接不到新 job,job 会一直排队。500 个一起滚就是全网 500 个 scale set 同时短暂失能。 + +必须按集群/按仓库分批,避开 CI 高峰,每批之后观察 `gha_assigned_jobs` 与排队指标。 + +#### 5.4.3 集群 event agent(三期) + +**定位** + +补 ghalistener 和轮询都拿不到的东西:**run 身份**和**重跑**。它不替代任何人。 + +| 它补什么 | 为什么别的源不行 | +| --- | --- | +| run/job 身份 | `gha_*` 的标签里没有 `run_id` / `job_id` | +| 重跑 | API 列表按创建时间排序,重跑旧 run 不会回到首页 | +| 精确完成时刻 | 轮询有 60s~5min 延迟 | + +**watch 对象** + +`actions.github.com/v1alpha1` 的 `EphemeralRunner`。它的 status 含 `WorkflowRunId`、`JobID`、`JobRepositoryName`、`JobWorkflowRef`、`JobDisplayName`、`Phase`、`Ready`、`Failures`;**没有时间戳**,所以创建/就绪时刻取 watch 事件的观测时间。 + +**事件模型** + +```json +{ + "platform": "github", + "cluster": "guiyang-004", + "repo": "vllm-project/vllm-ascend", + "run_id": "123456789", + "job_id": "987654321", + "job_name": "build-image", + "phase": "completed", + "observed_at": "2026-10-23T08:00:00Z" +} +``` -1. **样本量下限(≥ 20)不能省**:夜间只有零星几次运行,挂 1 次就是 50%。 -2. 单次失败**故意不告警**,只认"批量"和"连续",否则正常偶发失败会淹没通知。 -3. `CIWorkflowNoSuccess` 要**排除低频仓库**(白名单或"近 7 天运行数 ≥ N"才纳入),否则一周跑一次的仓库会天天报。 - -结果指标真正的价值是**区分责任边界**,判读规则(不是告警): - -- 同一时间段**多个仓库一起失败**,且 `github_probe_success == 0` → 我方链路问题(DNS / 代理 / 白名单 / 限流); -- **只有单个仓库失败** → 该仓库代码或 workflow 自身问题; -- **多个仓库同一 job 名失败** → runner 镜像或共享缓存的问题,去看 `runner_version_update_available` 与缓存组件。 - -#### 5.1.5 部署与运维 - -- **形态用 Deployment,不用 CronJob + Pushgateway**:Pushgateway 本身能存 counter,问题出在推送方—— - CronJob 是短生命周期进程,计数器每轮从 0 开始,推上去的值反复归零,`increase()` 只会看到重置。 - 要用就得自己持久化累计值,另外还要处理 Pushgateway 不做 TTL(进程停了指标停在最后一个值, - 现有探针靠 `push_time_seconds` + `*ProbeStale` 兜底)和多副本写同一个 group 互相覆盖。 - Deployment 单副本暴露 `/metrics:8080` 由中心 Prometheus 抓取,这些都由抓取模型天然解决: - 计数器重置 `rate()` 自己认,target 没了看 `up`。 -- **状态存 PVC**:已计数集合与每仓 `last_success_timestamp` 落在 `/data/state.json`,PVC 用 ReadWriteOnce、 - `strategy: Recreate`——单副本的前提下,滚动升级不会出现两个 Pod 同时写同一份去重状态。 - 这个文件的完整生命周期见 5.1.2「跨窗口去重」。 -- **部署位置**:中心集群的监控命名空间,与现有中心 Prometheus 同网段,能出网访问 `api.github.com`; - 走代理的出口要确认代理放行 GitHub API(`github-probe` 的 `path="proxy"` 就是这条链路)。 - **只部署这一份**:结果只存在于 GitHub、与哪个集群执行无关,每集群一份会重复拉同一批 run(API 额度 ×14) - 还要把 token 分发到 14 个集群;`cluster` 标签是从 job 的 `labels` 反查出来的,不依赖部署位置。 - 代价是单点——它挂了就全量失明,所以下面的自监控是必需的,不是可选项。 -- **与本仓其他数据源的分工**:结果看 GitHub(本章);排队与调度看 `RunnerPodPendingTooLong`、`ci_runner_active_jobs`; - Pod 的等待 / 运行时长看 pod-history-api。三者不互相替代。 - -### 5.2 日志接入 - -CI workflow 的日志有两条,来源不同,不要混: - -- **job 的步骤输出在 GitHub**(`/actions/runs/{run_id}/logs`,打包 zip),不进集群 LTS。GitHub 侧自己有保留期, - 不建议再往 LTS 灌一份——量大、且和 GitHub 的保留策略对不上。 -- **集群侧只有 runner Pod 自身的 stdout**(分配 job、拉镜像、连不上 GitHub 等),已经在 LTS 里, - 适合排查"runner 起不来"这类问题,看不到构建过程。 - -所以 CI workflow 的日志接入**只做到 runner Pod 这一层就够**,构建失败的具体原因回 GitHub 看。 +事件里**只带 job 级事实**(`cluster`、`repo`、`run_id`、`job_id`、`phase`),**不带 `conclusion`、不带 `run_attempt`**: + +- agent 判断不了 run 级结论——一条 run 的 job 可能跨多个集群,它只知道自己这几个 job 结束了; +- agent 也不知道 attempt。 + +两者都由 aggregator 收到事件后按 `run_id` 取一次 `GetRun` 拿权威值,再决定是否入账。 + +**协议与幂等** + +- agent → aggregator:`POST /ingest`,批量提交,带共享 token(Secret 挂载)。 +- aggregator 幂等键:`:::::`。 +- 同一 `(run_id, attempt)` 的 run 级指标只记一次,无论来自轮询还是事件。 + +**断线与回补** + +- 用 list-watch + `resourceVersion` 续接;连接断开后重新 list 对齐,避免漏事件。 +- 本地磁盘缓冲最近 N 条未确认事件,重试指数退避。 +- **事件不是唯一依赖**:agent 挂掉时轮询仍在跑,只是退化为一期的能力;所以 agent 失效不该产生「数据全空」,只影响重跑补齐。 + +**部署** + +每集群一个 Deployment(单副本),RBAC 只需 `ephemeralrunners: get/list/watch`(`base/monitoring-clusterrole.yaml` 已有)。它同时可以取代现有的 `runner-job-probe` CronJob——那个每 2 分钟全量 list 再 push 到 Pushgateway,是快照而不是事件流。 + +**与 GitCode 的关系(待定)** + +GitCode 的 CI 跑在自建 runner 上,终态与 GitHub 用同一套 runner 集群,但由**另一个编排器**创建 pod(ARC 不可能服务 GitCode)。所以三期对 GitCode 的可行性取决于两件事: + +1. 那个编排器把 run/job 身份写进 pod 的 label 或 annotation 了吗? +2. 它在 k8s 里以 CR 还是裸 Pod 存在? + +有身份 → 复用归一事件模型,再加一个 watch 源即可;没有 → 集群侧只能到 Pod 粒度,对不上 run,那 GitCode 半边放弃集群侧深度。 + +#### 5.4.4 核心流程 + +**流程一:一次普通 run** + +``` +GitHub/GitCode 创建 run + │ + ├─ 全程 < 5 分钟:A(60s 内看到)→ 首页去重 → 计数 → 抓 /jobs 出 job 级 + │ + └─ 全程 > 5 分钟:B(5min 采样)进入 registry + ├─ 每轮刷新在途 gauge(排队数、最久排队时长) + ├─ 每轮对活跃 run 抓 /jobs → 最久 job 排队时长 + └─ 列表里消失 + 本轮请求成功 → GetRun 按 ID 核实 → 入账 +``` + +**流程二:长任务不掉队** + +GitHub 的列表按 `created_at` 倒序。实测 `vllm-ascend` 首页 100 条只覆盖 23 分钟——**创建后跑满 25 分钟以上的 run,完成时早已被挤出首页**,A 永远看不到。 + +所以长 run 的终态只能由 B 提供:它在还在跑的时候被采样进 registry,等它结束时按 ID 取回。这也是为什么 B 不是「可选的补齐」,而是长任务的主路径。 + +**流程三:重跑** + +一期**接受缺口**:`Re-run` 不新建 run,只把 `run_attempt` 加一,`created_at` 不变,因此重跑旧 run 既不会出现在 A 的首页,B 也只在它恰好在跑且被采样到时才看得到。 + +影响是 `run_result_total` 少计这批结果,失败率在「依赖重跑救回来」的场景下偏高。三期由 event agent 修复:每次重跑都会新建 EphemeralRunner,事件必经。 + +**流程四:告警时延** + +| 阶段 | 最坏 | 说明 | +| --- | --- | --- | +| 采集(A) | 60s | 轮询间隔 | +| 抓取 | 60s | 中心 `scrapeInterval` | +| 规则评估 | 60s | 未配 `evaluationInterval` | +| `group_wait` | 10s | Alertmanager | +| **合计** | **~3.2 min** | 不含 `for:` | + +在途告警(排队过久、执行过久)走的是 B 的瞬时 gauge,**只需要「采样周期 + 抓取 + 评估」**:B 是 5 分钟一次,所以最坏约 7 分钟。对「排队 30 分钟」「执行 2 小时」这类阈值足够;要更快只能把 B 提到 60 秒,调用量翻 5 倍,不值。 + +**流程五:GitCode 接入** + +同一套 aggregator,新增一个适配器: + +1. `ListRuns` 把「已完成」拆成 `status=COMPLETED` 与 `status=FAILED` 两次调用(或本地过滤); +2. `OrderKey` 用 `start_time`; +3. job 从 `stages[].jobs[]` 展开为平铺列表; +4. job 排队 = `start_time − apply_time`,`apply_time` 为 null 时跳过该 job 的排队样本; +5. 重跑与长任务补齐逻辑**先用实测确认是否需要**——GitCode 按 `start_time` 排序,长 run 不会因为新 run 创建而变老,原样照搬 GitHub 的补齐逻辑可能是多余的。 + +### 5.5 已知边界 + +| 边界 | 影响 | 何时解决 | +| --- | --- | --- | +| 重跑旧 run 在一期看不到 | 失败率偏高 | 三期 | +| GitCode 的 run 级排队状态未验证 | 在途排队告警可能只有 GitHub 侧有效 | 接入 GitCode 前实测 | +| ghalistener 只覆盖 GitHub | GitCode 半边没有容量指标,也没有 ②+③ 启动耗时 | 取决于 GitCode runner 形态 | +| 非 ARC 执行的 run 不在集群侧视野 | event agent 只覆盖自建 runner | 接受 | +| aggregator 是单点 | 挂了就全量失明 | 用 5.2.3 的自监控兜底 | +| `total_count` 不可信 | 不能做精确对账 | 已改为四个守卫 | ## 6. Jenkins 任务