- process_heartbeat / process_heartbeat_sample (0034): api / worker / worker:agent / all
upsert every 15s (event loop lag last/max, concurrency, running runs, SSE connections, memory)
- GET /system/health (admin): processes with 45s stale flag, SSE, DB latency/size/connections,
disks (DATA_DIR / library roots / download dirs, cache thresholds), mount sentinel checks
with IO timeout, engine pending/oldest/24h failure rate, schedules
- GET /system/health/history?hours=: bucketed lag / running / SSE series
- /engine/runs and /engine/runs/{id}: business {kind, refId, title, triggeredBy} joined from
run input / task / agent_run / tasks_run_meta / outbox; businessKind / businessRefId filters
- docs/runbook/ops.md: field meanings and alert thresholds
Co-Authored-By: Claude Opus 5.5 <[email protected]>
90 lines
5.6 KiB
Markdown
90 lines
5.6 KiB
Markdown
# 进程与健康
|
||
|
||
控制台「运行日志 → 进程与健康」的数据来源。后端在 `apps/server/src/modules/system/`,两个端点都只给 admin。
|
||
|
||
## 心跳怎么写
|
||
|
||
| 进程 | 谁写 | `process_kind` |
|
||
| --- | --- | --- |
|
||
| `--role api` | system 模块注册路由时起写入器 | `api` |
|
||
| `--role worker`(io 组) | `worker/index.ts` | `worker` |
|
||
| `--role worker:agent` | `worker/index.ts` | `worker:agent` |
|
||
| `--role all` | `worker/index.ts`(同进程的 API 不再单写) | `all` |
|
||
|
||
- 每 15 秒 upsert `process_heartbeat` 一行,同时往 `process_heartbeat_sample` 追加一条采样。
|
||
- 行主键 `instance_id = <kind>:<host>:<pid>`,容器重启覆盖同一行。
|
||
- 写入方每小时裁剪一次:采样保留 7 天,7 天没心跳的进程行删除。
|
||
- 写库失败只记 debug 日志,不影响业务。
|
||
|
||
## GET /system/health 字段
|
||
|
||
| 字段 | 含义 |
|
||
| --- | --- |
|
||
| `level` | 各段最差档:`ok` / `warn` / `critical`。取不到数据的段是 `unknown`,不参与汇总 |
|
||
| `api` | 处理本次请求的 API 进程:版本、启动时间、运行秒数 |
|
||
| `processes[]` | 最近 24 小时有心跳的进程 |
|
||
| `processes[].stale` | 超过 45 秒没心跳 |
|
||
| `processes[].eventLoopLagMs` | 最近一次采样的事件循环延迟(1 秒采一次) |
|
||
| `processes[].eventLoopLagMaxMs` | 最近一个心跳周期(15 秒)内的最大延迟 |
|
||
| `processes[].concurrency` | 引擎 worker 并发 `{io, agent}`;api 进程为 `{}` |
|
||
| `processes[].runningRuns` | 本组任务正在执行的 run 数。同组多实例时是整组合计;api 进程为 null |
|
||
| `processes[].sseConnections` | 本进程打开的 SSE 流数。不托管 HTTP 的进程为 null |
|
||
| `processes[].extra` | `rssBytes`、`heapUsedBytes`、`lifetimeMaxLagMs`,worker 另有 `group`、`lagWarnings`、`probeMaxLagMs` |
|
||
| `sse.connections` | 存活 api/all 进程的 SSE 连接合计 |
|
||
| `db` | `select 1` 延迟、库大小、本库连接数(按 active / idle / idle in transaction 分)与 `max_connections` |
|
||
| `disks[]` | DATA_DIR、每个启用的库根、每个下载器路径映射的 statfs 用量 |
|
||
| `diskThresholds` | 取自设置 `cache.resumePercent`(warn)和 `cache.hardThresholdPercent`(critical) |
|
||
| `mounts[]` | 库根做哨兵检查(`checkRoot`),下载目录只查目录可读。`status`:`online` / `offline` / `timeout` |
|
||
| `engine` | 待处理 run 数、执行中 run 数、最老待处理时长、24 小时结束数 / 失败数 / 失败率 |
|
||
| `schedules` | 定时任务总数、启用数、执行中数、最近一次失败的启用任务、24 小时失败运行数 |
|
||
|
||
单个路径的 statfs 或哨兵检查超过 3 秒记 `timeout`:磁盘段标 critical,挂载段标 `timeout`。请求本身不会被卡住。
|
||
|
||
## GET /system/health/history?hours=
|
||
|
||
- `hours` 取 1–168,缺省 6。
|
||
- 按桶聚合,每条序列最多约 360 个点。桶宽最小 15 秒,168 小时时是 1680 秒。
|
||
- 每个进程一条序列,每个点有 `lagMs`(桶内平均)、`lagMaxMs`(桶内最大)、`runningRuns`、`sseConnections`。
|
||
|
||
## 告警阈值建议
|
||
|
||
- 进程 `stale`:warn。连续 2 分钟仍 stale 就去查容器状态和日志。
|
||
- 没有任何存活的 worker / all 进程:warn。任务不会被执行。
|
||
- 事件循环最大延迟 ≥ 1 秒记 warn,≥ 5 秒记 critical。延迟接近 15 秒时,引擎租约有丢失风险。
|
||
- DB 延迟 ≥ 100 ms 或连接数 ≥ 75% `max_connections` 记 warn;≥ 500 ms 或 ≥ 90% 记 critical。
|
||
- `idle in transaction` 连接超过 5 个记 warn,通常是有事务没提交。
|
||
- 磁盘用量 ≥ `cache.resumePercent`(缺省 85%)记 warn,≥ `cache.hardThresholdPercent`(缺省 90%)记 critical。
|
||
- 挂载根 `offline` 或 `timeout`:critical。先查宿主机挂载,再查哨兵文件。
|
||
- 最老待处理 run 等待 ≥ 5 分钟记 warn,≥ 30 分钟记 critical。通常是 worker 没起来或并发被占满。
|
||
- 24 小时失败率 ≥ 10% 记 warn;失败率 ≥ 30% 且结束数 ≥ 10 记 critical。
|
||
- 有启用的定时任务最近一次运行失败:warn。
|
||
|
||
## 引擎 runs 的业务字段
|
||
|
||
`GET /engine/runs` 和 `GET /engine/runs/{id}` 的每个 run 多了 `business`,推断不出时为 null:
|
||
|
||
```json
|
||
{ "kind": "feed", "refId": "<feed_source.id>", "title": "HDSky RSS", "triggeredBy": "event" }
|
||
```
|
||
|
||
- `kind`:`subscription` / `download` / `organization` / `agent` / `schedule` / `feed` / `site` / `decision` / `media`。
|
||
- `refId`:业务对象 id;`schedule` 是定时任务 key(如 `trash.purge`)。`download` 优先指向下载任务,计划还没提交时指向收录计划 id。
|
||
- `title`:业务对象的展示名。对象已删除时为 null。
|
||
- `triggeredBy`:`user` / `api_key` / `schedule` / `agent` / `event`,推断不出为 null。
|
||
|
||
`kind` 与 `refId` 的推断顺序(`auth-settings/engine-business.ts`,列表筛选和展示用同一条 SQL 表达式):
|
||
|
||
1. run 输入里的业务 id 键:`agentRunId`、`recordId`、`downloadTaskId`、`acquisitionPlanId`、`subscriptionId`、`acquisitionSlotId`、`feedSourceId`、`siteId`、`accountId`(换成站点)、`decisionId`、`mediaEntityId`。
|
||
2. 业务任务行 `task`(`engine_run_id` = run id)的 `subject_type` / `subject_id`。
|
||
3. 没有业务 id 的定时任务(`purgeTrash`、`refreshPiTokens`、`cleanupDownloadCache`、`reconcileLibrary`),且幂等键不是手动或重跑。
|
||
|
||
`triggeredBy` 的推断顺序:
|
||
|
||
1. Agent 会话看 `agent_run.trigger`。
|
||
2. 看 `task.triggered_by`。
|
||
3. 幂等键是 `retry:` 或含 `manual` 的记为 `user`。
|
||
4. 引擎幂等键能对上 outbox 事件的记为 `event`。
|
||
5. 调度器时间槽键记为 `schedule`。
|
||
|
||
筛选参数:`businessKind`、`businessRefId`,可以一起用。这两个参数对每行都要算一次推断表达式,runs 很多时要带上时间范围。
|