# 进程与健康 控制台「运行日志 → 进程与健康」的数据来源。后端在 `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 = ::`,容器重启覆盖同一行。 - 正常退出(SIGTERM / SIGINT)时写入器把本行标 `stopped_at`,读取方立即归入已下线实例;重启后同一行的 upsert 把它清回 null。 - 裁剪由定时任务 `system.heartbeat_cleanup`(维护组,1 小时)负责:24 小时没心跳的进程行删除,采样保留 7 天。 - 写库失败只记 debug 日志,不影响业务。 ## GET /system/health 字段 | 字段 | 含义 | | --- | --- | | `level` | 各段最差档:`ok` / `warn` / `critical`。取不到数据的段是 `unknown`,不参与汇总 | | `api` | 处理本次请求的 API 进程:版本、启动时间、运行秒数 | | `processes[]` | 当前实例:最近 10 分钟内有心跳的 host 上、同 host 同 kind 最新启动且没标 `stopped_at` 的进程 | | `processes[].stale` | 超过 45 秒没心跳。只有当前实例参与告警 | | `processes[].stoppedAt` | 正常退出时间;非 null 即已下线 | | `retiredProcesses[]` | 已下线实例:正常退出的、所在 host 10 分钟没有心跳(容器已重建)的、被同 host 新进程替换的。不计入 `level`,页面折叠显示 | | `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 就去查容器状态和日志。容器整个没了 10 分钟后它的进程转入已下线实例,此时靠下一条"没有存活的 worker"兜底。 - 没有任何存活的 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": "", "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 很多时要带上时间范围。