feat(反向WS管理): 实现多前端支持与完善清理机制
- 为每个前端创建独立的Bot实例,防止状态混乱 - 分离消息锁和时间戳存储,修复清理逻辑错误 - 完善客户端断开时的清理逻辑,包括负载计数和健康状态 - 添加文档说明多前端支持的功能和解决方案
This commit is contained in:
175
MULTI_FRONTEND_SUPPORT.md
Normal file
175
MULTI_FRONTEND_SUPPORT.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 多前端支持问题分析和解决方案
|
||||
|
||||
## 已实现的功能
|
||||
|
||||
### 1. 负载均衡
|
||||
- ✅ 自动选择负载最低的健康客户端
|
||||
- ✅ 健康检查(30秒内有活动)
|
||||
- ✅ 负载计数(消息处理次数)
|
||||
- ✅ API调用时自动切换客户端
|
||||
|
||||
### 2. 防重复发送
|
||||
- ✅ 事件ID检查(`id`/`post_id`/`time`)
|
||||
- ✅ 消息锁机制(异步锁)
|
||||
- ✅ 双重检查(锁内外各一次)
|
||||
- ✅ 自动清理过期数据
|
||||
|
||||
### 3. 多前端支持
|
||||
- ✅ 每个前端独立的Bot实例
|
||||
- ✅ 客户端连接/断开管理
|
||||
- ✅ 完整的清理机制
|
||||
|
||||
## 已修复的问题
|
||||
|
||||
### 1. 清理不完整
|
||||
**问题**:客户端断开连接时没有清理负载计数和健康状态
|
||||
|
||||
**解决方案**:
|
||||
```python
|
||||
async def _disconnect_client(self, client_id: str) -> None:
|
||||
if client_id in self.clients:
|
||||
del self.clients[client_id]
|
||||
if client_id in self.client_self_ids:
|
||||
del self.client_self_ids[client_id]
|
||||
if client_id in self._client_load:
|
||||
del self._client_load[client_id]
|
||||
if client_id in self._client_health:
|
||||
del self._client_health[client_id]
|
||||
if client_id in self.bots:
|
||||
del self.bots[client_id]
|
||||
```
|
||||
|
||||
### 2. Bot实例共享
|
||||
**问题**:多个前端共享同一个Bot实例,可能导致状态混乱
|
||||
|
||||
**解决方案**:为每个前端创建独立的Bot实例
|
||||
```python
|
||||
# Bot实例字典(每个前端独立的Bot实例)
|
||||
self.bots: Dict[str, Bot] = {}
|
||||
|
||||
# 为每个前端创建独立的Bot实例
|
||||
if client_id not in self.bots:
|
||||
temp_ws = WS()
|
||||
temp_ws.self_id = event.self_id if hasattr(event, 'self_id') else 0
|
||||
self.bots[client_id] = Bot(temp_ws)
|
||||
|
||||
event.bot = self.bots[client_id]
|
||||
```
|
||||
|
||||
### 3. 清理逻辑错误
|
||||
**问题**:清理过期数据时,将Lock对象当作时间戳来处理
|
||||
|
||||
**解决方案**:分离存储Lock对象和时间戳
|
||||
```python
|
||||
# 分离存储
|
||||
self._message_locks: Dict[str, asyncio.Lock] = {} # 存储Lock对象
|
||||
self._message_lock_times: Dict[str, datetime] = {} # 存储Lock的创建时间
|
||||
|
||||
# 清理时使用时间戳字典
|
||||
expired_locks = [
|
||||
lock_key for lock_key, timestamp in self._message_lock_times.items()
|
||||
if (current_time - timestamp).total_seconds() > self._lock_ttl
|
||||
]
|
||||
```
|
||||
|
||||
## 可能存在的问题
|
||||
|
||||
### 1. API响应混淆
|
||||
**问题描述**:当多个前端同时响应时,`_pending_requests`是全局共享的,可能导致响应匹配错误
|
||||
|
||||
**当前解决方案**:使用echo_id匹配响应,任何前端的响应都会被正确匹配
|
||||
```python
|
||||
echo_id = data.get("echo")
|
||||
if echo_id and echo_id in self._pending_requests:
|
||||
future = self._pending_requests.pop(echo_id)
|
||||
if not future.done():
|
||||
future.set_result(data)
|
||||
```
|
||||
|
||||
**潜在风险**:如果多个前端同时响应同一个API请求,只有第一个响应会被处理
|
||||
|
||||
**建议优化**:可以考虑在API调用时指定客户端ID,确保响应来自正确的客户端
|
||||
|
||||
### 2. 负载均衡不准确
|
||||
**问题描述**:负载计数可能不准确,因为多个前端可能同时处理相同的消息
|
||||
|
||||
**当前解决方案**:负载计数只是参考,系统会优先选择健康的客户端
|
||||
|
||||
**建议优化**:可以考虑使用更复杂的负载均衡策略,如加权轮询
|
||||
|
||||
### 3. Bot实例状态
|
||||
**问题描述**:每个前端有独立的Bot实例,可能导致状态不一致
|
||||
|
||||
**当前解决方案**:Bot实例是无状态的,只依赖于WS实例和self_id
|
||||
|
||||
**建议优化**:如果需要共享状态,可以使用Redis等外部存储
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 部署建议
|
||||
- 部署2-3个前端实例进行负载均衡
|
||||
- 确保前端实例之间的网络连接稳定
|
||||
- 定期检查前端连接状态
|
||||
|
||||
### 2. 监控建议
|
||||
- 关注重复事件日志,排查网络问题
|
||||
- 监控客户端健康状态
|
||||
- 监控API调用成功率
|
||||
|
||||
### 3. 调试建议
|
||||
- 使用`get_healthy_clients()`查看健康客户端
|
||||
- 使用`get_client_with_least_load()`查看负载最低的客户端
|
||||
- 查看日志了解API调用情况
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. API响应过滤
|
||||
可以在API调用时记录客户端ID,确保响应来自正确的客户端:
|
||||
```python
|
||||
# 记录API请求的客户端ID
|
||||
self._api_requests[echo_id] = {
|
||||
'client_id': client_id,
|
||||
'timestamp': datetime.now()
|
||||
}
|
||||
|
||||
# 处理响应时验证客户端ID
|
||||
if echo_id in self._api_requests:
|
||||
request_info = self._api_requests[echo_id]
|
||||
if request_info['client_id'] == client_id:
|
||||
# 处理响应
|
||||
```
|
||||
|
||||
### 2. 负载均衡策略
|
||||
可以考虑使用更复杂的负载均衡策略:
|
||||
- 加权轮询:根据客户端性能分配权重
|
||||
- 最少连接:选择连接数最少的客户端
|
||||
- 响应时间:选择响应时间最短的客户端
|
||||
|
||||
### 3. 缓存优化
|
||||
可以考虑使用Redis缓存Bot实例的状态:
|
||||
```python
|
||||
# 缓存Bot实例的状态
|
||||
await redis_manager.set(f"neobot:bot:{client_id}:status", status_data, ex=300)
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题:消息重复处理
|
||||
**原因**:网络延迟导致前端重复发送
|
||||
|
||||
**解决**:系统已自动处理,检查事件ID是否正确设置
|
||||
|
||||
### 问题:API调用超时
|
||||
**原因**:选择的客户端不健康或网络问题
|
||||
|
||||
**解决**:系统会自动切换到其他健康客户端
|
||||
|
||||
### 问题:所有客户端都不健康
|
||||
**原因**:前端断开连接或网络问题
|
||||
|
||||
**解决**:检查前端连接状态和网络连接
|
||||
|
||||
### 问题:Bot实例初始化失败
|
||||
**原因**:WS实例缺失或self_id未设置
|
||||
|
||||
**解决**:确保在事件处理时正确设置self_id
|
||||
@@ -20,6 +20,7 @@ from ..utils.error_codes import ErrorCode, create_error_response
|
||||
from .command_manager import matcher
|
||||
from models.events.factory import EventFactory
|
||||
from .redis_manager import redis_manager
|
||||
from ..bot import Bot
|
||||
|
||||
|
||||
class ReverseWSManager:
|
||||
@@ -48,11 +49,15 @@ class ReverseWSManager:
|
||||
self._processed_events: Dict[str, datetime] = {} # 已处理的事件ID和时间
|
||||
self._event_ttl = 60 # 事件ID保留时间(秒)
|
||||
self._message_locks: Dict[str, asyncio.Lock] = {} # 消息处理锁
|
||||
self._message_lock_times: Dict[str, datetime] = {} # 消息锁创建时间
|
||||
self._lock_ttl = 300 # 锁保留时间(秒)
|
||||
|
||||
# 启动清理任务
|
||||
self._cleanup_task = None
|
||||
|
||||
# Bot实例字典(每个前端独立的Bot实例)
|
||||
self.bots: Dict[str, Bot] = {}
|
||||
|
||||
async def start(self, host: str = "0.0.0.0", port: int = 3002) -> None:
|
||||
"""
|
||||
启动反向 WebSocket 服务端。
|
||||
@@ -162,11 +167,14 @@ class ReverseWSManager:
|
||||
|
||||
# 清理过期的消息锁
|
||||
expired_locks = [
|
||||
lock_key for lock_key, timestamp in self._message_locks.items()
|
||||
lock_key for lock_key, timestamp in self._message_lock_times.items()
|
||||
if (current_time - timestamp).total_seconds() > self._lock_ttl
|
||||
]
|
||||
for lock_key in expired_locks:
|
||||
if lock_key in self._message_locks:
|
||||
del self._message_locks[lock_key]
|
||||
if lock_key in self._message_lock_times:
|
||||
del self._message_lock_times[lock_key]
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
@@ -184,6 +192,14 @@ class ReverseWSManager:
|
||||
del self.clients[client_id]
|
||||
if client_id in self.client_self_ids:
|
||||
del self.client_self_ids[client_id]
|
||||
if client_id in self._client_load:
|
||||
del self._client_load[client_id]
|
||||
if client_id in self._client_health:
|
||||
del self._client_health[client_id]
|
||||
if client_id in self.bots:
|
||||
del self.bots[client_id]
|
||||
|
||||
self.logger.info(f"客户端已断开并清理: {client_id}")
|
||||
|
||||
async def _on_event(self, client_id: str, event_data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
@@ -199,7 +215,16 @@ class ReverseWSManager:
|
||||
if hasattr(event, 'self_id'):
|
||||
self.client_self_ids[client_id] = event.self_id
|
||||
|
||||
event.bot = None
|
||||
# 为事件注入Bot实例
|
||||
from ..ws import WS
|
||||
|
||||
# 为每个前端创建独立的Bot实例
|
||||
if client_id not in self.bots:
|
||||
temp_ws = WS()
|
||||
temp_ws.self_id = event.self_id if hasattr(event, 'self_id') else 0
|
||||
self.bots[client_id] = Bot(temp_ws)
|
||||
|
||||
event.bot = self.bots[client_id]
|
||||
|
||||
# 记录客户端健康状态
|
||||
self._client_health[client_id] = datetime.now()
|
||||
@@ -392,6 +417,7 @@ class ReverseWSManager:
|
||||
"""
|
||||
if key not in self._message_locks:
|
||||
self._message_locks[key] = asyncio.Lock()
|
||||
self._message_lock_times[key] = datetime.now()
|
||||
return self._message_locks[key]
|
||||
|
||||
def _update_client_load(self, client_id: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user