zlwu 1 rok temu
rodzic
commit
8afb2923d5

+ 2 - 2
backend/config.py

@@ -38,8 +38,8 @@ class Settings(BaseSettings):
     
     # 连接池配置
     CONNECTION_POOL_SIZE: int = 15  # 增加连接池大小
-    CONNECTION_MAX_IDLE_TIME: int = 180  # 减少最大空闲时间(秒)
-    CONNECTION_HEALTH_CHECK_INTERVAL: int = 30  # 减少健康检查间隔(秒)
+    CONNECTION_MAX_IDLE_TIME: int = 25  # 最大空闲时间(秒),必须小于豆包超时时间
+    CONNECTION_HEALTH_CHECK_INTERVAL: int = 20  # 健康检查间隔(秒)
     
     # 音频配置
     AUDIO_SAMPLE_RATE: int = 16000

+ 51 - 2
backend/connection_pool.py

@@ -166,8 +166,15 @@ class DoubaoConnectionPool:
                 await self.idle_connections.put(connection)
                 logger.debug(f"连接已归还,当前空闲连接: {self.idle_connections.qsize()}")
             else:
-                # 连接不健康,重新创建
-                logger.warning("连接不健康,重新创建...")
+                # 连接不健康,先关闭旧连接
+                reason = "致命错误" if hasattr(connection, 'has_fatal_error') and connection.has_fatal_error else "健康检查失败"
+                logger.warning(f"连接不健康 ({reason}),销毁并重新创建...")
+                try:
+                    await connection.close()
+                except:
+                    pass
+                
+                # 重新创建连接
                 new_conn = await self._create_connection(-1)
                 if new_conn:
                     await self.idle_connections.put(new_conn)
@@ -180,6 +187,11 @@ class DoubaoConnectionPool:
     async def _is_connection_healthy(self, connection) -> bool:
         """检查连接健康状态"""
         try:
+            # 检查是否有致命错误
+            if hasattr(connection, 'has_fatal_error') and connection.has_fatal_error:
+                logger.warning(f"连接有致命错误: {connection.session_id}")
+                return False
+            
             # 检查连接是否还活着
             if not connection or not connection.is_connected():
                 return False
@@ -214,6 +226,43 @@ class DoubaoConnectionPool:
                     f"总计: {idle_count + active_count}/{self.pool_size}"
                 )
                 
+                # 🔧 主动清理空闲连接池中的失效连接
+                unhealthy_count = 0
+                temp_connections = []
+                
+                # 取出所有空闲连接检查
+                while not self.idle_connections.empty():
+                    try:
+                        conn = await asyncio.wait_for(self.idle_connections.get(), timeout=1)
+                        temp_connections.append(conn)
+                    except asyncio.TimeoutError:
+                        break
+                
+                # 检查并重新放回健康的连接
+                for conn in temp_connections:
+                    if await self._is_connection_healthy(conn):
+                        await self.idle_connections.put(conn)
+                    else:
+                        unhealthy_count += 1
+                        logger.warning(f"清理不健康的空闲连接: {conn.session_id}")
+                        try:
+                            await conn.close()
+                        except:
+                            pass
+                        
+                        # 创建新连接补充
+                        new_conn = await self._create_connection(-1)
+                        if new_conn:
+                            await self.idle_connections.put(new_conn)
+                            self.connection_health[id(new_conn)] = {
+                                'created_at': time.time(),
+                                'last_used': time.time(),
+                                'request_count': 0
+                            }
+                
+                if unhealthy_count > 0:
+                    logger.info(f"🔧 清理了 {unhealthy_count} 个不健康的连接并重新创建")
+                
             except Exception as e:
                 logger.error(f"健康检查循环错误: {e}")
     

+ 13 - 1
backend/doubao_api/realtime_dialog_client.py

@@ -37,6 +37,7 @@ class RealtimeDialogClient:
         self.ws = None
         self.logid = ""
         self.connected = False
