Files

210 lines
8.9 KiB
Markdown
Raw Permalink Normal View History

# 脚本批量执行平台 — 技术说明(SSOT)
**最后更新**: 2026-05-22
**状态**: 已实现,待本地 / 外网 E2E 验证
## 1. 目标与约束
| 目标 | 实现方式 |
|------|----------|
| 2000+ 台批量执行 | 内置分批(默认 50 台/批),批内并发 10 |
| 长任务不阻塞 HTTP | `nohup` + 子机结束 `curl` 主站回调 |
| 短任务执行中可停止 | Agent `wait=false``pid``/exec/kill` |
| 减轻 MySQL 压力 | 执行态**只写 Redis**MySQL **60s 分批刷盘** |
| 全链路可审计 | `audit_logs` 即时写入;`results._meta.events` 时间线 |
| 不用 WS 换票 / 异步 exec API | 同步 `POST /api/scripts/exec` + 页顶状态条轮询 |
**不可改 / 环境项**`.env`):
- `NEXUS_API_BASE_URL` — 长任务子机回调主站地址(必填,否则长任务 400)
- `NEXUS_SCRIPT_EXEC_BATCH_SIZE`(默认 50)、`NEXUS_SCRIPT_EXEC_CONCURRENCY`(默认 10
**代码内常量**`server/infrastructure/redis/script_execution_store.py`,不写 `.env`):
| 常量 | 值 | 含义 |
|------|-----|------|
| `REDIS_TTL_SECONDS` | 7200 | 热数据最长 2h |
| `FLUSH_INTERVAL_SECONDS` | 60 | Redis→MySQL 刷盘周期 |
| `FLUSH_BATCH_LIMIT` | 50 | 每轮最多刷 50 条执行记录 |
| `STUCK_NO_PROGRESS_SECONDS` | 3600 | 卡住检测阈值(1h |
## 2. 架构与数据流
```mermaid
flowchart LR
UI[scripts.html / script-executions.html]
API[POST /api/scripts/exec]
SVC[ScriptService]
Redis[(Redis script_exec:id)]
Flush[script_execution_flush 60s]
MySQL[(script_executions)]
Agent[子机 Agent :8800]
CB[POST /api/agent/script-callback]
UI --> API --> SVC
SVC --> Redis
SVC --> Agent
Agent -->|长任务结束 curl| CB --> Redis
Flush --> MySQL
UI -->|GET executions/id| Redis
UI -.->|历史列表可能滞后| MySQL
```
### 2.1 Redis 热数据
- Key: `script_exec:{execution_id}` — JSON 全量快照(status、results、events、operator…)
- 队列: `script_exec:flush_queue` — 待刷 MySQL 的 id 集合
- 长任务回调: `script_job:{job_id}` — 一次性 secretTTL 7 天,见 `script_jobs.py`
### 2.2 MySQL 冷数据
- 表: `script_executions`
- `results` JSON`{ "<server_id>": { ... }, "_meta": { "events": [...] } }`
- 刷盘后从 flush 队列移除;读接口**优先 Redis**,无 Redis 再读 MySQL
### 2.3 进度字段
- API 字段 `progress`: `"done/total"``pending` 计为未完成)
- 实现: `execution_progress_summary()` in `script_execution_store.py`
## 3. 执行模式
### 3.1 短任务(默认)
1. 主站 `POST` 子机 Agent `/exec` + `wait: false` → 得 `pid`**立即** `patch_live` 写入 Redis`phase: pending`
2. 主站 `POST` `/exec/wait` 等待结束
3. 用户点「停止」→ 主站 `POST` `/exec/kill`(需**新版 Agent**
4. 未升级 Agent:回退同步 `/exec` `wait=true`**无法**中途停止
### 3.2 长任务(`long_task: true`
1. 命令存库前缀 `[long_task] `
2. `script_jobs.build_long_task_command()``nohup` + 日志 `/var/log/nexus-job/{job_id}.log` + 结束 curl 主站
3. 点火成功 → `phase: pending` + `job_id` + `started_at`
4. 子机 `POST /api/agent/script-callback` → 更新 results + WebSocket `script_job_done`
5. 停止:kill `${LOG}.pid` + 撤销 Redis job secret
## 4. HTTP APIJWT,除回调)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/scripts/exec-config` | `batch_size`, `concurrency` |
| POST | `/api/scripts/exec` | `server_ids`, `command`, `timeout`, `long_task`, `credential_id?` |
| GET | `/api/scripts/executions` | 分页历史 `limit`, `offset`, `status` |
| GET | `/api/scripts/executions/{id}` | `?fetch_logs=true` 拉子机 tail 日志 |
| POST | `/api/scripts/executions/{id}/stop` | 停止 pending |
| POST | `/api/scripts/executions/{id}/retry` | 失败/未完成重试;**新建** `execution_id`,响应含 `parent_execution_id` |
| POST | `/api/scripts/executions/{id}/mark-stuck` | 人工标记卡住 |
| POST | `/api/agent/script-callback` | `job_id`, `secret`, `server_id`, `exit_code`(无 JWTRedis 限速) |
## 4.1 安全与执行面
| 风险 | 现状 | 缓解 |
|------|------|------|
| 子机 `subprocess_shell` / 长任务 `bash -c` | 运维必需能力 | 仅 API Key 可调 `/exec`;主站 `check_dangerous_command` **拒绝**危险模式;Agent 每次 exec 打 WARNING 审计日志 |
| 回调 HTTP 明文 | 生产配置错误 | `master_callback_url()` 非 localhost 强制 `https://` |
| 回调刷接口 | 无 JWT | `script_callback_rate`:每 job 10/min、每 IP 60/min |
| 重试语义 | 每次 retry 新建记录 | API 返回 `parent_execution_id` 关联原单;历史页按 id 追踪 |
子机应网络隔离:仅中心机 IP 可访问 Agent `:8800`
## 5. 子机 Agent 接口
文件: `web/agent/agent.py`
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/exec` | `wait` 默认 truefalse 返回 `pid` |
| POST | `/exec/wait` | 等待 `pid` |
| POST | `/exec/kill` | 终止 `pid` |
**部署**:各子机更新 Agent 后重启;否则短任务 stop 不可用。
## 6. 后台任务
- 模块: `server/background/script_execution_flush.py`
- 注册: `server/main.py``script_execution_flush_loop()`primary worker
- 每 60s
1. `detect_stuck_executions()` — 单台 pending 超 1h → `server_auto_stuck`;整单无更新 1h → `auto_stuck`
2. `flush_pending_batch()` — 最多 50 条写入 MySQL
## 7. 审计(`audit_logs.action`
| action | 触发场景 |
|--------|----------|
| `execute_started` | 创建执行记录 |
| `execute_command` | 记录实际下发命令摘要 |
| `stop_execution` | 用户停止 |
| `retry_execution` | 用户重试 |
| `mark_stuck` | 用户标记卡住 |
| `script_job_callback` | 长任务子机回调 |
| `auto_stuck` | 整单 1h 无 Redis 更新 |
| `server_auto_stuck` | 单台 pending 超 1h |
前端筛选: `web/app/audit.html` 已增加上述选项。
## 8. 前端页面
| 页面 | 路径 | 功能 |
|------|------|------|
| 脚本库 | `web/app/scripts.html` | 执行弹窗、分批、长任务勾选、页顶「批量执行状态」 |
| 执行历史 | `web/app/script-executions.html` | 列表、进度、停止/重试/卡住、详情+日志、10s 轮询 |
| 侧栏 | `web/app/layout.js` | 菜单项「执行历史」 |
页顶状态条按钮:刷新状态、状态+日志、停止、重试、标记卡住。
## 9. 涉及文件清单
### 后端
| 文件 | 职责 |
|------|------|
| `server/application/services/script_service.py` | 分批执行、Agent 调用、stop/retry/list |
| `server/application/services/script_jobs.py` | 长任务 nohup + job Redis |
| `server/application/services/script_job_callback.py` | 回调合并结果 |
| `server/infrastructure/redis/script_execution_store.py` | Redis 热数据、刷盘队列、卡住检测、progress |
| `server/background/script_execution_flush.py` | 60s 循环 |
| `server/infrastructure/database/script_repo.py` | `list_recent`, `update_status` |
| `server/api/scripts.py` | REST 路由 |
| `server/api/agent.py` | `script-callback` |
| `server/config.py` | `SCRIPT_EXEC_BATCH_SIZE`, `SCRIPT_EXEC_CONCURRENCY` |
| `server/main.py` | 注册 flush 后台任务 |
### 前端 / Agent
| 文件 | 职责 |
|------|------|
| `web/app/scripts.html` | 执行 UI + 状态条 |
| `web/app/script-executions.html` | 历史页 |
| `web/app/layout.js` | 侧栏 |
| `web/app/audit.html` | 审计筛选 |
| `web/agent/agent.py` | exec / wait / kill |
## 10. 已知限制
1. MySQL 视图比 Redis **最多晚 60s**(直接查库时);页面轮询读 Redis 无影响。
2. 短任务 stop 依赖子机 Agent 新版本。
3. 自动卡住:单台按 `started_at`;整单按 `updated_at`(非单台日志活跃度)。
4. 危险命令仍由 `check_dangerous_command` 拦截。
5. 长任务依赖 `NEXUS_API_BASE_URL` 对子机可达,且生产须为 HTTPS(本地可用 `http://localhost`)。
6. **重试**会创建新的 `script_executions.id`;原单结果保留,通过 `parent_execution_id` 关联。
## 11. 测试清单(待执行)
- [ ] T-SE1: 选 3 台短任务,页顶进度 `3/3`,审计有 `execute_started`
- [ ] T-SE2: 选 60 台,确认分 2 批、合并一条状态卡
- [ ] T-SE3: 长任务 + 配置 `NEXUS_API_BASE_URL`,回调后状态变完成、WS 通知
- [ ] T-SE4: 长任务执行中「停止」,子机进程结束
- [ ] T-SE5: 新版 Agent 短任务执行中「停止」
- [ ] T-SE6: 执行历史页停止/重试/状态+日志
- [ ] T-SE7: 等待或调低 `STUCK_NO_PROGRESS_SECONDS` 验证 `server_auto_stuck` / `auto_stuck`
- [ ] T-SE8: Redis 有 key 时 MySQL 在 60s 内出现对应行
## 12. 相关 Changelog
| 文档 | 内容 |
|------|------|
| [2026-05-22-script-execution-platform.md](../changelog/2026-05-22-script-execution-platform.md) | 本次交付总览与文件变更索引 |
| [2026-05-22-script-exec-batching.md](../changelog/2026-05-22-script-exec-batching.md) | 内置分批与 exec-config |
| [2026-05-22-script-job-callback.md](../changelog/2026-05-22-script-job-callback.md) | 长任务回调与 Redis 刷盘摘要 |