mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
1222 lines
42 KiB
Plaintext
1222 lines
42 KiB
Plaintext
---
|
||
title: 'MCPHub Cluster Design'
|
||
description: 'Detailed design document for MCPHub cluster functionality supporting multi-node deployment'
|
||
---
|
||
|
||
# MCPHub 集群功能设计方案
|
||
|
||
## 1. 概述
|
||
|
||
### 1.1 设计目标
|
||
|
||
本方案旨在为 MCPHub 引入集群功能,支持多节点部署,实现以下目标:
|
||
|
||
- **高可用性**:通过多节点部署,避免单点故障
|
||
- **水平扩展**:支持动态增加 Worker 节点,提升 MCP Server 承载能力
|
||
- **负载均衡**:智能分发客户端请求到各节点,均衡系统负载
|
||
- **会话亲和性**:保证客户端 SSE 连接的稳定性(Sticky Session)
|
||
- **副本管理**:支持为单个 MCP Server 配置多个运行副本
|
||
- **兼容现有部署**:不影响现有单节点部署和使用方式
|
||
|
||
### 1.2 架构概览
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Master Node │
|
||
│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐│
|
||
Client Requests ───────►│ │ UI │ │ API │ │ Cluster Coordinator ││
|
||
(HTTP/SSE/MCP) │ │ (React) │ │ Routes │ │ - Node Registry ││
|
||
│ └──────────┘ └───────────┘ │ - Load Balancer ││
|
||
│ │ - Session Manager ││
|
||
│ ┌───────────────────────────┐│ - Health Monitor ││
|
||
│ │ Local MCP Servers │└───────────────────────┘│
|
||
│ │ (Optional) │ │
|
||
│ └───────────────────────────┘ │
|
||
└───────────────────────┬─────────────────────────────────┘
|
||
│
|
||
┌─────────────────────────────┼─────────────────────────────┐
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
|
||
│ Worker Node 1 │ │ Worker Node 2 │ │ Worker Node N │
|
||
│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │
|
||
│ │ MCP Server A │ │ │ │ MCP Server A │ │ │ │ MCP Server C │ │
|
||
│ │ (Replica 1) │ │ │ │ (Replica 2) │ │ │ │ (Replica 1) │ │
|
||
│ ├────────────────┤ │ │ ├────────────────┤ │ │ ├────────────────┤ │
|
||
│ │ MCP Server B │ │ │ │ MCP Server C │ │ │ │ MCP Server D │ │
|
||
│ │ (Replica 1) │ │ │ │ (Replica 2) │ │ │ │ (Replica 1) │ │
|
||
│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │
|
||
└────────────────────┘ └────────────────────┘ └────────────────────┘
|
||
```
|
||
|
||
## 2. 节点类型与职责
|
||
|
||
### 2.1 Master 节点(主节点)
|
||
|
||
Master 节点是集群的控制中心,负责:
|
||
|
||
| 职责 | 描述 |
|
||
|------|------|
|
||
| **UI 服务** | 对外提供 React 前端管理界面 |
|
||
| **API 网关** | 统一接收所有客户端 API 请求 |
|
||
| **SSE/MCP 端点** | 提供 `/sse`, `/mcp` 端点,代理到对应的 Worker |
|
||
| **节点注册** | 管理 Worker 节点的注册与注销 |
|
||
| **健康监控** | 监控所有节点和 MCP Server 的运行状态 |
|
||
| **负载均衡** | 根据策略分发请求到各 Worker 节点 |
|
||
| **会话管理** | 维护 Session 到 Worker 的映射关系 |
|
||
| **配置同步** | 将配置变更同步到所有 Worker 节点 |
|
||
| **可选:运行 MCP Server** | Master 也可以运行本地 MCP Server |
|
||
|
||
### 2.2 Worker 节点(从节点)
|
||
|
||
Worker 节点是实际运行 MCP Server 的工作节点:
|
||
|
||
| 职责 | 描述 |
|
||
|------|------|
|
||
| **运行 MCP Server** | 执行实际的 MCP Server 进程 |
|
||
| **心跳上报** | 定期向 Master 上报自身状态 |
|
||
| **接收配置** | 从 Master 接收 MCP Server 配置 |
|
||
| **暴露内部 API** | 提供 Master 调用的内部管理 API |
|
||
| **不对外暴露端点** | 不直接对外提供 UI/API/SSE/MCP 端点 |
|
||
|
||
## 3. 核心模块设计
|
||
|
||
### 3.1 新增类型定义
|
||
|
||
```typescript
|
||
// src/types/cluster.ts
|
||
|
||
/**
|
||
* 节点角色
|
||
*/
|
||
export type NodeRole = 'master' | 'worker';
|
||
|
||
/**
|
||
* 节点状态
|
||
*/
|
||
export type NodeStatus = 'online' | 'offline' | 'unhealthy';
|
||
|
||
/**
|
||
* 集群节点信息
|
||
*/
|
||
export interface ClusterNode {
|
||
id: string; // 节点唯一标识 (UUID)
|
||
role: NodeRole; // 节点角色
|
||
host: string; // 节点主机地址 (IP 或域名)
|
||
port: number; // 节点端口
|
||
internalPort?: number; // 内部通信端口 (默认与 port 相同)
|
||
status: NodeStatus; // 节点状态
|
||
lastHeartbeat: number; // 最后心跳时间戳
|
||
registeredAt: number; // 注册时间戳
|
||
metadata?: {
|
||
hostname?: string; // 主机名
|
||
version?: string; // MCPHub 版本
|
||
os?: string; // 操作系统
|
||
cpuUsage?: number; // CPU 使用率
|
||
memoryUsage?: number; // 内存使用率
|
||
};
|
||
}
|
||
|
||
/**
|
||
* MCP Server 副本信息
|
||
*/
|
||
export interface ServerReplica {
|
||
id: string; // 副本唯一标识
|
||
serverName: string; // MCP Server 名称
|
||
nodeId: string; // 运行节点 ID
|
||
status: 'running' | 'stopped' | 'error';
|
||
createdAt: number;
|
||
error?: string;
|
||
}
|
||
|
||
/**
|
||
* MCP Server 副本配置
|
||
*/
|
||
export interface ReplicaConfig {
|
||
serverName: string; // MCP Server 名称
|
||
replicas: number; // 期望副本数
|
||
affinity?: {
|
||
nodeIds?: string[]; // 首选节点列表
|
||
excludeNodeIds?: string[]; // 排除节点列表
|
||
};
|
||
strategy?: 'round-robin' | 'least-connections' | 'random';
|
||
}
|
||
|
||
/**
|
||
* 会话路由信息
|
||
*/
|
||
export interface SessionRoute {
|
||
sessionId: string; // 客户端会话 ID
|
||
nodeId: string; // 关联的 Worker 节点 ID
|
||
serverName: string; // 关联的 MCP Server 名称
|
||
replicaId: string; // 关联的副本 ID
|
||
createdAt: number;
|
||
lastActivity: number;
|
||
}
|
||
|
||
/**
|
||
* 集群配置
|
||
*/
|
||
export interface ClusterConfig {
|
||
enabled: boolean; // 是否启用集群模式
|
||
role: NodeRole; // 当前节点角色
|
||
nodeId?: string; // 节点 ID (自动生成或指定)
|
||
|
||
// Master 节点配置
|
||
master?: {
|
||
host: string; // Master 地址 (Worker 连接用)
|
||
port: number;
|
||
heartbeatInterval?: number; // 心跳间隔 (毫秒, 默认 5000)
|
||
heartbeatTimeout?: number; // 心跳超时 (毫秒, 默认 15000)
|
||
loadBalanceStrategy?: 'round-robin' | 'least-connections' | 'weighted';
|
||
};
|
||
|
||
// Worker 节点配置
|
||
worker?: {
|
||
masterUrl: string; // Master 节点 URL
|
||
advertiseHost?: string; // Worker 对外地址 (NAT 场景)
|
||
advertisePort?: number; // Worker 对外端口
|
||
maxServers?: number; // 最大运行 Server 数量
|
||
};
|
||
|
||
// 通信安全配置
|
||
security?: {
|
||
token?: string; // 节点间通信 Token
|
||
tlsEnabled?: boolean; // 是否启用 TLS
|
||
tlsCert?: string; // TLS 证书路径
|
||
tlsKey?: string; // TLS 私钥路径
|
||
};
|
||
}
|
||
```
|
||
|
||
### 3.2 集群协调器服务 (Master 端)
|
||
|
||
```typescript
|
||
// src/services/clusterCoordinatorService.ts
|
||
|
||
/**
|
||
* 集群协调器 - 运行在 Master 节点
|
||
* 职责:
|
||
* - 管理 Worker 节点注册与心跳
|
||
* - 负载均衡与请求路由
|
||
* - 会话亲和性管理
|
||
* - 副本调度与管理
|
||
*/
|
||
export class ClusterCoordinatorService {
|
||
private nodes: Map<string, ClusterNode>; // 已注册节点
|
||
private replicas: Map<string, ServerReplica[]>; // Server -> 副本列表
|
||
private sessions: Map<string, SessionRoute>; // 会话路由表
|
||
private replicaConfigs: Map<string, ReplicaConfig>; // 副本配置
|
||
|
||
/**
|
||
* 注册 Worker 节点
|
||
*/
|
||
async registerNode(nodeInfo: ClusterNode): Promise<void>;
|
||
|
||
/**
|
||
* 处理心跳
|
||
*/
|
||
async handleHeartbeat(nodeId: string, status: NodeHeartbeat): Promise<void>;
|
||
|
||
/**
|
||
* 分配 MCP Server 到 Worker
|
||
*/
|
||
async allocateServer(serverName: string, nodeId?: string): Promise<ServerReplica>;
|
||
|
||
/**
|
||
* 根据负载均衡策略选择节点
|
||
*/
|
||
async selectNode(serverName: string): Promise<ClusterNode>;
|
||
|
||
/**
|
||
* 获取或创建会话路由
|
||
*/
|
||
async getOrCreateSessionRoute(sessionId: string, serverName: string): Promise<SessionRoute>;
|
||
|
||
/**
|
||
* 代理请求到 Worker 节点
|
||
*/
|
||
async proxyToWorker(nodeId: string, path: string, options: RequestInit): Promise<Response>;
|
||
|
||
/**
|
||
* 同步配置到所有 Worker
|
||
*/
|
||
async syncConfigToWorkers(config: any): Promise<void>;
|
||
|
||
/**
|
||
* 获取集群状态摘要
|
||
*/
|
||
async getClusterStatus(): Promise<ClusterStatus>;
|
||
|
||
/**
|
||
* 设置 Server 副本数
|
||
*/
|
||
async setServerReplicas(serverName: string, replicas: number): Promise<void>;
|
||
|
||
/**
|
||
* 重新调度副本 (节点故障时)
|
||
*/
|
||
async rescheduleReplicas(failedNodeId: string): Promise<void>;
|
||
}
|
||
```
|
||
|
||
### 3.3 Worker 代理服务 (Worker 端)
|
||
|
||
```typescript
|
||
// src/services/clusterWorkerService.ts
|
||
|
||
/**
|
||
* Worker 代理服务 - 运行在 Worker 节点
|
||
* 职责:
|
||
* - 向 Master 注册和发送心跳
|
||
* - 接收并执行 MCP Server 管理指令
|
||
* - 上报本地 Server 状态
|
||
*/
|
||
export class ClusterWorkerService {
|
||
private masterClient: MasterClient; // Master 通信客户端
|
||
private localServers: Map<string, ServerInfo>; // 本地运行的 Server
|
||
|
||
/**
|
||
* 连接到 Master 并注册
|
||
*/
|
||
async connectToMaster(): Promise<void>;
|
||
|
||
/**
|
||
* 发送心跳
|
||
*/
|
||
async sendHeartbeat(): Promise<void>;
|
||
|
||
/**
|
||
* 启动 MCP Server
|
||
*/
|
||
async startServer(serverName: string, config: ServerConfig): Promise<ServerReplica>;
|
||
|
||
/**
|
||
* 停止 MCP Server
|
||
*/
|
||
async stopServer(serverName: string): Promise<void>;
|
||
|
||
/**
|
||
* 获取本地 Server 状态
|
||
*/
|
||
getLocalServerStatus(): ServerInfo[];
|
||
|
||
/**
|
||
* 处理来自 Master 的配置更新
|
||
*/
|
||
async handleConfigUpdate(config: any): Promise<void>;
|
||
}
|
||
```
|
||
|
||
### 3.4 请求代理服务
|
||
|
||
```typescript
|
||
// src/services/clusterProxyService.ts
|
||
|
||
/**
|
||
* 请求代理服务 - 运行在 Master 节点
|
||
* 职责:
|
||
* - 代理 SSE 连接到 Worker
|
||
* - 代理 MCP 请求到 Worker
|
||
* - 维护代理连接池
|
||
*/
|
||
export class ClusterProxyService {
|
||
private connectionPool: Map<string, ProxyConnection>;
|
||
|
||
/**
|
||
* 代理 SSE 连接
|
||
* 建立 Client -> Master -> Worker 的 SSE 通道
|
||
*/
|
||
async proxySseConnection(
|
||
req: Request,
|
||
res: Response,
|
||
targetNode: ClusterNode,
|
||
sessionId: string
|
||
): Promise<void>;
|
||
|
||
/**
|
||
* 代理 MCP POST 请求
|
||
*/
|
||
async proxyMcpRequest(
|
||
req: Request,
|
||
res: Response,
|
||
targetNode: ClusterNode,
|
||
sessionId: string
|
||
): Promise<void>;
|
||
|
||
/**
|
||
* 代理 Tool 调用请求
|
||
*/
|
||
async proxyToolCall(
|
||
serverName: string,
|
||
toolName: string,
|
||
args: any,
|
||
sessionId: string
|
||
): Promise<any>;
|
||
}
|
||
```
|
||
|
||
## 4. API 设计
|
||
|
||
### 4.1 复用现有 API
|
||
|
||
以下现有 API 可在 Worker 节点复用,Master 通过这些 API 与 Worker 通信:
|
||
|
||
| API | 方法 | 用途 |
|
||
|-----|------|------|
|
||
| `/health` | GET | 节点健康检查 |
|
||
| `/api/servers` | GET | 获取 Server 列表和状态 |
|
||
| `/api/servers/:name` | GET | 获取单个 Server 配置 |
|
||
| `/api/servers` | POST | 创建 Server |
|
||
| `/api/servers/:name` | DELETE | 删除 Server |
|
||
| `/api/servers/:name/toggle` | POST | 启用/禁用 Server |
|
||
| `/api/servers/:name/reload` | POST | 重新加载 Server |
|
||
| `/api/tools/call/:server` | POST | 调用 Tool |
|
||
| `/sse/:group?` | GET | SSE 连接 |
|
||
| `/mcp/:group?` | POST | MCP Streamable HTTP 请求 |
|
||
|
||
### 4.2 新增集群管理 API
|
||
|
||
```typescript
|
||
// src/routes/clusterRoutes.ts
|
||
|
||
// ==================== Master 端 API ====================
|
||
|
||
/**
|
||
* 获取集群状态
|
||
* GET /api/cluster/status
|
||
*/
|
||
interface ClusterStatusResponse {
|
||
enabled: boolean;
|
||
role: 'master';
|
||
nodes: ClusterNode[];
|
||
totalReplicas: number;
|
||
healthyReplicas: number;
|
||
sessions: number;
|
||
}
|
||
|
||
/**
|
||
* 获取所有节点
|
||
* GET /api/cluster/nodes
|
||
*/
|
||
interface NodesResponse {
|
||
nodes: ClusterNode[];
|
||
}
|
||
|
||
/**
|
||
* 获取节点详情
|
||
* GET /api/cluster/nodes/:nodeId
|
||
*/
|
||
interface NodeDetailResponse {
|
||
node: ClusterNode;
|
||
servers: ServerReplica[];
|
||
}
|
||
|
||
/**
|
||
* 移除节点
|
||
* DELETE /api/cluster/nodes/:nodeId
|
||
*/
|
||
|
||
/**
|
||
* 获取 Server 副本配置
|
||
* GET /api/cluster/replicas/:serverName
|
||
*/
|
||
interface ReplicaConfigResponse {
|
||
serverName: string;
|
||
desiredReplicas: number;
|
||
currentReplicas: number;
|
||
replicas: ServerReplica[];
|
||
}
|
||
|
||
/**
|
||
* 设置 Server 副本数
|
||
* PUT /api/cluster/replicas/:serverName
|
||
*/
|
||
interface SetReplicasRequest {
|
||
replicas: number;
|
||
affinity?: {
|
||
nodeIds?: string[];
|
||
excludeNodeIds?: string[];
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取会话路由表
|
||
* GET /api/cluster/sessions
|
||
*/
|
||
interface SessionsResponse {
|
||
sessions: SessionRoute[];
|
||
}
|
||
|
||
// ==================== Worker 端内部 API ====================
|
||
|
||
/**
|
||
* 节点注册 (Worker -> Master)
|
||
* POST /internal/cluster/register
|
||
*/
|
||
interface RegisterRequest {
|
||
nodeId: string;
|
||
host: string;
|
||
port: number;
|
||
metadata?: ClusterNode['metadata'];
|
||
}
|
||
|
||
/**
|
||
* 心跳 (Worker -> Master)
|
||
* POST /internal/cluster/heartbeat
|
||
*/
|
||
interface HeartbeatRequest {
|
||
nodeId: string;
|
||
servers: Array<{
|
||
name: string;
|
||
status: 'connected' | 'disconnected';
|
||
tools: number;
|
||
}>;
|
||
metrics?: {
|
||
cpuUsage: number;
|
||
memoryUsage: number;
|
||
activeConnections: number;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 分配 Server (Master -> Worker)
|
||
* POST /internal/servers/allocate
|
||
*/
|
||
interface AllocateServerRequest {
|
||
serverName: string;
|
||
config: ServerConfig;
|
||
replicaId: string;
|
||
}
|
||
|
||
/**
|
||
* 释放 Server (Master -> Worker)
|
||
* POST /internal/servers/release
|
||
*/
|
||
interface ReleaseServerRequest {
|
||
serverName: string;
|
||
replicaId: string;
|
||
}
|
||
```
|
||
|
||
## 5. 数据流与通信机制
|
||
|
||
### 5.1 Worker 节点注册流程
|
||
|
||
```
|
||
┌────────────┐ ┌────────────┐
|
||
│ Worker │ │ Master │
|
||
└─────┬──────┘ └──────┬─────┘
|
||
│ │
|
||
│ 1. POST /internal/cluster/register │
|
||
│ { nodeId, host, port, metadata } │
|
||
│ ─────────────────────────────────────────────► │
|
||
│ │
|
||
│ 2. Response: { success, config, servers } │
|
||
│ ◄───────────────────────────────────────────── │
|
||
│ │
|
||
│ 3. 启动分配的 MCP Servers │
|
||
│ │
|
||
│ 4. POST /internal/cluster/heartbeat (定期) │
|
||
│ { nodeId, servers: [...], metrics } │
|
||
│ ─────────────────────────────────────────────► │
|
||
│ │
|
||
```
|
||
|
||
### 5.2 客户端请求路由流程 (SSE)
|
||
|
||
```
|
||
┌────────────┐ ┌────────────┐ ┌────────────┐
|
||
│ Client │ │ Master │ │ Worker │
|
||
└─────┬──────┘ └──────┬─────┘ └──────┬─────┘
|
||
│ │ │
|
||
│ 1. GET /sse/group │ │
|
||
│ ─────────────────────►│ │
|
||
│ │ │
|
||
│ 2. 查询会话路由表 │ │
|
||
│ 3. 选择目标 Worker │ │
|
||
│ │ │
|
||
│ │ 4. GET /sse/group │
|
||
│ │ (internal) │
|
||
│ │ ─────────────────────►│
|
||
│ │ │
|
||
│ 5. SSE Stream │ ◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
|
||
│ (proxied) │ SSE Stream │
|
||
│ ◄─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │
|
||
│ │ │
|
||
│ 6. POST /messages │ │
|
||
│ ─────────────────────►│ │
|
||
│ │ │
|
||
│ │ 7. POST /messages │
|
||
│ │ (to same Worker) │
|
||
│ │ ─────────────────────►│
|
||
│ │ │
|
||
```
|
||
|
||
### 5.3 Tool 调用请求路由
|
||
|
||
```
|
||
┌────────────┐ ┌────────────┐ ┌────────────┐
|
||
│ Client │ │ Master │ │ Worker │
|
||
└─────┬──────┘ └──────┬─────┘ └──────┬─────┘
|
||
│ │ │
|
||
│ 1. POST /mcp/group │ │
|
||
│ {call_tool: xxx} │ │
|
||
│ ─────────────────────►│ │
|
||
│ │ │
|
||
│ 2. 解析目标 Server │ │
|
||
│ 3. 查找 Server 副本 │ │
|
||
│ 4. 负载均衡选择 │ │
|
||
│ │ │
|
||
│ │ 5. POST /api/tools/ │
|
||
│ │ call/:server │
|
||
│ │ ─────────────────────►│
|
||
│ │ │
|
||
│ │ 6. Tool Result │
|
||
│ │ ◄─────────────────────│
|
||
│ │ │
|
||
│ 7. Result (proxied) │ │
|
||
│ ◄─────────────────────│ │
|
||
│ │ │
|
||
```
|
||
|
||
## 6. 负载均衡策略
|
||
|
||
### 6.1 支持的策略
|
||
|
||
| 策略 | 描述 | 适用场景 |
|
||
|------|------|----------|
|
||
| **Round Robin** | 轮询分发请求 | 节点配置相同时 |
|
||
| **Least Connections** | 选择当前连接数最少的节点 | 长连接场景 (SSE) |
|
||
| **Weighted** | 根据节点权重分发 | 节点性能差异大时 |
|
||
|
||
### 6.2 会话亲和性 (Sticky Session)
|
||
|
||
```typescript
|
||
/**
|
||
* 会话亲和性实现
|
||
* 确保同一 Session 的所有请求路由到同一 Worker
|
||
*/
|
||
class SessionAffinityManager {
|
||
private sessionMap: Map<string, string>; // sessionId -> nodeId
|
||
|
||
/**
|
||
* 绑定会话到节点
|
||
*/
|
||
bindSession(sessionId: string, nodeId: string): void;
|
||
|
||
/**
|
||
* 获取会话绑定的节点
|
||
*/
|
||
getNodeForSession(sessionId: string): string | undefined;
|
||
|
||
/**
|
||
* 解绑会话 (连接关闭时)
|
||
*/
|
||
unbindSession(sessionId: string): void;
|
||
|
||
/**
|
||
* 节点故障时迁移会话
|
||
*/
|
||
migrateSessionsFromNode(failedNodeId: string, newNodeId: string): void;
|
||
}
|
||
```
|
||
|
||
## 7. 副本管理
|
||
|
||
### 7.1 副本调度算法
|
||
|
||
```typescript
|
||
/**
|
||
* 副本调度器
|
||
* 负责将 MCP Server 副本分配到合适的 Worker 节点
|
||
*/
|
||
class ReplicaScheduler {
|
||
/**
|
||
* 计算最优节点分配
|
||
*/
|
||
schedule(
|
||
serverName: string,
|
||
desiredReplicas: number,
|
||
nodes: ClusterNode[],
|
||
currentReplicas: ServerReplica[]
|
||
): ScheduleResult {
|
||
// 1. 过滤可用节点
|
||
const availableNodes = nodes.filter(n =>
|
||
n.status === 'online' &&
|
||
!this.isNodeExcluded(n, config.affinity)
|
||
);
|
||
|
||
// 2. 计算每个节点的得分
|
||
const scores = availableNodes.map(node => ({
|
||
node,
|
||
score: this.calculateScore(node, serverName)
|
||
}));
|
||
|
||
// 3. 按得分排序并选择
|
||
scores.sort((a, b) => b.score - a.score);
|
||
|
||
// 4. 返回调度结果
|
||
return {
|
||
toCreate: [...], // 需要在哪些节点创建副本
|
||
toRemove: [...], // 需要移除哪些副本
|
||
};
|
||
}
|
||
|
||
private calculateScore(node: ClusterNode, serverName: string): number {
|
||
let score = 100;
|
||
|
||
// 已运行副本数越少得分越高
|
||
const currentCount = this.getReplicaCount(node.id, serverName);
|
||
score -= currentCount * 20;
|
||
|
||
// CPU/内存使用率越低得分越高
|
||
if (node.metadata?.cpuUsage) {
|
||
score -= node.metadata.cpuUsage * 0.5;
|
||
}
|
||
|
||
// 节点亲和性加分
|
||
if (this.hasAffinity(node.id, serverName)) {
|
||
score += 30;
|
||
}
|
||
|
||
return score;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.2 副本生命周期
|
||
|
||
```
|
||
创建请求
|
||
│
|
||
▼
|
||
┌───────────────────────────────┐
|
||
│ Pending │
|
||
│ (等待调度到 Worker) │
|
||
└───────────────┬───────────────┘
|
||
│ 调度成功
|
||
▼
|
||
┌───────────────────────────────┐
|
||
│ Starting │
|
||
│ (Worker 启动 Server 中) │
|
||
└───────────────┬───────────────┘
|
||
│ 启动成功
|
||
▼
|
||
┌───────────────────────────────┐
|
||
│ Running │◄───┐
|
||
│ (Server 运行中) │ │ 健康检查通过
|
||
└───────────────┬───────────────┘ │
|
||
│ 错误/停止 │
|
||
▼ │
|
||
┌───────────────────────────────┐ │
|
||
│ Error/Stopped │────┘
|
||
│ (需要重试或移除) │ 重试
|
||
└───────────────┬───────────────┘
|
||
│ 删除
|
||
▼
|
||
┌───────────────────────────────┐
|
||
│ Terminated │
|
||
│ (副本已删除) │
|
||
└───────────────────────────────┘
|
||
```
|
||
|
||
## 8. 健康检查与故障恢复
|
||
|
||
### 8.1 健康检查机制
|
||
|
||
```typescript
|
||
/**
|
||
* 健康检查器
|
||
*/
|
||
class HealthChecker {
|
||
private checkInterval: number = 5000; // 检查间隔 (毫秒)
|
||
private unhealthyThreshold: number = 3; // 连续失败次数阈值
|
||
private failureCounts: Map<string, number>; // 节点失败计数
|
||
|
||
/**
|
||
* 执行健康检查
|
||
*/
|
||
async checkNode(node: ClusterNode): Promise<HealthStatus> {
|
||
try {
|
||
// 调用 Worker 的 /health 端点
|
||
const response = await fetch(`http://${node.host}:${node.port}/health`, {
|
||
timeout: 3000,
|
||
headers: { 'X-Cluster-Token': this.token }
|
||
});
|
||
|
||
if (response.ok) {
|
||
this.failureCounts.set(node.id, 0);
|
||
return { healthy: true };
|
||
}
|
||
} catch (error) {
|
||
// 累加失败次数
|
||
const count = (this.failureCounts.get(node.id) || 0) + 1;
|
||
this.failureCounts.set(node.id, count);
|
||
|
||
if (count >= this.unhealthyThreshold) {
|
||
return { healthy: false, reason: 'consecutive_failures' };
|
||
}
|
||
}
|
||
|
||
return { healthy: true }; // 尚未达到阈值
|
||
}
|
||
|
||
/**
|
||
* 处理节点故障
|
||
*/
|
||
async handleNodeFailure(nodeId: string): Promise<void> {
|
||
// 1. 标记节点为 offline
|
||
await this.coordinator.markNodeOffline(nodeId);
|
||
|
||
// 2. 重新调度该节点上的副本
|
||
await this.coordinator.rescheduleReplicas(nodeId);
|
||
|
||
// 3. 通知管理员 (可选)
|
||
await this.notifyAdmin(`Node ${nodeId} is offline`);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.2 故障恢复流程
|
||
|
||
```
|
||
Worker 节点故障
|
||
│
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ Master 检测到心跳超时 │
|
||
│ (连续 3 次健康检查失败) │
|
||
└──────────────┬──────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ 标记节点为 offline │
|
||
│ 更新节点状态到 UI │
|
||
└──────────────┬──────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ 获取故障节点上的所有副本 │
|
||
└──────────────┬──────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ 调用 ReplicaScheduler │
|
||
│ 重新调度副本到其他健康节点 │
|
||
└──────────────┬──────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ 迁移相关会话到新节点 │
|
||
│ (如启用会话重建) │
|
||
└──────────────┬──────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ 故障恢复完成 │
|
||
│ 系统继续正常服务 │
|
||
└─────────────────────────────┘
|
||
```
|
||
|
||
## 9. 配置管理
|
||
|
||
### 9.1 集群配置示例
|
||
|
||
```json
|
||
// 环境变量或配置文件
|
||
{
|
||
"cluster": {
|
||
"enabled": true,
|
||
"role": "master",
|
||
"nodeId": "master-001",
|
||
"master": {
|
||
"host": "0.0.0.0",
|
||
"port": 3000,
|
||
"heartbeatInterval": 5000,
|
||
"heartbeatTimeout": 15000,
|
||
"loadBalanceStrategy": "least-connections"
|
||
},
|
||
"security": {
|
||
"token": "your-cluster-secret-token",
|
||
"tlsEnabled": false
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
```json
|
||
// Worker 节点配置
|
||
{
|
||
"cluster": {
|
||
"enabled": true,
|
||
"role": "worker",
|
||
"nodeId": "worker-001",
|
||
"worker": {
|
||
"masterUrl": "http://master-host:3000",
|
||
"advertiseHost": "worker-001.internal",
|
||
"advertisePort": 3001,
|
||
"maxServers": 20
|
||
},
|
||
"security": {
|
||
"token": "your-cluster-secret-token"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.2 环境变量配置
|
||
|
||
| 环境变量 | 描述 | 默认值 |
|
||
|----------|------|--------|
|
||
| `CLUSTER_ENABLED` | 启用集群模式 | `false` |
|
||
| `CLUSTER_ROLE` | 节点角色 (master/worker) | - |
|
||
| `CLUSTER_NODE_ID` | 节点 ID | 自动生成 UUID |
|
||
| `CLUSTER_MASTER_HOST` | Master 主机地址 | `0.0.0.0` |
|
||
| `CLUSTER_MASTER_PORT` | Master 端口 | `3000` |
|
||
| `CLUSTER_MASTER_URL` | Master URL (Worker 用) | - |
|
||
| `CLUSTER_TOKEN` | 集群通信 Token | - |
|
||
| `CLUSTER_HEARTBEAT_INTERVAL` | 心跳间隔 (ms) | `5000` |
|
||
| `CLUSTER_HEARTBEAT_TIMEOUT` | 心跳超时 (ms) | `15000` |
|
||
| `CLUSTER_ADVERTISE_HOST` | Worker 对外地址 | - |
|
||
| `CLUSTER_ADVERTISE_PORT` | Worker 对外端口 | - |
|
||
|
||
## 10. 前端 UI 扩展
|
||
|
||
### 10.1 集群仪表板
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ Cluster Dashboard [Refresh] │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ Total Nodes │ │ Online Nodes │ │ Total Replicas │ │
|
||
│ │ 4 │ │ 3 │ │ 12 │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
│ │
|
||
│ Nodes │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ ● master-001 │ Master │ 192.168.1.10:3000 │ Online │ 0/0 │ │
|
||
│ │ ● worker-001 │ Worker │ 192.168.1.11:3001 │ Online │ 5/20 │ │
|
||
│ │ ● worker-002 │ Worker │ 192.168.1.12:3001 │ Online │ 4/20 │ │
|
||
│ │ ○ worker-003 │ Worker │ 192.168.1.13:3001 │ Offline│ - │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 10.2 副本管理界面
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ Server: fetch │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Replicas: ← 2 → [Apply] │
|
||
│ │
|
||
│ Current Replicas: │
|
||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||
│ │ fetch-replica-1 │ worker-001 │ ● Running │ Since: 2h ago │ │
|
||
│ │ fetch-replica-2 │ worker-002 │ ● Running │ Since: 1h ago │ │
|
||
│ └─────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Node Affinity: │
|
||
│ □ worker-001 ☑ worker-002 □ worker-003 (offline) │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 11. 单节点兼容性
|
||
|
||
### 11.1 兼容性设计
|
||
|
||
为确保集群功能不影响现有单节点部署:
|
||
|
||
1. **默认关闭**:`CLUSTER_ENABLED=false` 为默认值
|
||
2. **条件加载**:集群相关服务只在启用时初始化
|
||
3. **代码隔离**:集群逻辑封装在独立模块,通过依赖注入使用
|
||
4. **无缝切换**:同一镜像支持单节点/集群两种模式
|
||
|
||
```typescript
|
||
// src/server.ts 修改示例
|
||
|
||
export class AppServer {
|
||
private clusterCoordinator?: ClusterCoordinatorService;
|
||
private clusterWorker?: ClusterWorkerService;
|
||
|
||
async initialize(): Promise<void> {
|
||
// ... 现有初始化代码 ...
|
||
|
||
// 条件初始化集群功能
|
||
if (config.cluster?.enabled) {
|
||
if (config.cluster.role === 'master') {
|
||
this.clusterCoordinator = new ClusterCoordinatorService();
|
||
await this.clusterCoordinator.start();
|
||
console.log('Cluster coordinator started (Master mode)');
|
||
} else if (config.cluster.role === 'worker') {
|
||
this.clusterWorker = new ClusterWorkerService();
|
||
await this.clusterWorker.connectToMaster();
|
||
console.log('Connected to master node (Worker mode)');
|
||
}
|
||
} else {
|
||
console.log('Running in standalone mode');
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 11.2 路由适配
|
||
|
||
```typescript
|
||
// src/middlewares/clusterMiddleware.ts
|
||
|
||
/**
|
||
* 集群路由中间件
|
||
* 在 Master 模式下,根据会话路由请求到对应 Worker
|
||
* 在单节点或 Worker 模式下,直接处理请求
|
||
*/
|
||
export const clusterRouteMiddleware = async (
|
||
req: Request,
|
||
res: Response,
|
||
next: NextFunction
|
||
) => {
|
||
// 单节点模式或 Worker 模式 - 直接处理
|
||
if (!config.cluster?.enabled || config.cluster.role !== 'master') {
|
||
return next();
|
||
}
|
||
|
||
// Master 模式 - 检查是否需要代理
|
||
const sessionId = req.headers['mcp-session-id'] as string;
|
||
|
||
if (sessionId) {
|
||
const route = await clusterCoordinator.getSessionRoute(sessionId);
|
||
if (route && route.nodeId !== config.cluster.nodeId) {
|
||
// 代理到 Worker
|
||
return clusterProxy.proxyToWorker(route.nodeId, req, res);
|
||
}
|
||
}
|
||
|
||
// 本地处理
|
||
next();
|
||
};
|
||
```
|
||
|
||
## 12. Docker 部署示例
|
||
|
||
### 12.1 Docker Compose 集群配置
|
||
|
||
```yaml
|
||
# docker-compose.cluster.yml
|
||
|
||
version: '3.8'
|
||
|
||
services:
|
||
mcphub-master:
|
||
image: samanhappy/mcphub:latest
|
||
ports:
|
||
- "3000:3000"
|
||
environment:
|
||
- CLUSTER_ENABLED=true
|
||
- CLUSTER_ROLE=master
|
||
- CLUSTER_NODE_ID=master-001
|
||
- CLUSTER_MASTER_HOST=0.0.0.0
|
||
- CLUSTER_MASTER_PORT=3000
|
||
- CLUSTER_TOKEN=your-secure-cluster-token
|
||
- CLUSTER_HEARTBEAT_INTERVAL=5000
|
||
- CLUSTER_HEARTBEAT_TIMEOUT=15000
|
||
volumes:
|
||
- ./mcp_settings.json:/app/mcp_settings.json
|
||
networks:
|
||
- mcphub-cluster
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 3
|
||
|
||
mcphub-worker-1:
|
||
image: samanhappy/mcphub:latest
|
||
environment:
|
||
- CLUSTER_ENABLED=true
|
||
- CLUSTER_ROLE=worker
|
||
- CLUSTER_NODE_ID=worker-001
|
||
- CLUSTER_MASTER_URL=http://mcphub-master:3000
|
||
- CLUSTER_TOKEN=your-secure-cluster-token
|
||
- CLUSTER_ADVERTISE_HOST=mcphub-worker-1
|
||
- CLUSTER_ADVERTISE_PORT=3000
|
||
depends_on:
|
||
mcphub-master:
|
||
condition: service_healthy
|
||
networks:
|
||
- mcphub-cluster
|
||
|
||
mcphub-worker-2:
|
||
image: samanhappy/mcphub:latest
|
||
environment:
|
||
- CLUSTER_ENABLED=true
|
||
- CLUSTER_ROLE=worker
|
||
- CLUSTER_NODE_ID=worker-002
|
||
- CLUSTER_MASTER_URL=http://mcphub-master:3000
|
||
- CLUSTER_TOKEN=your-secure-cluster-token
|
||
- CLUSTER_ADVERTISE_HOST=mcphub-worker-2
|
||
- CLUSTER_ADVERTISE_PORT=3000
|
||
depends_on:
|
||
mcphub-master:
|
||
condition: service_healthy
|
||
networks:
|
||
- mcphub-cluster
|
||
|
||
networks:
|
||
mcphub-cluster:
|
||
driver: bridge
|
||
```
|
||
|
||
### 12.2 Kubernetes 部署 (可选)
|
||
|
||
```yaml
|
||
# kubernetes/mcphub-cluster.yaml
|
||
|
||
apiVersion: apps/v1
|
||
kind: Deployment
|
||
metadata:
|
||
name: mcphub-master
|
||
spec:
|
||
replicas: 1 # Master 通常只需要 1 个
|
||
selector:
|
||
matchLabels:
|
||
app: mcphub
|
||
role: master
|
||
template:
|
||
metadata:
|
||
labels:
|
||
app: mcphub
|
||
role: master
|
||
spec:
|
||
containers:
|
||
- name: mcphub
|
||
image: samanhappy/mcphub:latest
|
||
ports:
|
||
- containerPort: 3000
|
||
env:
|
||
- name: CLUSTER_ENABLED
|
||
value: "true"
|
||
- name: CLUSTER_ROLE
|
||
value: "master"
|
||
- name: CLUSTER_TOKEN
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: mcphub-cluster-secret
|
||
key: token
|
||
|
||
---
|
||
apiVersion: apps/v1
|
||
kind: Deployment
|
||
metadata:
|
||
name: mcphub-worker
|
||
spec:
|
||
replicas: 3 # Worker 可以水平扩展
|
||
selector:
|
||
matchLabels:
|
||
app: mcphub
|
||
role: worker
|
||
template:
|
||
metadata:
|
||
labels:
|
||
app: mcphub
|
||
role: worker
|
||
spec:
|
||
containers:
|
||
- name: mcphub
|
||
image: samanhappy/mcphub:latest
|
||
env:
|
||
- name: CLUSTER_ENABLED
|
||
value: "true"
|
||
- name: CLUSTER_ROLE
|
||
value: "worker"
|
||
- name: CLUSTER_MASTER_URL
|
||
value: "http://mcphub-master:3000"
|
||
- name: CLUSTER_TOKEN
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: mcphub-cluster-secret
|
||
key: token
|
||
```
|
||
|
||
## 13. 实现计划与优先级
|
||
|
||
### Phase 1: 基础架构 (优先级: 高)
|
||
|
||
| 任务 | 描述 | 工作量估算 |
|
||
|------|------|-----------|
|
||
| 类型定义 | 新增集群相关 TypeScript 类型 | 1 天 |
|
||
| 配置加载 | 支持集群配置环境变量/配置文件 | 1 天 |
|
||
| ClusterCoordinatorService | Master 端核心服务框架 | 3 天 |
|
||
| ClusterWorkerService | Worker 端核心服务框架 | 2 天 |
|
||
| 节点注册与心跳 | 实现 Worker 注册和心跳机制 | 2 天 |
|
||
|
||
### Phase 2: 请求路由 (优先级: 高)
|
||
|
||
| 任务 | 描述 | 工作量估算 |
|
||
|------|------|-----------|
|
||
| 会话路由表 | 实现 Session 到 Node 的映射管理 | 1 天 |
|
||
| SSE 代理 | 实现 SSE 连接的透明代理 | 3 天 |
|
||
| MCP 请求代理 | 实现 MCP POST 请求代理 | 2 天 |
|
||
| 负载均衡 | 实现 Round-Robin 和 Least-Connections | 2 天 |
|
||
|
||
### Phase 3: 副本管理 (优先级: 中)
|
||
|
||
| 任务 | 描述 | 工作量估算 |
|
||
|------|------|-----------|
|
||
| 副本调度器 | 实现副本分配算法 | 2 天 |
|
||
| 副本 CRUD API | 实现副本管理 REST API | 1 天 |
|
||
| 自动扩缩容 | 支持动态调整副本数 | 2 天 |
|
||
|
||
### Phase 4: 高可用与监控 (优先级: 中)
|
||
|
||
| 任务 | 描述 | 工作量估算 |
|
||
|------|------|-----------|
|
||
| 健康检查 | 实现节点健康检查机制 | 1 天 |
|
||
| 故障检测与恢复 | 实现节点故障时的副本迁移 | 3 天 |
|
||
| 监控指标 | 收集并暴露集群指标 | 2 天 |
|
||
|
||
### Phase 5: 前端 UI (优先级: 低)
|
||
|
||
| 任务 | 描述 | 工作量估算 |
|
||
|------|------|-----------|
|
||
| 集群仪表板 | 新增集群状态展示页面 | 3 天 |
|
||
| 节点管理 | 节点列表和详情页面 | 2 天 |
|
||
| 副本管理 | Server 副本配置 UI | 2 天 |
|
||
|
||
### 总工作量估算
|
||
|
||
- **Phase 1-2 (核心功能)**: 约 17 天
|
||
- **Phase 3-4 (增强功能)**: 约 9 天
|
||
- **Phase 5 (UI)**: 约 7 天
|
||
- **总计**: 约 33 个工作日
|
||
|
||
## 14. 风险与缓解措施
|
||
|
||
| 风险 | 影响 | 缓解措施 |
|
||
|------|------|----------|
|
||
| SSE 长连接代理复杂性 | 可能导致连接不稳定 | 使用成熟的代理库,添加重连机制 |
|
||
| 网络分区 | 脑裂问题 | 实现简单的 quorum 机制,或依赖外部协调服务 |
|
||
| 会话状态丢失 | 故障转移时连接中断 | 复用现有的 Session Rebuild 机制 |
|
||
| 性能开销 | 代理层增加延迟 | 优化代理路径,支持连接复用 |
|
||
| 配置同步延迟 | Worker 配置不一致 | 使用版本号,支持配置强制刷新 |
|
||
|
||
## 15. 未来扩展
|
||
|
||
1. **多 Master 高可用**: 支持 Master 节点热备
|
||
2. **地理分布式部署**: 支持跨数据中心部署
|
||
3. **自动伸缩**: 根据负载自动增减 Worker 节点
|
||
4. **服务网格集成**: 与 Istio/Linkerd 等服务网格集成
|
||
5. **Prometheus/Grafana 监控**: 提供标准监控指标和仪表板
|
||
|
||
---
|
||
|
||
**文档版本**: 1.0.0
|
||
**最后更新**: 2024-12-05
|
||
**作者**: MCPHub Team
|