+        self.has_fatal_error = False  # 标记是否遇到致命错误(如超时)
         
         # 配置请求头
         self.headers = {
@@ -158,6 +159,9 @@ class RealtimeDialogClient:
     
     def is_connected(self) -> bool:
         """检查连接状态"""
+        # 如果遇到致命错误,连接不再可用
+        if self.has_fatal_error:
+            return False
         return self.connected and self.ws and self.ws.open
     
     async def send_audio(self, audio_data: bytes):
@@ -290,7 +294,15 @@ class RealtimeDialogClient:
                 if isinstance(payload_msg, dict):
                     # 检查是否是错误消息
                     if 'error' in payload_msg:
-                        logger.error(f"❌ 豆包错误 (Event {event}): {payload_msg['error']}")
+                        error_msg = payload_msg['error']
+                        logger.error(f"❌ 豆包错误 (Event {event}): {error_msg}")
+                        
+                        # 检查是否是致命错误(如超时)
+                        if 'IdleTimeoutError' in error_msg or 'TimeoutError' in error_msg:
+                            logger.error(f"🚫 检测到致命错误,标记连接为不可用: {self.session_id}")
+                            self.has_fatal_error = True
+                            self.connected = False
+                            return False  # 停止接收
                     else:
                         logger.debug(f"收到豆包事件 {event}: {payload_msg}")
                 else:

+ 9 - 0
backend/websocket_handler.py

@@ -155,6 +155,15 @@ class StreamingSession:
                     continue
                 
                 try:
+                    # 检查豆包连接是否健康
+                    if not self.doubao_client.is_connected():
+                        logger.error(f"豆包连接已失效 (has_fatal_error={self.doubao_client.has_fatal_error}),停止处理")
+                        await self.send_error(
+                            ErrorCode.INTERNAL_ERROR,
+                            "语音服务连接异常,请刷新页面重试"
+                        )
+                        break
+                    
                     # 接收豆包音频数据,设置响应超时
                     audio_data = await asyncio.wait_for(
                         self.doubao_client.receive_audio(), 

+ 164 - 0
豆包超时错误修复.md

@@ -0,0 +1,164 @@
+# 豆包API超时错误修复
+
+## 问题现象
+
+1. **前端无法收到AI回复**:部分会话开始录音后,无法收到豆包返回的音频
+2. **后端错误日志**:
+   ```
+   ❌ 豆包错误 (Event None): sami error: codes=52000042, 
+   desc=DialogAudioIdleTimeoutError, please use audio_file mod
+   ```
+3. **连接池被占用**:失效的连接占用连接池,导致活跃连接数持续增加
+
+## 根本原因
+
+### 1. 豆包API空闲超时机制
+- 豆包API对话有空闲超时限制(约30-60秒无音频数据)
+- 超时后连接会收到 `DialogAudioIdleTimeoutError` 错误
+- 此时连接已失效,不会再返回任何音频数据
+
+### 2. 连接池未清理失效连接
+- 连接池从连接中获取一个已超时的连接
+- 旧代码只记录错误,继续使用失效连接
+- 失效连接不会被释放,占用连接池资源
+
+### 3. 健康检查不完善
+- 只检查 `is_connected()` 状态(WebSocket层面)
+- 未检查豆包API层面的错误状态
+
+## 修复方案
+
+### 1. 豆包客户端 (`doubao_api/realtime_dialog_client.py`)
+
+**添加致命错误标志:**
+```python
+self.has_fatal_error = False  # 标记是否遇到致命错误
+```
+
+**更新连接检查:**
+```python
+def is_connected(self) -> bool:
+    if self.has_fatal_error:
+        return False
+    return self.connected and self.ws and self.ws.open
+```
+
+**捕获超时错误:**
+```python
+if 'IdleTimeoutError' in error_msg or 'TimeoutError' in error_msg:
+    logger.error(f"🚫 检测到致命错误,标记连接为不可用")
+    self.has_fatal_error = True
+    self.connected = False
+    return False  # 停止接收
+```
+
+### 2. WebSocket处理器 (`websocket_handler.py`)
+
+**主动检测连接失效:**
+```python
+# 检查豆包连接是否健康
+if not self.doubao_client.is_connected():
+    logger.error(f"豆包连接已失效,停止处理")
+    await self.send_error(
+        ErrorCode.INTERNAL_ERROR,
+        "语音服务连接异常,请刷新页面重试"
+    )
+    break
+```
+
+### 3. 连接池 (`connection_pool.py`)
+
+**增强健康检查:**
+```python
+async def _is_connection_healthy(self, connection) -> bool:
+    # 检查是否有致命错误
+    if hasattr(connection, 'has_fatal_error') and connection.has_fatal_error:
+        logger.warning(f"连接有致命错误: {connection.session_id}")
+        return False
+    
+    # 检查连接是否还活着
+    if not connection or not connection.is_connected():
+        return False
+    ...
+```
+
+**主动清理失效连接:**
+```python
+# 在定期健康检查中
+for conn in temp_connections:
+    if await self._is_connection_healthy(conn):
+        await self.idle_connections.put(conn)
+    else:
+        # 销毁失效连接并创建新的
+        await conn.close()
+        new_conn = await self._create_connection(-1)
+        await self.idle_connections.put(new_conn)
+```
+
+**释放时销毁失效连接:**
+```python
+if await self._is_connection_healthy(connection):
+    await self.idle_connections.put(connection)
+else:
+    # 先关闭旧连接
+    await connection.close()
+    # 创建新连接替换
+    new_conn = await self._create_connection(-1)
+    await self.idle_connections.put(new_conn)
+```
+
+## 效果
+
+1. ✅ 自动检测并标记失效的豆包连接
+2. ✅ 失效连接不会被重用
+3. ✅ 定期健康检查主动清理失效连接
+4. ✅ 失效连接被销毁后自动创建新连接补充
+5. ✅ 前端收到错误提示,可以刷新页面重连
+
+## 监控日志
+
+### 正常情况:
+```
+连接池健康检查 - 空闲: 7, 活跃: 3, 总计: 10/10
+```
+
+### 检测到失效连接:
+```
+🚫 检测到致命错误,标记连接为不可用: session_xxx
+连接有致命错误: session_xxx
+清理不健康的空闲连接: session_xxx
+🔧 清理了 2 个不健康的连接并重新创建
+```
+
+### 使用中检测到失效:
+```
+豆包连接已失效 (has_fatal_error=True),停止处理
+前端收到错误:语音服务连接异常,请刷新页面重试
+```
+
+## 配置优化
+
+已调整 `backend/config.py` 参数:
+```python
+CONNECTION_MAX_IDLE_TIME: int = 25  # 从180秒降到25秒
+CONNECTION_HEALTH_CHECK_INTERVAL: int = 20  # 从30秒降到20秒
+```
+
+**原因**:豆包API的实际超时时间约30-60秒,将空闲时间设为25秒可以在豆包超时前主动清理连接。
+
+## 测试验证
+
+1. **重启后端服务**:让新配置生效
+   ```bash
+   cd backend && ./start.sh
+   ```
+
+2. **观察健康检查日志**:
+   - 每20秒会打印健康检查报告
+   - 如果有失效连接,会自动清理并重新创建
+
+3. **测试长时间空闲**:
+   - 打开页面后等待30秒不操作
+   - 然后开始录音,应该正常工作
+   - 如果出错,前端会提示"语音服务连接异常,请刷新页面重试"
+