1209 lines
31 KiB
Markdown
1209 lines
31 KiB
Markdown
# 任务队列后端 - message-dispatcher 优先分发技术方案
|
||
|
||
## 1. 项目概述
|
||
|
||
### 1.1 背景
|
||
当前任务队列后端系统通过 runninghub 等外部平台处理 comfyui 任务。为降低成本、提高响应速度,需要实现内部算力优先使用机制,将 runninghub 类型任务优先路由至内部 message-dispatcher 系统。
|
||
|
||
### 1.2 目标
|
||
- 实现 comfyui 类型任务优先分发至内部 message-dispatcher
|
||
- 确保当内部算力不可用时自动降级至外部平台
|
||
- 保持接口兼容性,不影响现有功能
|
||
- 提供完善的健康检查和监控机制
|
||
|
||
## 2. 系统架构分析
|
||
|
||
### 2.1 任务队列后端架构
|
||
```
|
||
任务队列后端 (任务队列后端/)
|
||
├── index.js # HTTP 回调服务 (端口 8066)
|
||
├── outside/
|
||
│ ├── generat.js # 任务分发逻辑
|
||
│ └── outPlatforms/
|
||
│ ├── outside.js # 平台管理
|
||
│ └── runninghub.js # RunningHub 平台实现
|
||
├── worker_threads/
|
||
│ └── wait/
|
||
│ ├── waiting.js # 任务队列轮询
|
||
│ └── generatTask.js # 任务处理 Worker
|
||
└── redis/ # Redis 数据存储
|
||
```
|
||
|
||
### 2.2 message-dispatcher 架构
|
||
```
|
||
message-dispatcher/
|
||
├── src/
|
||
│ ├── index.js # 主服务 (端口 4000)
|
||
│ ├── api/
|
||
│ │ └── index.js # REST API 路由
|
||
│ ├── bridge-manager/ # 桥接器管理
|
||
│ └── websocket-server/ # WebSocket 服务
|
||
```
|
||
|
||
### 2.3 关键接口分析
|
||
|
||
**任务队列后端 - 任务流程:**
|
||
1. 任务进入 Redis 等待队列
|
||
2. `waiting.js` 轮询获取任务
|
||
3. 调用 `generatTask.js` → `externalPostRequest()`
|
||
4. 通过 `outside[runninghub]` 发送至外部平台
|
||
|
||
**message-dispatcher - 任务接口:**
|
||
- `POST /api/task` - 提交任务
|
||
- `GET /api/health` - 健康检查
|
||
- `GET /api/overview` - 系统概览
|
||
|
||
## 3. 方案设计
|
||
|
||
### 3.1 总体架构
|
||
|
||
```
|
||
任务队列后端 ──┐
|
||
│ 优先
|
||
▼
|
||
message-dispatcher (内部算力)
|
||
│
|
||
└─ 失败/不可用时
|
||
│
|
||
▼
|
||
runninghub (外部平台)
|
||
```
|
||
|
||
### 3.2 核心模块设计
|
||
|
||
#### 3.2.1 新增模块:内部算力客户端 (InternalDispatcher)
|
||
|
||
**文件位置:** `任务队列后端/outside/outPlatforms/messageDispatcher.js`
|
||
|
||
**功能:**
|
||
- 封装 message-dispatcher API 调用
|
||
- 提供与 runninghub 一致的接口方法
|
||
- 仅修改请求地址和请求头,其他配置与 runninghub 完全一致
|
||
|
||
**接口方法:**
|
||
```javascript
|
||
getGenerateUrl() // 获取 API 地址
|
||
getGenerateHeader(apikey) // 获取请求头(包含 JWT Token)
|
||
getGenerateBody(task) // 构造请求体(与 runninghub 完全一致)
|
||
getSuccessTasks(response) // 处理成功响应
|
||
getTaskResult(response) // 处理结果回调
|
||
getQueryUrl() // 获取回调地址
|
||
```
|
||
|
||
#### 3.2.2 健康检查模块
|
||
|
||
**文件位置:** `任务队列后端/utils/healthCheck.js`
|
||
|
||
**功能:**
|
||
- 定期检查 message-dispatcher 健康状态
|
||
- 维护可用状态缓存
|
||
- 提供可用性查询接口
|
||
- 监控内部算力可用数量
|
||
|
||
**检查项:**
|
||
- HTTP 接口连通性 (`/api/health`)
|
||
- 可用桥接器数量
|
||
- 在线实例数量(内部算力可用数)
|
||
|
||
#### 3.2.3 路由决策模块
|
||
|
||
**修改位置:** `任务队列后端/outside/generat.js` 的 `externalPostRequest()`
|
||
|
||
**决策逻辑:**
|
||
```
|
||
获取内部算力可用数 (internalCapacity)
|
||
获取 runninghub 可容纳上限 (externalCapacity)
|
||
总可分发任务上限 = internalCapacity + externalCapacity
|
||
|
||
if (待分发任务数 ≤ internalCapacity) {
|
||
全部任务使用内部算力
|
||
} else {
|
||
前 internalCapacity 个任务使用内部算力
|
||
超出部分使用 runninghub(不超过 externalCapacity)
|
||
}
|
||
```
|
||
|
||
### 3.3 接口对齐方案
|
||
|
||
#### 3.3.1 配置对齐要求
|
||
|
||
**重要原则:**
|
||
- message-dispatcher 配置与 runninghub 保持完全一致
|
||
- 仅修改以下两个参数:
|
||
1. **请求地址**:从 runninghub URL 改为 message-dispatcher URL
|
||
2. **请求头**:将 apiKey 字段值替换为 JWT Token
|
||
- 其他所有配置项、参数、字段名称均不做任何改动
|
||
|
||
#### 3.3.2 请求体标准化
|
||
|
||
**请求体必须与 runninghub 完全一致,包括:**
|
||
|
||
| 字段名称 | 类型 | 说明 | 是否修改 |
|
||
|---------|------|------|---------|
|
||
| `workflow_id` | String | 工作流 ID | 否 |
|
||
| `node_info_list` | Array | 节点信息列表 | 否 |
|
||
| `apiKey` | String | API Key | 是(值改为 JWT Token) |
|
||
| `webhookUrl` | String | 回调地址 | 否 |
|
||
| 其他所有字段 | - | 保持原样 | 否 |
|
||
|
||
**示例对比:**
|
||
|
||
```javascript
|
||
// runninghub 请求体(原样)
|
||
{
|
||
"workflow_id": "123",
|
||
"node_info_list": [...],
|
||
"apiKey": "runninghub-api-key-xxx",
|
||
"webhookUrl": "http://callback-url"
|
||
}
|
||
|
||
// message-dispatcher 请求体(仅修改 apiKey 值)
|
||
{
|
||
"workflow_id": "123",
|
||
"node_info_list": [...],
|
||
"apiKey": "eyJhbGciOiJIUzI1NiIs...", // JWT Token
|
||
"webhookUrl": "http://callback-url"
|
||
}
|
||
```
|
||
|
||
#### 3.3.3 响应格式转换
|
||
|
||
**message-dispatcher → 任务队列后端 格式映射:**
|
||
|
||
```javascript
|
||
// message-dispatcher 响应
|
||
{
|
||
success: true,
|
||
data: {
|
||
requestId: "uuid",
|
||
taskId: "..."
|
||
}
|
||
}
|
||
|
||
// 转换为 runninghub 兼容格式
|
||
{
|
||
msg: "success",
|
||
code: 0,
|
||
data: {
|
||
taskId: "requestId"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.4 降级机制
|
||
|
||
#### 3.4.1 降级触发条件
|
||
1. message-dispatcher 健康检查失败
|
||
2. 任务提交请求超时 (30秒)
|
||
3. 任务提交返回错误
|
||
4. 无可用桥接器/实例(内部算力可用数为0)
|
||
|
||
#### 3.4.2 降级流程
|
||
1. 记录降级日志
|
||
2. 标记本次任务使用外部平台
|
||
3. 更新健康检查状态
|
||
4. 继续使用 runninghub 完成任务
|
||
|
||
### 3.5 JWT 认证机制
|
||
|
||
#### 3.5.1 JWT 生成与发送
|
||
|
||
**JWT 获取流程:**
|
||
|
||
1. **初始登录**:
|
||
- 调用 message-dispatcher 登录接口
|
||
- 接口:`POST /api/auth/login`
|
||
- 请求体:`{ username: "admin", password: "2233..2233" }`
|
||
- 获取 access_token
|
||
|
||
2. **JWT 存储**:
|
||
- 存储在内存变量中
|
||
- 同时存储过期时间(expires_at)
|
||
|
||
3. **请求头设置**:
|
||
- 将 JWT Token 作为 apiKey 字段的值
|
||
- 请求头格式与 runninghub 保持一致
|
||
|
||
#### 3.5.2 JWT 定期更新机制
|
||
|
||
**更新策略:**
|
||
- **更新间隔**:20小时(默认 JWT 有效期 24小时,提前4小时更新)
|
||
- **更新流程**:
|
||
1. 检查当前 Token 是否在 4 小时内过期
|
||
2. 如果是,重新调用登录接口获取新 Token
|
||
3. 更新内存中的 Token 和过期时间
|
||
4. 记录 Token 更新日志
|
||
|
||
**实现代码:**
|
||
```javascript
|
||
class JWTManager {
|
||
constructor() {
|
||
this.token = null;
|
||
this.expiresAt = null;
|
||
this.refreshInterval = 20 * 60 * 60 * 1000; // 20小时
|
||
}
|
||
|
||
async init() {
|
||
await this.refreshToken();
|
||
setInterval(() => this.refreshToken(), this.refreshInterval);
|
||
}
|
||
|
||
async refreshToken() {
|
||
try {
|
||
const response = await fetch(`${MESSAGE_DISPATCHER_URL}/api/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
username: process.env.MD_USERNAME || 'admin',
|
||
password: process.env.MD_PASSWORD || '2233..2233'
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success && data.data?.access_token) {
|
||
this.token = data.data.access_token;
|
||
this.expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 假设24小时有效期
|
||
console.log('[JWTManager] Token 已更新');
|
||
}
|
||
} catch (error) {
|
||
console.error('[JWTManager] Token 更新失败:', error);
|
||
}
|
||
}
|
||
|
||
getToken() {
|
||
return this.token;
|
||
}
|
||
|
||
isTokenValid() {
|
||
return this.token && Date.now() < this.expiresAt;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.6 任务分流逻辑
|
||
|
||
#### 3.6.1 核心计算公式
|
||
|
||
```
|
||
内部算力可用数 = message-dispatcher 在线实例数
|
||
runninghub 可容纳上限 = 配置的外部平台并发数
|
||
总可分发任务上限 = 内部算力可用数 + runninghub 可容纳上限
|
||
```
|
||
|
||
#### 3.6.2 具体分发策略
|
||
|
||
**场景 1:待分发任务数 ≤ 内部算力可用数**
|
||
```
|
||
示例:
|
||
内部算力可用数 = 30
|
||
待分发任务数 = 25
|
||
|
||
结果:
|
||
全部 25 个任务使用内部算力处理
|
||
```
|
||
|
||
**场景 2:待分发任务数 > 内部算力可用数**
|
||
```
|
||
示例:
|
||
内部算力可用数 = 30
|
||
runninghub 可容纳上限 = 10
|
||
待分发任务数 = 45
|
||
|
||
结果:
|
||
前 30 个任务 → 内部算力
|
||
后 10 个任务 → runninghub(达到上限)
|
||
剩余 5 个任务 → 等待队列(等待资源释放)
|
||
```
|
||
|
||
#### 3.6.3 分发决策实现
|
||
|
||
**修改位置:** `worker_threads/wait/waiting.js` 和 `worker_threads/wait/generatTask.js`
|
||
|
||
**实现逻辑:**
|
||
```javascript
|
||
async function distributeTasks(tasks) {
|
||
const internalCapacity = await getInternalCapacity();
|
||
const externalCapacity = await getExternalCapacity();
|
||
|
||
const internalTasks = tasks.slice(0, internalCapacity);
|
||
const externalTasks = tasks.slice(internalCapacity, internalCapacity + externalCapacity);
|
||
|
||
// 分发到内部算力
|
||
for (const task of internalTasks) {
|
||
await sendToMessageDispatcher(task);
|
||
}
|
||
|
||
// 分发到外部平台
|
||
for (const task of externalTasks) {
|
||
await sendToRunningHub(task);
|
||
}
|
||
|
||
// 剩余任务返回队列
|
||
const remainingTasks = tasks.slice(internalCapacity + externalCapacity);
|
||
return remainingTasks;
|
||
}
|
||
```
|
||
|
||
### 3.7 配置设计
|
||
|
||
**新增环境变量 (.env):**
|
||
```env
|
||
# Message Dispatcher 配置
|
||
MESSAGE_DISPATCHER_URL=http://localhost:4000
|
||
MESSAGE_DISPATCHER_ENABLED=true
|
||
MESSAGE_DISPATCHER_TIMEOUT=30000
|
||
MESSAGE_DISPATCHER_HEALTH_CHECK_INTERVAL=10000
|
||
|
||
# Message Dispatcher 认证
|
||
MD_USERNAME=admin
|
||
MD_PASSWORD=2233..2233
|
||
|
||
# 容量配置
|
||
INTERNAL_CAPACITY_MAX=30
|
||
EXTERNAL_CAPACITY_MAX=10
|
||
```
|
||
|
||
**新增配置文件 (config/messageDispatcher.json):**
|
||
```json
|
||
{
|
||
"enabled": true,
|
||
"priority": true,
|
||
"healthCheck": {
|
||
"interval": 10000,
|
||
"timeout": 5000,
|
||
"failureThreshold": 3
|
||
},
|
||
"task": {
|
||
"timeout": 30000,
|
||
"retryCount": 1
|
||
},
|
||
"capacity": {
|
||
"internal": 30,
|
||
"external": 10
|
||
},
|
||
"jwt": {
|
||
"refreshInterval": 72000000
|
||
}
|
||
}
|
||
```
|
||
|
||
## 4. 实现步骤
|
||
|
||
### 步骤 1: 创建 message-dispatcher 平台适配器
|
||
- 创建 `outside/outPlatforms/messageDispatcher.js`
|
||
- 实现与 runninghub.js 相同的接口
|
||
- 请求体与 runninghub 完全一致,仅修改 apiKey 值为 JWT Token
|
||
|
||
### 步骤 2: 实现 JWT 认证模块
|
||
- 创建 `utils/jwtManager.js`
|
||
- 实现 Token 获取、存储、定期更新逻辑
|
||
- 集成到启动流程
|
||
|
||
### 步骤 3: 实现健康检查模块
|
||
- 创建 `utils/healthCheck.js`
|
||
- 实现定期健康检查逻辑
|
||
- 监控内部算力可用数量
|
||
|
||
### 步骤 4: 修改任务分发逻辑
|
||
- 修改 `outside/generat.js` - 单任务分发决策
|
||
- 修改 `worker_threads/wait/waiting.js` - 批量任务分流
|
||
- 根据内部/外部容量进行任务分配
|
||
|
||
### 步骤 5: 更新平台管理
|
||
- 修改 `outside/outPlatforms/outside.js`
|
||
- 注册 messageDispatcher 平台
|
||
|
||
### 步骤 6: 添加日志和监控
|
||
- 新增分发决策日志
|
||
- 新增容量使用统计日志
|
||
- 新增降级统计日志
|
||
|
||
### 步骤 7: 测试验证
|
||
- 单元测试
|
||
- 集成测试
|
||
- 降级场景测试
|
||
- 容量边界测试
|
||
|
||
## 5. 验收标准
|
||
|
||
### 5.1 功能验收
|
||
- [ ] 任务分流逻辑正确:≤内部容量全部走内部,超出部分走外部
|
||
- [ ] message-dispatcher 不可用时自动降级至 runninghub
|
||
- [ ] 请求体与 runninghub 完全一致(仅 apiKey 值不同)
|
||
- [ ] JWT Token 自动更新机制正常工作
|
||
- [ ] 任务成功执行并返回正确结果
|
||
- [ ] 回调接口正常工作
|
||
- [ ] 现有功能不受影响
|
||
|
||
### 5.2 接口兼容性验收
|
||
- [ ] messageDispatcher.js 接口方法签名与 runninghub.js 完全一致
|
||
- [ ] 请求体字段名称、类型、格式与 runninghub 完全一致
|
||
- [ ] 响应格式与 runninghub 兼容
|
||
- [ ] 不修改 runninghub.js 的任何代码
|
||
|
||
### 5.3 性能验收
|
||
- [ ] 健康检查延迟 < 1s
|
||
- [ ] 任务分发决策时间 < 100ms
|
||
- [ ] 降级切换时间 < 1s
|
||
- [ ] 系统整体吞吐量不降低
|
||
|
||
### 5.4 可靠性验收
|
||
- [ ] 连续 24 小时稳定运行
|
||
- [ ] 降级机制在故障场景下正确触发
|
||
- [ ] JWT Token 自动更新不中断服务
|
||
- [ ] 日志完整可追溯
|
||
- [ ] 错误处理完善
|
||
|
||
## 6. 风险与应对
|
||
|
||
| 风险 | 影响 | 概率 | 应对措施 |
|
||
|-----|------|------|---------|
|
||
| message-dispatcher 接口变更 | 高 | 中 | 版本兼容检查,完善监控 |
|
||
| JWT Token 过期 | 高 | 低 | 提前更新,多重校验 |
|
||
| 任务格式不匹配 | 高 | 低 | 充分测试,格式验证 |
|
||
| 降级失败 | 高 | 低 | 多级降级,超时保护 |
|
||
| 性能下降 | 中 | 低 | 性能测试,优化热点路径 |
|
||
|
||
## 7. WebSocket 双向通信机制
|
||
|
||
### 7.1 架构设计
|
||
|
||
建立任务队列后端与 message-dispatcher 之间的持久化 WebSocket 连接,实现实时双向通信。
|
||
|
||
```
|
||
任务队列后端 (WebSocket 客户端)
|
||
│
|
||
│ 建立连接
|
||
▼
|
||
message-dispatcher (WebSocket 服务端)
|
||
```
|
||
|
||
### 7.2 连接管理
|
||
|
||
#### 7.2.1 连接建立流程
|
||
1. **任务队列后端启动时**:
|
||
- 初始化 WebSocket 客户端
|
||
- 连接至 message-dispatcher 的 WebSocket 端点
|
||
- 发送 `CONNECT` 消息进行身份认证
|
||
- 接收 `CONNECT_ACK` 确认连接建立
|
||
|
||
2. **消息格式定义**:
|
||
```javascript
|
||
// 连接消息
|
||
{
|
||
type: 'CONNECT',
|
||
data: {
|
||
clientId: 'task-queue-backend',
|
||
timestamp: '2024-01-01T00:00:00.000Z'
|
||
}
|
||
}
|
||
|
||
// 连接确认
|
||
{
|
||
type: 'CONNECT_ACK',
|
||
data: {
|
||
serverId: 'message-dispatcher',
|
||
timestamp: '2024-01-01T00:00:00.000Z'
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 7.2.2 心跳保活机制
|
||
- **心跳间隔**:30秒
|
||
- **超时时间**:10秒
|
||
- **重连策略**:指数退避(1s, 2s, 4s, 8s, 16s, 最大32s)
|
||
|
||
```javascript
|
||
// 心跳消息
|
||
{
|
||
type: 'HEARTBEAT',
|
||
data: {
|
||
timestamp: '2024-01-01T00:00:00.000Z'
|
||
}
|
||
}
|
||
|
||
// 心跳响应
|
||
{
|
||
type: 'HEARTBEAT_ACK',
|
||
data: {
|
||
timestamp: '2024-01-01T00:00:00.000Z'
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.3 算力状态上报
|
||
|
||
#### 7.3.1 上报内容
|
||
message-dispatcher 定期向任务队列后端上报以下信息:
|
||
|
||
```javascript
|
||
{
|
||
type: 'CAPACITY_UPDATE',
|
||
data: {
|
||
timestamp: '2024-01-01T00:00:00.000Z',
|
||
bridges: [
|
||
{
|
||
bridgeId: 'bridge-1',
|
||
connectedAt: '2024-01-01T00:00:00.000Z',
|
||
instances: [
|
||
{
|
||
instanceId: 'instance-1',
|
||
status: 'online', // online | busy | offline
|
||
currentTask: null | { taskId: 'xxx', startTime: '...' }
|
||
}
|
||
]
|
||
}
|
||
],
|
||
summary: {
|
||
totalBridges: 2,
|
||
totalInstances: 8,
|
||
onlineInstances: 6,
|
||
busyInstances: 2,
|
||
availableCapacity: 4 // 可同时处理的任务数
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 7.3.2 上报触发条件
|
||
1. **定期上报**:每 10 秒上报一次
|
||
2. **事件触发**:
|
||
- 桥接器连接/断开
|
||
- 实例状态变化(online → busy, busy → online, offline)
|
||
- 任务开始/完成
|
||
|
||
### 7.4 任务数量下发
|
||
|
||
任务队列后端向 message-dispatcher 下发任务分配信息:
|
||
|
||
```javascript
|
||
{
|
||
type: 'TASK_ALLOCATION',
|
||
data: {
|
||
timestamp: '2024-01-01T00:00:00.000Z',
|
||
allocatedTasks: [
|
||
{
|
||
taskId: 'task-1',
|
||
targetInstance: 'instance-1'
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.5 实例状态变化监测
|
||
|
||
#### 7.5.1 变化事件类型
|
||
```javascript
|
||
// 实例上线
|
||
{
|
||
type: 'INSTANCE_ONLINE',
|
||
data: {
|
||
bridgeId: 'bridge-1',
|
||
instanceId: 'instance-1',
|
||
timestamp: '2024-01-01T00:00:00.000Z'
|
||
}
|
||
}
|
||
|
||
// 实例下线
|
||
{
|
||
type: 'INSTANCE_OFFLINE',
|
||
data: {
|
||
bridgeId: 'bridge-1',
|
||
instanceId: 'instance-1',
|
||
timestamp: '2024-01-01T00:00:00.000Z',
|
||
reason: 'disconnected'
|
||
}
|
||
}
|
||
|
||
// 实例繁忙
|
||
{
|
||
type: 'INSTANCE_BUSY',
|
||
data: {
|
||
bridgeId: 'bridge-1',
|
||
instanceId: 'instance-1',
|
||
taskId: 'task-1',
|
||
timestamp: '2024-01-01T00:00:00.000Z'
|
||
}
|
||
}
|
||
|
||
// 实例空闲
|
||
{
|
||
type: 'INSTANCE_IDLE',
|
||
data: {
|
||
bridgeId: 'bridge-1',
|
||
instanceId: 'instance-1',
|
||
completedTaskId: 'task-1',
|
||
timestamp: '2024-01-01T00:00:00.000Z'
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Redis 状态管理机制
|
||
|
||
### 8.1 Redis 数据结构设计
|
||
|
||
#### 8.1.1 连接状态存储
|
||
|
||
**Key 前缀:** `md:state:`
|
||
|
||
| Key | 类型 | 说明 | TTL |
|
||
|-----|------|------|-----|
|
||
| `md:state:connection` | Hash | 连接状态信息 | 永久 |
|
||
| `md:state:capacity` | Hash | 算力容量信息 | 60s |
|
||
| `md:state:instances` | Hash | 实例详细状态 | 60s |
|
||
| `md:state:last_heartbeat` | String | 最后心跳时间 | 60s |
|
||
|
||
**connection Hash 字段:**
|
||
```javascript
|
||
{
|
||
status: 'connected', // connected | disconnected
|
||
connectedAt: '2024-01-01T00:00:00.000Z',
|
||
disconnectedAt: null,
|
||
disconnectReason: null,
|
||
reconnectAttempts: 0
|
||
}
|
||
```
|
||
|
||
**capacity Hash 字段:**
|
||
```javascript
|
||
{
|
||
totalBridges: 2,
|
||
totalInstances: 8,
|
||
onlineInstances: 6,
|
||
busyInstances: 2,
|
||
availableCapacity: 4,
|
||
updatedAt: '2024-01-01T00:00:00.000Z'
|
||
}
|
||
```
|
||
|
||
**instances Hash 结构:**
|
||
Key: `md:state:instances:{instanceId}`
|
||
```javascript
|
||
{
|
||
instanceId: 'instance-1',
|
||
bridgeId: 'bridge-1',
|
||
status: 'online',
|
||
currentTaskId: null,
|
||
currentTaskStartTime: null,
|
||
lastUpdated: '2024-01-01T00:00:00.000Z'
|
||
}
|
||
```
|
||
|
||
### 8.2 健康检查机制
|
||
|
||
#### 8.2.1 检查维度
|
||
|
||
1. **WebSocket 连接检查**
|
||
- 检查连接状态是否为 `connected`
|
||
- 检查最后心跳时间是否在 60 秒内
|
||
|
||
2. **算力可用性检查**
|
||
- 检查在线实例数量 > 0
|
||
- 检查可用容量 > 0
|
||
|
||
3. **综合健康评分**
|
||
```
|
||
健康状态 = 连接状态 × 0.4 + 算力状态 × 0.6
|
||
```
|
||
|
||
#### 8.2.2 健康检查流程
|
||
|
||
```javascript
|
||
async function checkHealth() {
|
||
const state = await getConnectionState();
|
||
const capacity = await getCapacity();
|
||
|
||
const isConnected = state.status === 'connected';
|
||
const hasHeartbeat = Date.now() - state.lastHeartbeat < 60000;
|
||
const hasCapacity = capacity.availableCapacity > 0;
|
||
|
||
const isHealthy = isConnected && hasHeartbeat && hasCapacity;
|
||
|
||
if (!isHealthy) {
|
||
await triggerDegradation();
|
||
}
|
||
|
||
return isHealthy;
|
||
}
|
||
```
|
||
|
||
### 8.3 自动降级触发
|
||
|
||
#### 8.3.1 降级触发条件
|
||
|
||
| 条件 | 说明 | 优先级 |
|
||
|-----|------|--------|
|
||
| WebSocket 断开 | 连接完全断开 | 1 |
|
||
| 心跳超时 (>60s) | 连接可能异常 | 2 |
|
||
| 无可用实例 | 在线实例数为0 | 3 |
|
||
| 可用容量为0 | 所有实例都忙 | 4 |
|
||
|
||
#### 8.3.2 降级流程
|
||
|
||
1. **检测到异常**
|
||
2. **更新 Redis 状态**:设置 `status: 'degraded'`
|
||
3. **记录降级日志**:记录降级时间、原因、当前状态
|
||
4. **通知路由模块**:触发路由策略切换
|
||
5. **持续监控**:定期检查是否恢复
|
||
|
||
#### 8.3.3 自动恢复流程
|
||
|
||
1. **健康检查通过**
|
||
2. **等待稳定期**:连续 3 次健康检查通过(30秒)
|
||
3. **更新 Redis 状态**:设置 `status: 'healthy'`
|
||
4. **记录恢复日志**
|
||
5. **通知路由模块**:恢复优先使用内部算力
|
||
|
||
---
|
||
|
||
## 9. Message-Dispatcher 任务缓存与调度机制
|
||
|
||
### 9.1 缓存队列设计
|
||
|
||
#### 9.1.1 缓存队列数据结构
|
||
|
||
使用 Redis List 实现:
|
||
|
||
| Key | 类型 | 说明 |
|
||
|-----|------|------|
|
||
| `md:cache:pending` | List | 待执行任务缓存 |
|
||
| `md:cache:processing` | Hash | 执行中任务 |
|
||
| `md:cache:completed` | List | 已完成任务(最近1000条) |
|
||
|
||
**pending List 元素格式:**
|
||
```javascript
|
||
{
|
||
taskId: 'xxx',
|
||
payload: {...},
|
||
submittedAt: '2024-01-01T00:00:00.000Z',
|
||
retryCount: 0
|
||
}
|
||
```
|
||
|
||
**processing Hash 字段:**
|
||
```javascript
|
||
{
|
||
taskId: {
|
||
instanceId: 'instance-1',
|
||
bridgeId: 'bridge-1',
|
||
startTime: '2024-01-01T00:00:00.000Z',
|
||
payload: {...}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.2 任务调度器
|
||
|
||
#### 9.2.1 调度器架构
|
||
|
||
```
|
||
调度器主循环
|
||
│
|
||
├─ 获取可用实例
|
||
│
|
||
├─ 从缓存队列取出任务
|
||
│
|
||
├─ 分配任务至实例
|
||
│
|
||
└─ 移动至 processing
|
||
```
|
||
|
||
#### 9.2.2 调度算法
|
||
|
||
```javascript
|
||
async function scheduleTasks() {
|
||
const availableInstances = await getAvailableInstances();
|
||
const pendingTasks = await getPendingTasks(availableInstances.length);
|
||
|
||
for (let i = 0; i < pendingTasks.length; i++) {
|
||
const task = pendingTasks[i];
|
||
const instance = availableInstances[i];
|
||
|
||
await assignTask(task, instance);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 9.2.3 实例选择策略
|
||
|
||
1. **最少任务优先**:选择当前任务数最少的实例
|
||
2. **同桥接优先**:优先选择同一桥接器下的实例
|
||
3. **随机打散**:避免总是选择同一实例
|
||
|
||
```javascript
|
||
function selectOptimalInstance(availableInstances, task) {
|
||
// 按当前任务数排序
|
||
const sorted = [...availableInstances].sort((a, b) =>
|
||
a.currentTasks - b.currentTasks
|
||
);
|
||
|
||
// 如果有任务指定偏好实例
|
||
if (task.preferredInstanceId) {
|
||
const preferred = sorted.find(i => i.id === task.preferredInstanceId);
|
||
if (preferred) return preferred;
|
||
}
|
||
|
||
return sorted[0];
|
||
}
|
||
```
|
||
|
||
### 9.3 任务完成监听
|
||
|
||
#### 9.3.1 完成事件处理
|
||
|
||
```javascript
|
||
async function handleTaskComplete(taskId, result) {
|
||
// 1. 从 processing 移除
|
||
const processingInfo = await removeFromProcessing(taskId);
|
||
|
||
// 2. 标记实例为空闲
|
||
await markInstanceIdle(processingInfo.instanceId);
|
||
|
||
// 3. 添加到 completed
|
||
await addToCompleted(taskId, result);
|
||
|
||
// 4. 触发新一轮调度
|
||
await scheduleTasks();
|
||
|
||
// 5. 调用回调(如果有)
|
||
if (processingInfo.webhookUrl) {
|
||
await invokeWebhook(processingInfo.webhookUrl, result);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 9.3.2 失败重试机制
|
||
|
||
```javascript
|
||
async function handleTaskFailure(taskId, error) {
|
||
const task = await getTask(taskId);
|
||
const maxRetries = 3;
|
||
|
||
if (task.retryCount < maxRetries) {
|
||
task.retryCount++;
|
||
task.lastError = error.message;
|
||
await returnToPending(task);
|
||
} else {
|
||
await markAsFailed(taskId, error);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.4 缓存队列管理
|
||
|
||
#### 9.4.1 队列扩容/缩容
|
||
|
||
当实例数量变化时:
|
||
|
||
```javascript
|
||
async function adjustCacheQueue(instanceDelta) {
|
||
const currentPending = await getPendingCount();
|
||
const currentCapacity = await getAvailableCapacity();
|
||
|
||
if (instanceDelta > 0) {
|
||
// 实例增加,立即调度
|
||
await scheduleTasks();
|
||
} else if (instanceDelta < 0) {
|
||
// 实例减少,检查是否需要回退任务
|
||
const excess = currentPending - currentCapacity;
|
||
if (excess > 0) {
|
||
// 通知任务队列后端,这些任务需要降级
|
||
await notifyTaskQueueForDegradation(excess);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 9.4.2 超时清理
|
||
|
||
```javascript
|
||
async function cleanStaleTasks() {
|
||
const now = Date.now();
|
||
const staleThreshold = 30 * 60 * 1000; // 30分钟
|
||
|
||
const pendingTasks = await getAllPendingTasks();
|
||
|
||
for (const task of pendingTasks) {
|
||
if (now - task.submittedAt > staleThreshold) {
|
||
await removeFromPending(task.taskId);
|
||
await markAsExpired(task.taskId);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 任务队列逻辑健壮性测试方案
|
||
|
||
### 10.1 测试场景设计
|
||
|
||
#### 10.1.1 场景1:内部算力减少
|
||
|
||
**测试目的:** 验证当内部算力减少时,系统能否正确处理已下发任务
|
||
|
||
**测试步骤:**
|
||
1. 初始状态:4个在线实例,下发4个任务
|
||
2. 操作:突然断开2个实例
|
||
3. 预期结果:
|
||
- 未开始的任务重新回到缓存队列
|
||
- 正在运行的任务继续执行(如果实例还在)
|
||
- 系统记录日志,不崩溃
|
||
- 自动调度可用实例处理缓存任务
|
||
|
||
**测试用例代码:**
|
||
```javascript
|
||
async function testCapacityReduction() {
|
||
// 1. 准备环境
|
||
await setupTestEnvironment({ onlineInstances: 4 });
|
||
|
||
// 2. 下发任务
|
||
const taskIds = await submitTasks(4);
|
||
|
||
// 3. 确认任务开始执行
|
||
await waitForTasksRunning(taskIds);
|
||
|
||
// 4. 断开2个实例
|
||
await disconnectInstances(2);
|
||
|
||
// 5. 验证
|
||
const remainingTasks = await getRemainingTasks();
|
||
assert(remainingTasks.length <= 2, '不应有超过2个任务继续执行');
|
||
|
||
const cacheQueue = await getCacheQueue();
|
||
assert(cacheQueue.length >= 2, '应有至少2个任务回到缓存');
|
||
|
||
const systemHealth = await checkSystemHealth();
|
||
assert(systemHealth.status === 'degraded', '系统应处于降级状态');
|
||
assert(systemHealth.crashed === false, '系统不应崩溃');
|
||
}
|
||
```
|
||
|
||
#### 10.1.2 场景2:任务数 > 可用实例数
|
||
|
||
**测试目的:** 验证当上一次下发任务数大于当前可用任务数时的系统行为
|
||
|
||
**测试步骤:**
|
||
1. 初始状态:4个在线实例
|
||
2. 操作1:下发8个任务
|
||
3. 预期1:4个开始执行,4个进入缓存
|
||
4. 操作2:断开2个实例(仅剩2个)
|
||
5. 预期2:
|
||
- 缓存队列保持4个任务
|
||
- 正在执行的任务继续
|
||
- 完成后只调度2个新任务
|
||
- 不发生任务丢失
|
||
|
||
**验证点:**
|
||
- [ ] 任务总数始终为8(执行中 + 缓存)
|
||
- [ ] 没有任务重复执行
|
||
- [ ] 没有任务丢失
|
||
- [ ] 最终所有任务都能完成(可能部分降级)
|
||
|
||
#### 10.1.3 场景3:容量边界测试
|
||
|
||
**测试目的:** 验证任务分流逻辑在各种容量边界下的正确性
|
||
|
||
**测试用例:**
|
||
|
||
| 用例 | 内部容量 | 外部容量 | 待分发任务数 | 预期结果 |
|
||
|-----|---------|---------|------------|---------|
|
||
| 1 | 30 | 10 | 25 | 全部25个走内部 |
|
||
| 2 | 30 | 10 | 30 | 全部30个走内部 |
|
||
| 3 | 30 | 10 | 35 | 30个内部,5个外部 |
|
||
| 4 | 30 | 10 | 40 | 30个内部,10个外部 |
|
||
| 5 | 30 | 10 | 45 | 30个内部,10个外部,5个等待 |
|
||
| 6 | 0 | 10 | 5 | 全部5个走外部 |
|
||
| 7 | 30 | 0 | 35 | 30个内部,5个等待 |
|
||
|
||
### 10.2 潜在错误检查清单
|
||
|
||
#### 10.2.1 并发安全检查
|
||
|
||
| 检查项 | 风险 | 验证方法 |
|
||
|-------|------|---------|
|
||
| Redis 操作原子性 | 高 | 使用 MULTI/EXEC 事务 |
|
||
| 任务重复下发 | 高 | 任务 ID 幂等性检查 |
|
||
| 状态竞态条件 | 中 | 使用 Redis 锁保护关键操作 |
|
||
| 内存泄漏 | 中 | 长时间运行监控内存使用 |
|
||
|
||
#### 10.2.2 边界条件检查
|
||
|
||
| 边界条件 | 测试方法 | 预期结果 |
|
||
|---------|---------|---------|
|
||
| 0 个实例 | 断开所有实例 | 全部任务降级,系统不崩溃 |
|
||
| 瞬间涌入1000任务 | 批量提交任务 | 队列可控,按容量分流 |
|
||
| 网络闪断 | 模拟网络抖动 | 自动重连,任务不丢失 |
|
||
| message-dispatcher 重启 | 重启服务 | 任务状态恢复,继续执行 |
|
||
| JWT Token 过期 | 模拟 Token 过期 | 自动获取新 Token,服务不中断 |
|
||
|
||
### 10.3 错误处理与恢复机制建议
|
||
|
||
#### 10.3.1 错误分类与处理策略
|
||
|
||
| 错误类型 | 严重程度 | 处理策略 |
|
||
|---------|---------|---------|
|
||
| 网络超时 | 低 | 重试(最多3次) |
|
||
| 实例离线 | 中 | 任务回退至缓存,重新调度 |
|
||
| Redis 连接失败 | 高 | 立即降级,使用本地缓存 |
|
||
| JWT Token 失效 | 中 | 立即刷新 Token |
|
||
| 内存溢出 | 严重 | 紧急降级,停止接收新任务 |
|
||
| 未知异常 | 中 | 记录日志,降级处理 |
|
||
|
||
#### 10.3.2 熔断机制
|
||
|
||
```javascript
|
||
class CircuitBreaker {
|
||
constructor(options) {
|
||
this.failureThreshold = options.failureThreshold || 5;
|
||
this.resetTimeout = options.resetTimeout || 30000;
|
||
this.failureCount = 0;
|
||
this.state = 'closed'; // closed, open, half-open
|
||
this.lastFailureTime = null;
|
||
}
|
||
|
||
async execute(fn) {
|
||
if (this.state === 'open') {
|
||
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
|
||
this.state = 'half-open';
|
||
} else {
|
||
throw new Error('Circuit breaker is open');
|
||
}
|
||
}
|
||
|
||
try {
|
||
const result = await fn();
|
||
this.onSuccess();
|
||
return result;
|
||
} catch (error) {
|
||
this.onFailure();
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
onSuccess() {
|
||
this.failureCount = 0;
|
||
this.state = 'closed';
|
||
}
|
||
|
||
onFailure() {
|
||
this.failureCount++;
|
||
this.lastFailureTime = Date.now();
|
||
|
||
if (this.failureCount >= this.failureThreshold) {
|
||
this.state = 'open';
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 10.3.3 状态快照与恢复
|
||
|
||
定期保存系统状态快照:
|
||
|
||
```javascript
|
||
async function saveStateSnapshot() {
|
||
const snapshot = {
|
||
timestamp: new Date().toISOString(),
|
||
connectionState: await getConnectionState(),
|
||
capacity: await getCapacity(),
|
||
pendingTasks: await getPendingTasks(100),
|
||
processingTasks: await getProcessingTasks()
|
||
};
|
||
|
||
await redis.set('md:snapshot:latest', JSON.stringify(snapshot));
|
||
await redis.lPush('md:snapshot:history', JSON.stringify(snapshot));
|
||
await redis.lTrim('md:snapshot:history', 0, 99); // 保留最近100个
|
||
}
|
||
|
||
async function restoreFromSnapshot() {
|
||
const snapshotData = await redis.get('md:snapshot:latest');
|
||
if (!snapshotData) return;
|
||
|
||
const snapshot = JSON.parse(snapshotData);
|
||
await restoreConnectionState(snapshot.connectionState);
|
||
await logRecovery(snapshot.timestamp);
|
||
}
|
||
```
|
||
|
||
#### 10.3.4 监控告警建议
|
||
|
||
| 监控指标 | 告警阈值 | 告警级别 |
|
||
|---------|---------|---------|
|
||
| 降级率 | > 50% | 严重 |
|
||
| 任务超时率 | > 10% | 警告 |
|
||
| 缓存队列长度 | > 100 | 警告 |
|
||
| 系统崩溃次数 | > 0 | 严重 |
|
||
| JWT Token 更新失败 | > 3次/小时 | 警告 |
|
||
| 内部容量使用率 | > 90% | 警告 |
|
||
|
||
---
|
||
|
||
## 11. 验证方法
|
||
|
||
### 11.1 接口兼容性验证
|
||
|
||
**验证清单:**
|
||
- [ ] messageDispatcher.js 与 runninghub.js 拥有完全相同的方法签名
|
||
- [ ] getGenerateBody() 返回的请求体字段名称与 runninghub 完全一致
|
||
- [ ] getGenerateBody() 返回的请求体字段类型与 runninghub 完全一致
|
||
- [ ] 仅 apiKey 字段的值被修改为 JWT Token
|
||
- [ ] 响应格式与 runninghub 兼容
|
||
|
||
**验证命令:**
|
||
```javascript
|
||
// 对比两个平台适配器的接口
|
||
const runninghubMethods = Object.keys(runninghub);
|
||
const messageDispatcherMethods = Object.keys(messageDispatcher);
|
||
assert.deepEqual(runninghubMethods, messageDispatcherMethods, '接口方法必须一致');
|
||
```
|
||
|
||
### 11.2 任务分流验证
|
||
|
||
**验证场景:**
|
||
1. 构造不同数量的待分发任务
|
||
2. 检查内部/外部任务分配比例
|
||
3. 验证不超过各自容量上限
|
||
|
||
**验证代码:**
|
||
```javascript
|
||
async function verifyTaskDistribution() {
|
||
const testCases = [
|
||
{ internal: 30, external: 10, total: 25, expectedInternal: 25, expectedExternal: 0 },
|
||
{ internal: 30, external: 10, total: 35, expectedInternal: 30, expectedExternal: 5 },
|
||
{ internal: 30, external: 10, total: 45, expectedInternal: 30, expectedExternal: 10 }
|
||
];
|
||
|
||
for (const testCase of testCases) {
|
||
const result = await distributeTasks(testCase);
|
||
assert(result.internalCount === testCase.expectedInternal);
|
||
assert(result.externalCount === testCase.expectedExternal);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 11.3 JWT 认证验证
|
||
|
||
**验证清单:**
|
||
- [ ] 系统启动时成功获取 JWT Token
|
||
- [ ] Token 正确存储在内存中
|
||
- [ ] 请求头中 apiKey 字段值为 JWT Token
|
||
- [ ] Token 在过期前自动更新
|
||
- [ ] Token 更新不影响正在进行的任务
|
||
|
||
---
|
||
|
||
## 12. 后续优化方向
|
||
|
||
1. **智能负载均衡** - 根据内部算力负载动态调整分发策略
|
||
2. **任务缓存** - 缓存常用工作流,减少重复提交
|
||
3. **批量处理** - 支持任务批量提交,提高吞吐量
|
||
4. **可视化监控** - 新增分发统计 dashboard
|
||
5. **A/B 测试** - 支持新旧方案并行,逐步切换
|
||
6. **预测性调度** - 基于历史数据预测任务完成时间
|
||
7. **多区域部署** - 支持跨区域算力调度
|
||
8. **成本优化** - 综合考虑内部成本与外部成本
|