feat: add process resource monitor

This commit is contained in:
Codex
2026-05-06 06:09:29 +00:00
parent cd784f5e4b
commit a0b9f8fb97
13 changed files with 543 additions and 19 deletions
+5 -5
View File
@@ -20,7 +20,7 @@ Provider Gateway 是计算节点侧容器。它只主动连出到主 server 暴
当前主 server 公网 IP 是 `74.48.78.17``config.json` 中的 `network.publicHost` 必须保持为该地址;公网 frontend 入口是 `http://74.48.78.17:18081/`provider gateway 对外接入入口是 `ws://74.48.78.17:18082/ws/provider`provider ingress 健康检查是 `http://74.48.78.17:18082/health`。主 server 本机 provider 由根目录 `docker-compose.yml``provider-gateway` 服务启动,容器内使用 Docker 内网地址 `ws://backend-core:8081/ws/provider` 自接入;外部计算节点部署 provider-gateway 时必须改用公网 provider ingress URL,并复用 `config.json` / `.state/docker-compose.env` 中的 provider token、心跳间隔和重连参数。
计算节点部署 provider-gateway 的最小方法是:准备可运行 `unidesk_provider-gateway` 镜像的 Docker 环境,为节点分配唯一 `PROVIDER_ID` 与可读 `PROVIDER_NAME`,设置 `PROVIDER_SERVER_URL=ws://74.48.78.17:18082/ws/provider``PROVIDER_TOKEN``PROVIDER_LABELS_JSON``HEARTBEAT_INTERVAL_MS``RECONNECT_BASE_MS``RECONNECT_MAX_MS`,并挂载 `/var/run/docker.sock:/var/run/docker.sock` 作为 Docker 状态采集、任务执行和远程升级的唯一自动化通道。所有长期接入节点都必须配置 `PROVIDER_UPGRADE_*` 环境变量,把节点上的 UniDesk 仓库只读挂载到 `PROVIDER_UPGRADE_WORKSPACE_PATH`,并确保升级命令只重建 `provider-gateway` service,不影响 database、backend-core、frontend。provider-gateway 容器必须使用 Docker restart policy `always`Compose 写法是 `restart: always``docker run` 写法是 `--restart always`。provider-gateway 部署必须同时交付 Host SSH / WSL SSH 透传维护桥;WSL 节点应设置 `HOST_SSH_HOST=host.docker.internal``HOST_SSH_PORT=22``HOST_SSH_USER=<WSL 用户>``HOST_SSH_KEY=/run/host-ssh/id_ed25519``HOST_REMOTE_CWD=/home/<WSL 用户>`,并把只含维护私钥的宿主目录只读挂载到 `/run/host-ssh`
计算节点部署 provider-gateway 的最小方法是:准备可运行 `unidesk_provider-gateway` 镜像的 Docker 环境,为节点分配唯一 `PROVIDER_ID` 与可读 `PROVIDER_NAME`,设置 `PROVIDER_SERVER_URL=ws://74.48.78.17:18082/ws/provider``PROVIDER_TOKEN``PROVIDER_LABELS_JSON``HEARTBEAT_INTERVAL_MS``RECONNECT_BASE_MS``RECONNECT_MAX_MS`,并挂载 `/var/run/docker.sock:/var/run/docker.sock` 作为 Docker 状态采集、任务执行和远程升级的唯一自动化通道。为了让 `资源监控` 能看到节点级进程占用,provider-gateway 容器还必须运行在宿主 PID namespaceCompose 写法是 `pid: "host"``docker run` 写法是 `--pid host`;缺少该配置时只能看到 provider 容器命名空间内的进程,不能视为完整节点资源监控。所有长期接入节点都必须配置 `PROVIDER_UPGRADE_*` 环境变量,把节点上的 UniDesk 仓库只读挂载到 `PROVIDER_UPGRADE_WORKSPACE_PATH`,并确保升级命令只重建 `provider-gateway` service,不影响 database、backend-core、frontend。provider-gateway 容器必须使用 Docker restart policy `always`Compose 写法是 `restart: always``docker run` 写法是 `--restart always`。provider-gateway 部署必须同时交付 Host SSH / WSL SSH 透传维护桥;WSL 节点应设置 `HOST_SSH_HOST=host.docker.internal``HOST_SSH_PORT=22``HOST_SSH_USER=<WSL 用户>``HOST_SSH_KEY=/run/host-ssh/id_ed25519``HOST_REMOTE_CWD=/home/<WSL 用户>`,并把只含维护私钥的宿主目录只读挂载到 `/run/host-ssh`
## Mandatory SSH Passthrough Bundle
@@ -38,7 +38,7 @@ WSL 节点应优先使用 WSL 内部原生 Docker Engine 和 `/var/run/docker.so
WSL provider 的最小环境文件应放在节点本地私有路径,例如 `/home/ubuntu/unidesk/.state/provider-<ID>.env`,并由 `docker run --env-file` 读取。`PROVIDER_LABELS_JSON` 在 Docker env-file 中可以写成单行 JSON;如果临时用 shell `source` 方式调试,必须对整段 JSON 加引号,否则 shell 会按 `{}` 和逗号拆分导致 JSON 解析失败。WSL 节点建议至少包含这些 labels:`host``role=wsl-provider``wsl=true``distro``docker=true`;运行时 provider-gateway 会自动追加 `runtime``dockerSocketPresent``gatewayUptimeSeconds``.state/provider-<ID>.env``logs/provider-<ID>/` 和容器日志属于节点本地运行态,必须保持在 `.gitignore` 覆盖范围内,不能提交 provider token、登录态或运行日志。
长期运行推荐用 systemd 管理 provider-gateway 容器,而不是只在交互 shell 中运行 Bun 进程。systemd unit 的稳定形态是:`ExecStartPre=-docker rm -f unidesk-provider-gateway-<ID>` 清理同名旧容器,`ExecStart=docker run --restart always --name unidesk-provider-gateway-<ID> --env-file ... -v /var/run/docker.sock:/var/run/docker.sock -v /home/ubuntu/unidesk:/workspace:ro -v /home/ubuntu/unidesk/logs/provider-<ID>:/var/log/unidesk -v <ssh-key-dir>:/run/host-ssh:ro unidesk_provider-gateway:<id>``ExecStop=docker stop unidesk-provider-gateway-<ID>`,并设置 `Restart=always`。临时部署也必须使用 `docker run -d --restart always`,并保证容器名、env 文件、日志目录、SSH 私钥只读挂载和镜像 tag 都带上节点 ID,便于 frontend、Docker 状态、SSH 透传和本地排障互相对应。`provider.upgrade` 是长期接入节点的必备能力,provider-gateway 不提供 `PROVIDER_UPGRADE_ENABLED` 或等价禁用开关;如果节点缺少升级环境变量或 SSH 透传环境变量,必须修正节点部署,而不是在服务端接受只能预检、不能升级或不能维护透传的半成品状态。
长期运行推荐用 systemd 管理 provider-gateway 容器,而不是只在交互 shell 中运行 Bun 进程。systemd unit 的稳定形态是:`ExecStartPre=-docker rm -f unidesk-provider-gateway-<ID>` 清理同名旧容器,`ExecStart=docker run --restart always --pid host --name unidesk-provider-gateway-<ID> --env-file ... -v /var/run/docker.sock:/var/run/docker.sock -v /home/ubuntu/unidesk:/workspace:ro -v /home/ubuntu/unidesk/logs/provider-<ID>:/var/log/unidesk -v <ssh-key-dir>:/run/host-ssh:ro unidesk_provider-gateway:<id>``ExecStop=docker stop unidesk-provider-gateway-<ID>`,并设置 `Restart=always`。临时部署也必须使用 `docker run -d --restart always --pid host`,并保证容器名、env 文件、日志目录、SSH 私钥只读挂载和镜像 tag 都带上节点 ID,便于 frontend、Docker 状态、SSH 透传、进程资源表和本地排障互相对应。`provider.upgrade` 是长期接入节点的必备能力,provider-gateway 不提供 `PROVIDER_UPGRADE_ENABLED` 或等价禁用开关;如果节点缺少升级环境变量或 SSH 透传环境变量,必须修正节点部署,而不是在服务端接受只能预检、不能升级或不能维护透传的半成品状态。
WSL 本身会在没有前台进程时被 Windows 回收;如果该节点要作为长期在线算力,必须通过 Windows 启动项、计划任务或后台 `wsl.exe -d <distro> -u root -- bash -lc "systemctl start docker unidesk-provider-gateway-<ID>.service; exec sleep infinity"` 这类 keepalive 进程保持发行版运行。仅启用 WSL 内 systemd service 不等价于 Windows 层面的常驻守护。
@@ -98,7 +98,7 @@ provider-gateway 连接成功后必须周期性上报 Docker daemon 状态,数
## System Status Telemetry
provider-gateway 连接成功后必须周期性上报节点 CPU、内存硬盘用量。采集来源是节点本地 `/proc/stat``/proc/loadavg``/proc/meminfo``df -PB1`backend-core 将最新快照保存到 `unidesk_node_system_status`,并将历史采样保存到 `unidesk_node_metric_samples` 供 frontend 绘制任务管理器风格曲线。内存使用量采用实际占用口径:`MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem`,也就是不把 Linux page cache / buffer 计入占用;上报中同时保留 `cacheBytes` 便于排查。该链路仍然由 provider 主动上报,主 server 不反向探测计算节点。
provider-gateway 连接成功后必须周期性上报节点 CPU、内存硬盘和进程资源占用。整体采集来源是节点本地 `/proc/stat``/proc/loadavg``/proc/meminfo``df -PB1`进程表来源是 `/proc/[pid]/stat``/proc/[pid]/status``/proc/[pid]/cmdline``/proc/[pid]/io`backend-core 将最新快照保存到 `unidesk_node_system_status`,并将历史采样保存到 `unidesk_node_metric_samples` 供 frontend 绘制任务管理器风格曲线。内存使用量采用实际占用口径:`MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem`,也就是不把 Linux page cache / buffer 计入占用;上报中同时保留 `cacheBytes` 便于排查。进程表的 `rssBytes` 是实际常驻内存,默认按 `rssBytes` 降序截取前 120 个进程;`cpuPercent` 使用相邻采样 CPU tick 差值,首个采样用进程生命周期平均值兜底;磁盘 I/O 速率使用相邻 `/proc/[pid]/io``read_bytes/write_bytes` 差值。该链路仍然由 provider 主动上报,主 server 不反向探测计算节点。
## Remote Provider Upgrade
@@ -106,7 +106,7 @@ backend-core 可以通过真实 WebSocket 调度向在线 provider 下发 `provi
远程升级策略固定为 always-enabled:只要 provider-gateway 在线并声明 `provider.upgrade``mode: "schedule"` 就必须真正调度升级容器,不允许被 `PROVIDER_UPGRADE_ENABLED=false`、前端隐藏按钮或服务端特殊名单禁用。升级能力的安全边界不是开关,而是显式 `PROVIDER_UPGRADE_*` 配置、Docker socket 权限、只读仓库挂载、固定 Compose service 和 `--no-deps` 约束。升级计划中必须展示 `policy: "always-enabled"`、updater 容器名、runner image、workspace、Compose project/service、env file、compose file 和实际 `docker run` 命令,方便前端任务历史与 CLI debug 直接诊断。
`mode: "schedule"` 的成功返回只代表 updater 已被调度,最终升级成败由候选 gateway 自验证决定。updater 必须先按 Compose 构建新镜像,再用旧容器的 `Config.Env` 生成候选 env-file,并复用旧容器的 Docker socket、日志目录、SSH 私钥只读挂载、Compose 网络和 `extra_hosts`;候选容器启动时 restart policy 必须先是 `no`,验证通过后才能改成 `always` 并删除旧容器。升级计划的 `replacementStrategy` 必须包含 `oldGatewaySleepMs``validationTimeoutMs``promoteOnlyAfterCandidateValidation``candidateRestartPolicyAfterPromotion: "always"``candidateUsesOldContainerEnvironment``candidateUsesOldContainerMounts``candidateUsesOldContainerNetworks``candidateUsesOldContainerExtraHosts`,并且必须在 plan 中显示指定 Provider 的当前/目标 gateway 版本号,便于前端和 CLI 判断这不是旧的先删旧容器再 up 的危险流程。
`mode: "schedule"` 的成功返回只代表 updater 已被调度,最终升级成败由候选 gateway 自验证决定。updater 必须先按 Compose 构建新镜像,再用旧容器的 `Config.Env` 生成候选 env-file,并复用旧容器的 Docker socket、日志目录、SSH 私钥只读挂载、Compose 网络和 `extra_hosts`;候选容器启动时 restart policy 必须先是 `no`并显式使用 `--pid host` 保持节点级进程资源采集,验证通过后才能改成 `always` 并删除旧容器。升级计划的 `replacementStrategy` 必须包含 `oldGatewaySleepMs``validationTimeoutMs``promoteOnlyAfterCandidateValidation``candidateRestartPolicyAfterPromotion: "always"``candidateUsesOldContainerEnvironment``candidateUsesOldContainerMounts``candidateUsesOldContainerNetworks``candidateUsesOldContainerExtraHosts``candidateUsesHostPidNamespace`,并且必须在 plan 中显示指定 Provider 的当前/目标 gateway 版本号,便于前端和 CLI 判断这不是旧的先删旧容器再 up 的危险流程。
自动更新记录的权威来源是 backend-core 保存的 `provider.upgrade` 任务历史,而不是 provider-gateway 容器日志文件。frontend 必须按 Provider 聚合这些任务,并把状态、模式、task id、来源、耗时、策略、updater 容器摘要、失败原因和更新时间渲染为表格或卡片;完整 task/result JSON 只能由操作员点击 `查看原始JSON` 后查看。
@@ -116,7 +116,7 @@ backend-core 可以通过真实 WebSocket 调度向在线 provider 下发 `provi
手动升级只用于把旧节点 bootstrap 到支持 always-enabled 远程升级的版本;bootstrap 完成后,常规重建/升级必须回到 `provider.upgrade mode=schedule`,不得再用 SSH 透传同步重建 `provider-gateway`。节点侧维护步骤是:进入节点本地 UniDesk 仓库,执行 `git pull --ff-only` 获取主 server 已推送版本;确认 `.state/provider-<ID>.env` 中存在 `PROVIDER_SERVER_URL=ws://74.48.78.17:18082/ws/provider``PROVIDER_ID=<ID>``PROVIDER_NAME=<ID>``PROVIDER_TOKEN``PROVIDER_LABELS_JSON``PROVIDER_UPGRADE_HOST_PROJECT_ROOT=/home/ubuntu/unidesk``PROVIDER_UPGRADE_WORKSPACE_PATH=/workspace``PROVIDER_UPGRADE_COMPOSE_FILE``PROVIDER_UPGRADE_ENV_FILE``PROVIDER_UPGRADE_COMPOSE_PROJECT``PROVIDER_UPGRADE_SERVICE=provider-gateway``PROVIDER_UPGRADE_RUNNER_IMAGE=unidesk_provider-gateway:<id>``DOCKER_SOCKET_PATH=/var/run/docker.sock``MONITOR_DISK_PATH=/`、心跳和重连参数。旧 env 文件中如果还残留 `PROVIDER_UPGRADE_ENABLED`,新版 provider-gateway 会忽略它;长期文档和新部署不得再依赖这个键。
如果节点已有专用 Compose,优先用节点本地 Compose 手动重建一次:`docker compose --env-file .state/provider-<ID>.env -f <compose-file> -p <compose-project> up -d --no-deps --build --force-recreate provider-gateway`。这条命令必须在节点本地终端、节点自有 Web terminal、系统计划任务或 detached shell 中执行;不得通过正在被重建的 UniDesk provider-gateway 自己提供的 SSH 透传同步执行,否则旧 provider 容器停止时会切断 SSH client,可能导致重建中断在旧容器已停、新容器未起的状态。若只能通过 UniDesk 触达该节点,必须使用 `provider.upgrade mode=schedule` 的 detached updater,或先用节点本地 `nohup`/systemd 启动一个不依赖当前 provider 容器生命周期的重建脚本。老版 `docker-compose` 可能在重建已存在容器时因为 `ContainerConfig` 兼容问题失败;此时只能移除目标 provider-gateway 容器后重新 `up -d --no-deps provider-gateway`,不得执行 `down -v``docker volume rm` 或任何会影响 database 命名卷的命令。如果节点当前只有 `docker run` 部署,则先构建镜像 `docker build -f src/components/provider-gateway/Dockerfile -t unidesk_provider-gateway:<id> .`,再以固定容器名重建:挂载 `/var/run/docker.sock:/var/run/docker.sock``/home/ubuntu/unidesk:/workspace:ro`、节点日志目录到 `/var/log/unidesk`,如需 WSL SSH 维护桥还要把只读私钥目录挂载到 `/run/host-ssh`,并使用同一个 `.state/provider-<ID>.env` 启动。无论 Compose 还是 `docker run`,容器名和镜像 tag 都必须带 Provider ID,便于 Docker 状态页、任务历史和节点本地排障互相对应。
如果节点已有专用 Compose,优先用节点本地 Compose 手动重建一次:`docker compose --env-file .state/provider-<ID>.env -f <compose-file> -p <compose-project> up -d --no-deps --build --force-recreate provider-gateway`。这条命令必须在节点本地终端、节点自有 Web terminal、系统计划任务或 detached shell 中执行;不得通过正在被重建的 UniDesk provider-gateway 自己提供的 SSH 透传同步执行,否则旧 provider 容器停止时会切断 SSH client,可能导致重建中断在旧容器已停、新容器未起的状态。若只能通过 UniDesk 触达该节点,必须使用 `provider.upgrade mode=schedule` 的 detached updater,或先用节点本地 `nohup`/systemd 启动一个不依赖当前 provider 容器生命周期的重建脚本。老版 `docker-compose` 可能在重建已存在容器时因为 `ContainerConfig` 兼容问题失败;此时只能移除目标 provider-gateway 容器后重新 `up -d --no-deps provider-gateway`,不得执行 `down -v``docker volume rm` 或任何会影响 database 命名卷的命令。如果节点当前只有 `docker run` 部署,则先构建镜像 `docker build -f src/components/provider-gateway/Dockerfile -t unidesk_provider-gateway:<id> .`,再以固定容器名重建:使用 `--restart always --pid host`挂载 `/var/run/docker.sock:/var/run/docker.sock``/home/ubuntu/unidesk:/workspace:ro`、节点日志目录到 `/var/log/unidesk`,如需 WSL SSH 维护桥还要把只读私钥目录挂载到 `/run/host-ssh`,并使用同一个 `.state/provider-<ID>.env` 启动。无论 Compose 还是 `docker run`,容器名和镜像 tag 都必须带 Provider ID,便于 Docker 状态页、进程资源表、任务历史和节点本地排障互相对应。
手动升级完成后的判定标准固定为主 server 可观测结果,而不是节点容器 `running`:访问公网 frontend `http://74.48.78.17:18081/`,确认该 Provider 在线;随后在任意装有本仓库且 `config.json` 含正确 frontend 登录凭据的计算节点上执行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch <PROVIDER_ID> provider.upgrade --mode schedule --wait-ms 15000`,确认任务 `succeeded` 且 result 包含 updater 容器信息;最后再次查看 frontend 或执行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health`,确认节点重连、指标恢复、labels 中 `host.ssh` 能力存在。每个 provider-gateway 手动升级后都必须用 remote CLI 再执行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch <PROVIDER_ID> host.ssh --wait-ms 15000``bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh <PROVIDER_ID> hostname`,验证维护桥没有在升级后丢失;该 remote CLI 默认走公网 frontend,不需要指定 `--main-server-key`