功能: v2.2 节点池调度 + Grafana Dashboard + 版本漂移 UI (#49)

节点池动态调度(企业集群核心需求):
- model.Node 新增 Labels CSV;Node.HasLabel / LabelSet 辅助方法
- model.BackupTask 新增 NodePoolTag;与 NodeID 互斥(校验层拒绝同时设置)
- BackupExecutionService.selectPoolNode:匹配标签的在线节点中选"运行中任务最少"
  并列按 ID 升序稳定;空池返回 NODE_POOL_EMPTY 让用户立即感知
- 选中节点仅写 BackupRecord,不回写 task.NodeID —— 每次执行重选实现真轮转均衡

Grafana Dashboard(v2.1 指标的可视化闭环):
- deploy/grafana/backupx-dashboard.json:11 个面板覆盖概览/时序/容量/集群
- deploy/grafana/README.md:Prometheus 抓取配置 + 告警建议
- release workflow 打包 grafana/ + nginx.conf 到 tar.gz

前端:
- 节点列表:Agent 版本 vs Master 不一致时橙红 Tag + Tooltip 提示升级
- 节点列表新增"标签/节点池"列,支持 CSV 编辑 + 并发/带宽一起改
- 任务表单新增 NodePoolTag 输入框,与节点选择器互斥禁用

测试:
- model/node_label_test.go:HasLabel / LabelSet / nil 安全
- service/node_pool_scheduler_test.go:负载最低优先 / 空池错误 / nil repo 降级
- go test ./... + npm run build 全绿
This commit is contained in:
Wu Qing
2026-04-21 14:05:48 +08:00
committed by GitHub
parent 1a699da8d6
commit bc8742977e
15 changed files with 616 additions and 15 deletions

View File

@@ -59,6 +59,7 @@ function createEmptyDraft(storageTargets?: StorageTargetSummary[]): BackupTaskPa
storageTargetId: defaultIds[0] ?? 0,
storageTargetIds: defaultIds,
nodeId: 0,
nodePoolTag: '',
tags: '',
retentionDays: 30,
compression: 'gzip',
@@ -127,6 +128,7 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
storageTargetId: editTargetIds[0] ?? 0,
storageTargetIds: editTargetIds,
nodeId: (initialValue as any).nodeId ?? 0,
nodePoolTag: (initialValue as any).nodePoolTag ?? '',
tags: initialValue.tags ?? '',
retentionDays: initialValue.retentionDays,
compression: initialValue.compression,
@@ -297,12 +299,28 @@ export function BackupTaskFormDrawer({ visible, loading, initialValue, storageTa
<Select
value={draft.nodeId ?? 0}
options={nodeOptions}
onChange={(value) => updateDraft({ nodeId: Number(value ?? 0) })}
onChange={(value) => {
const nodeId = Number(value ?? 0)
// 固定节点与节点池互斥:切到固定节点时清空 NodePoolTag
updateDraft(nodeId > 0 ? { nodeId, nodePoolTag: '' } : { nodeId })
}}
/>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
/"节点管理" Agent
</Typography.Paragraph>
</div>
<div>
<Typography.Text></Typography.Text>
<Input
placeholder="填写标签后从节点池动态调度(与固定节点互斥)"
value={draft.nodePoolTag ?? ''}
disabled={(draft.nodeId ?? 0) > 0}
onChange={(value) => updateDraft({ nodePoolTag: value })}
/>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
"本机 / 未指定" Labels tag 线
</Typography.Paragraph>
</div>
<div>
<Typography.Text>Cron </Typography.Text>
<CronInput value={draft.cronExpr} onChange={(value) => updateDraft({ cronExpr: value })} />