功能: 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
+11 -1
View File
@@ -35,7 +35,9 @@ type BackupTaskUpsertInput struct {
DBPath string `json:"dbPath" binding:"max=500"`
StorageTargetID uint `json:"storageTargetId"` // deprecated: 向后兼容
StorageTargetIDs []uint `json:"storageTargetIds"` // 新增:多存储目标
NodeID uint `json:"nodeId"` // 执行节点(0 = 本机 Master
NodeID uint `json:"nodeId"` // 执行节点(0 = 本机 Master 或节点池
// NodePoolTag 节点池标签。NodeID=0 且本字段非空时,调度器动态从 Labels 命中的在线节点中选负载最低者。
NodePoolTag string `json:"nodePoolTag" binding:"max=64"`
Tags string `json:"tags" binding:"max=500"` // 逗号分隔标签
RetentionDays int `json:"retentionDays"`
Compression string `json:"compression" binding:"omitempty,oneof=gzip none"`
@@ -74,6 +76,7 @@ type BackupTaskSummary struct {
StorageTargetNames []string `json:"storageTargetNames"`
NodeID uint `json:"nodeId"`
NodeName string `json:"nodeName,omitempty"`
NodePoolTag string `json:"nodePoolTag,omitempty"`
Tags string `json:"tags"`
RetentionDays int `json:"retentionDays"`
Compression string `json:"compression"`
@@ -494,6 +497,11 @@ func (s *BackupTaskService) validateInput(ctx context.Context, existing *model.B
return apperror.BadRequest("BACKUP_TASK_INVALID", "所选执行节点不存在", nil)
}
}
// 节点池与固定节点互斥:固定节点已确定执行位置,不再动态调度
if input.NodeID > 0 && strings.TrimSpace(input.NodePoolTag) != "" {
return apperror.BadRequest("BACKUP_TASK_INVALID",
"固定执行节点与节点池标签只能选其一", nil)
}
if input.RetentionDays < 0 {
return apperror.BadRequest("BACKUP_TASK_INVALID", "保留天数不能小于 0", nil)
}
@@ -648,6 +656,7 @@ func (s *BackupTaskService) buildTask(existing *model.BackupTask, input BackupTa
StorageTargetID: primaryTargetID,
StorageTargets: storageTargets,
NodeID: input.NodeID,
NodePoolTag: strings.TrimSpace(input.NodePoolTag),
Tags: strings.TrimSpace(input.Tags),
RetentionDays: input.RetentionDays,
Compression: compression,
@@ -738,6 +747,7 @@ func toBackupTaskSummary(item *model.BackupTask) BackupTaskSummary {
StorageTargetNames: targetNames,
NodeID: item.NodeID,
NodeName: item.Node.Name,
NodePoolTag: item.NodePoolTag,
Tags: item.Tags,
RetentionDays: item.RetentionDays,
Compression: item.Compression,