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..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,9 +514,494 @@ Service 端口名命中白名单即可,无需写 job。以 oauth2-proxy 为例 ## 5. CI workflow -### 5.1 指标接入 +本章解决「CI 跑的结果怎么样、为什么慢、卡在哪」,覆盖两类业务: -### 5.2 日志接入 +- **参与型**:仓库在 GitHub,跑 GitHub Actions; +- **主导型**:仓库在 GitCode,跑 GitCode Actions。 + +两者共用同一套指标契约,靠 `platform` 标签(`github` / `gitcode`)区分,不做两套大盘、两套告警。 + +当前只有 runner 维度的在跑信息,见[CI 可观测现状 第 8 章](CI可观测现状.md)。本章是方案,尚未落地。 + +### 5.1 整体架构 + +#### 5.1.1 三个组件与两条既有通路 + +``` +┌──────────── 每个 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 │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +| 组件 | 位置 | 数量 | 数据源 | 产出 | +| --- | --- | --- | --- | --- | +| `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 产生。** 这里有**两层重复**,成因不同: + +- **跨集群 agent 之间**:一个 job 只在一个集群执行,各 agent 看到的 job 互不相交,本身不重复。但如果让每个 agent 各自算 run 级结论,同一条 run 就会在它涉及的每个集群各被记一次,而且每一份都是残缺的——agent 只知道"我这个集群的这几个 job 结束了",不知道整条 run 是否结束、结论是什么。一条 run 跨 6 个集群就会记 6 次。 +- **轮询与事件之间**:同一条 run 的终态会被两条路径各发现一次,这是真重复,由 aggregator 的幂等键 `:::` 合并。 + +#### 5.1.2 为什么必须三种采集并存 + +| 观测问题 | 谁回答 | 其他源为什么答不了 | +| --- | --- | --- | +| 这次 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 落不到首页 | + +#### 5.1.3 能力矩阵(平台 × 能力) + +| 能力 | GitHub | GitCode | +| --- | --- | --- | +| run/job 结果与耗时(轮询) | ✅ | ✅ 换适配器即可 | +| 在途排队/执行 gauge | ✅ | ⚠️ run 级排队状态待实测 | +| 容量 gauge、②+③ 启动耗时与 ④ 执行耗时(ghalistener) | ✅ | ❌ ARC 是 GitHub Actions 专用 | +| run 身份、重跑补齐(event agent) | ✅ | ❌ 无 `EphemeralRunner` 对应物 | + +两边观测深度**天然不对称**:GitHub 半边能看到「扩容慢还是平台派发慢」,GitCode 半边只有 API 视角。大盘上必须显式标注,否则「没数据」会被误读成「没跑任务」。 + +#### 5.1.4 阶段划分与数据源对应 + +一条 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_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` | 一轮内新增数占满整页(可能漏采) | + +#### 5.2.4 集群侧容量与阶段(ghalistener,二期) + +| 指标 | 类型 | 对应字段 | 用途 | +| --- | --- | --- | --- | +| `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`,不构成维度。 + +#### 5.2.5 事件侧(ci-event-agent,三期) + +三期**不新增业务指标**。事件路径不产出另一套指标,它做的是让 `ci_workflow_*` 更快、更全地拿到同一批事实:重跑补齐、精确完成时刻。入账仍在 aggregator,按幂等键合并,两条路径的产出用 `ci_workflow_runs_finalized_total{source}` 分开计数。agent 自身只需存活类自监控,不单列。 + +### 5.3 分期规划 + +| 期次 | 组件 | 观测能力 | 工期 | 主要风险 | +| --- | --- | --- | --- | --- | +| **一期** | 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 形态 | + +一期的两个适配器同步落地: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 | +| --- | --- | --- | --- | +| 成功 | `success` | `success` | `COMPLETED` | +| 失败 | `failure` | `failure` | `FAILED`(可能带后缀,取 `split("(")[0]`) | +| 取消 | `cancelled` | `cancelled` | `CANCELED` | +| 跳过 | `skipped` | `skipped` | `IGNORED` | +| 未定 | 原样保留 | — | `PAUSED` / `SUPPEND`,不并入成功/失败 | + +`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" +} +``` + +事件里**只带 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 任务