14 KiB
LandSAR 集群数据搬运与运维设计(2026-06-25)
背景
LANDSAR_CLUSTER_WORKER_DEPLOYMENT_20260624.md 已完成集群调度骨架:主服务器提交 LandSAR 集群任务 → 按 pair 拆分为 LANDSAR_CLUSTER_ITEM → 本机或远端 worker 通过 DB 队列领取执行。
该文档留下了两个明确缺口:
- 数据搬运:远端 worker 执行
engine.run()时读取item.source_task_dir(如D:\Task_Pool\DInSAR\Task_20250601_20250612\master),该路径在远端不存在。 - 结果回传:远端 worker 处理完成后,标准产品包在远端本地磁盘,不会自动进入主服务器 D-InSAR catalog。
本文档定义这两个能力的设计,以及 Windows 集群运维方案。
设计目标
- 不依赖 Windows 文件共享(SMB)、映射盘符、UNC 路径
- 不要求主服务器和 worker 共用盘符或路径结构;worker 可通过
CLUSTER_WORKER_TASK_ROOT、CLUSTER_WORKER_RESULT_ROOT使用本机任务/结果缓存根 - Worker 节点可以动态增减,配置简单
- 复用现有
SOURCE_PRODUCT_DIRS压缩包源池和TASK_POOL_ROOT体系 - 传输失败利用队列系统自带的重试机制
架构总览
主服务器 (192.168.1.62) 远端 Worker (192.168.1.6 / .7 / ...)
════════════════════════ ═══════════════════════════════════
[前端] 提交集群任务
│
▼
[Router] POST /landsar-cluster/run
│
▼
[Service] create_landsar_cluster_run()
│ 扫描 Task_* → 为每个 pair 创建
│ DinsarProductionRunItemORM +
│ LANDSAR_CLUSTER_ITEM 队列任务
▼
[system_jobs 表] ◄─────────── claim_next_job ────── [run_landsar_cluster_worker.py]
│
┌────▼──────────────────┐
│ 检查 source_task_dir │
│ 本地是否存在 │
├────有─────────────────┤
│ → 跳到 LandSAR 执行 │
├────无─────────────────┤
│ 1. GET /api/cluster/ │
│ input-package/ │
│ 2. 解包到本地路径 │
└───────────────────────┘
│
▼
LandSAR engine.run()
│
▼
┌───────────────────────┐
│ POST /api/cluster/ │
│ upload-result/ │
│ → 主服务器写入结果目录 │
│ → catalog 登记 │
└───────────────────────┘
数据搬运详细设计
阶段 1: Pre-flight 输入数据下载
触发时机:_handle_landsar_cluster_item 执行 engine.run() 之前。
Worker 端流程:
- 读取
item.source_task_dir,解析 worker 本地输入目录。未配置CLUSTER_WORKER_TASK_ROOT时沿用原路径;已配置时使用CLUSTER_WORKER_TASK_ROOT\item_<item_id>\Task_* - 若无 → 调用
GET /api/cluster/input-package/{item_id} - Worker 请求头携带
X-Cluster-Token,主服务器校验CLUSTER_SHARED_TOKEN - 主服务器读取
item.source_task_dir指向的现有 Task_Pool 目录 - 将该 Task_Pool 目录打成 zip 流式返回给 worker
- Worker 解包到本地输入目录
- Worker 对 zip 成员路径做目录逃逸校验,并校验解包后的
Input_Data或master/、slave/目录可用
主服务器新增 API:
GET /api/cluster/input-package/{item_id}
Header: X-Cluster-Token: <CLUSTER_SHARED_TOKEN>
Response: application/zip (streaming)
内容结构:
Task_YYYYMMDD_YYYYMMDD/
.dinsar_pair.json
master/
<源文件...>
slave/
<源文件...>
orbit/
<精轨文件...>
当前实现边界:主服务器不在传输接口里重新从 SOURCE_PRODUCT_DIRS 解包源压缩包;传输接口只打包已经由 Task_Pool 准备流程生成的 item.source_task_dir。如果主服务器上的 source_task_dir 不存在,接口返回 404,队列重试会保留失败信息。
阶段 2: Post-flight 结果上传
触发时机:LandSAR 执行成功、execution manifest 构建完成后。
Worker 端流程:
- LandSAR 完成后,收集标准产品包文件列表(从
task_result.source_files和 manifest 获取)。未配置CLUSTER_WORKER_RESULT_ROOT时沿用item.results_root_dir\runs\<run_key>;已配置时使用CLUSTER_WORKER_RESULT_ROOT\<pair_key>\runs\<run_key> - 调用
POST /api/cluster/upload-result/{item_id} - 以 multipart 上传 managed run 目录内容(不是外层 run 目录本身)
- 主服务器接收后写入该 item 的标准目录
results_root_dir\runs\<run_key> - 触发 catalog 登记,修复
execution_manifest.json和 current 指针 - 主服务器把 item/execution 标记为完成并尝试 finalize cluster run;远端 worker 不再在上传后自行标记完成
主服务器新增 API:
POST /api/cluster/upload-result/{item_id}
Header: X-Cluster-Token: <CLUSTER_SHARED_TOKEN>
Body: multipart/form-data
- result_zip: managed run directory zip
- run_id: 集群 run ID
- run_key: 当前执行 run key
Response: { "registered": true, "processed": 1, "failed": 0, "catalog_path": "..." }
上传后处理:
- 安全解压上传 zip,替换该 item 的
results_root_dir\runs\<run_key> - 调用
result_catalog_service增量登记 - 校验 catalog 只登记 1 条且生成
execution_manifest.json和 current 指针 - 更新
DinsarProductionExecutionORM/DinsarProductionRunItemORM指向主服务器最终结果路径 - 标记 item 完成,并在所有 item 终态后 finalize run
- 返回登记结果给 worker
错误处理与重试
- 下载失败:worker 端抛异常 →
job_queue_service.mark_failed→ 按max_attempts自动重试 - 上传失败:同上,LandSAR 已完成的中间结果在 worker 本地保留(下次重试跳过 LandSAR,直接上传)
- 主服务器端打包失败:返回 500 + 错误详情,worker 捕获后走重试
- 超时控制:复用
LANDSAR_DINSAR_TIMEOUT_SECONDS,传输阶段额外设CLUSTER_TRANSFER_TIMEOUT_SECONDS(默认 3600)
Windows 集群运维设计
Worker 开机自启
每个 worker 节点配置一条 Windows Task Scheduler 任务:
# 创建计划任务(以管理员身份运行)
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File D:\Code\Insar_management_system_v2\scripts\start_landsar_cluster_worker.ps1 -Background"
$trigger = New-ScheduledTaskTrigger -AtStartup
$settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName "InSAR_LandSAR_Cluster_Worker" `
-Action $action -Trigger $trigger -Settings $settings `
-RunLevel Highest -Description "LandSAR 集群 Worker 常驻进程"
关键设置:
- 触发器:系统启动时
- 失败重试:3 次,间隔 1 分钟
- 运行级别:最高权限
- 不因电池模式停止(针对笔记本)
Worker 监控
Worker 进程已内建心跳机制(_touch_worker),每隔 JOB_WORKER_HEARTBEAT_INTERVAL 秒向 system_worker_heartbeats 表写入心跳。主服务器健康检查页面可以展示:
- 各 worker 的 hostname / PID / worker_id
- 最后心跳时间
- 当前正在处理的 job 数量
- 历史完成/失败统计
代码同步
远端 worker 建议通过 Git 同步代码:
# 在远端 192.168.1.6 上
cd D:\Code\Insar_management_system_v2
git fetch origin
git checkout <branch>
.env 文件不在 Git 中,需手动维护。远端 .env 的 DATABASE_URL 指向主服务器,LandSAR 路径指向远端本地。
配置新增项
主服务器 .env 新增:
# 集群传输配置
CLUSTER_SHARED_TOKEN=<same-long-random-token-on-main-and-workers>
CLUSTER_TRANSFER_TIMEOUT_SECONDS=3600
Worker 端 .env 新增(或保持现有模板字段):
# 集群 worker 主服务器地址
CLUSTER_MAIN_SERVER_URL=http://192.168.1.62
CLUSTER_SHARED_TOKEN=<same-long-random-token-on-main-and-workers>
CLUSTER_TRANSFER_TIMEOUT_SECONDS=3600
CLUSTER_WORKER_TASK_ROOT=D:\Cluster_Work\Task_Pool
CLUSTER_WORKER_RESULT_ROOT=D:\Cluster_Work\Results
LANDSAR_RUNTIME_ID=landsar_runtime_v1
CLUSTER_MAIN_SERVER_URL 用于 worker 构造下载/上传 API 的完整 URL。未配置时默认使用 DATABASE_URL 中的 host 推断。
CLUSTER_SHARED_TOKEN 是集群传输接口的专用共享密钥,主服务器和所有远端 worker 必须一致且非空;未配置时 /api/cluster/... 接口返回 503。
CLUSTER_WORKER_TASK_ROOT、CLUSTER_WORKER_RESULT_ROOT 是远端 worker 本机缓存根。未配置时保持旧行为,直接使用数据库中的 source_task_dir 和 results_root_dir。
实现路线图
| 阶段 | 内容 | 依赖 |
|---|---|---|
| 1 | 主服务器 GET /api/cluster/input-package/{item_id} |
Task_Pool source_task_dir |
| 2 | Worker handler 增加 pre-flight download + extract | 阶段 1 |
| 3 | 主服务器 POST /api/cluster/upload-result/{item_id} |
result_catalog_service |
| 4 | Worker handler 增加 post-flight upload | 阶段 3 |
| 5 | 端到端测试(主服务器 + 远端 .6) | 阶段 1-4 |
| 6 | Windows Task Scheduler 开机自启配置 | 阶段 5 |
当前状态(2026-06-26)
- 集群调度骨架(提交 → 拆 item → DB 队列 → worker 领取 → LandSAR 执行)
- Worker 入口脚本 + 启动/停止脚本
- Worker 心跳上报
- 本机集群模式验证通过(
SameSite=laxCookie 修复后) - 数据搬运 pre-flight(HTTP 下载 Task_Pool zip + 安全解压)
- 结果上传 post-flight(HTTP 上传结果 zip + catalog 登记)
- 集群传输接口
CLUSTER_SHARED_TOKEN鉴权 - 主服务器上传登记后负责 item/execution 完成和 run finalize
- 支持 worker 本机输入/结果根,避免强依赖主服务器盘符
- 远端 .6 端到端测试
- Worker 开机自启
相关文档
- LANDSAR_CLUSTER_WORKER_DEPLOYMENT_20260624.md — 集群调度骨架与部署记录
- LANDSAR_DEM_PREPARATION_CONTRACT_20260618.md — LandSAR DEM 准备约定
- THREE_SENSOR_LOCAL_PRODUCTION_CONTRACT_20260616.md — 三数据本机生产约定
- DINSAR_TASK_POOL_THREE_ENGINE_REFACTOR_20260614.md — Task_Pool 与三引擎 refactor
2026-06-26 Remote 1.6 E2E Findings
- Remote worker
landsar-worker-192-168-1-6can reach PostgreSQL on192.168.1.62:5432, authenticate to/api/cluster/..., heartbeat intosystem_worker_heartbeats, and claimLANDSAR_CLUSTER_ITEMjobs. - The first remote E2E attempt failed before LandSAR execution with
HTTP 504while downloading/api/cluster/input-package/{item_id}through nginx. - Root cause: the old input endpoint synchronously built a full Task directory zip inside the FastAPI request. The tested Task directory was 15.47 GB because it contained duplicate
Input_Data,master, andslavecopies. nginx's default 60s upstream timeout returned 504 while the backend was still packaging, and the synchronous packaging could also block normal UI API responses. - Current behavior: the input endpoint streams a ZIP directly to the worker and uses
ZIP_STOREDto avoid wasting CPU on already-compressed TIFF data. - Current package selection: if
Input_Datacontains a valid LandSAR SLC xml/tif pair, onlyInput_Data,orbit, and.dinsar_pair.jsonare shipped. Otherwise, only direct raw files frommasterandslave, plusorbitand.dinsar_pair.json, are shipped. For item135, this reduced the transfer set from 15.47 GB to 5.18 GB. - nginx now has a dedicated
/api/cluster/location with large body support, disabled proxy buffering for cluster transfers, and 7200s send/read timeouts.scripts/start_app.ps1also rewrites this location's backend port whenPORTchanges. - Upload registration now resolves
DinsarProductionRunORMby businessrun_id; usingdb.get()with the string run id was invalid because the table primary key is integer.