You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
6.2 KiB
6.2 KiB
修复 Celery Worker Redis Event Loop 冲突问题
日期: 2026-02-11
类型: Bug 修复
影响范围: 后端 - Celery 任务执行
问题描述
Celery Worker 在执行 AI 任务时遇到 RuntimeError: Event Loop is closed 错误,导致任务失败。
错误信息
RuntimeError: Event loop is closed
File "/app/app/tasks/ai_tasks.py", line 905, in _execute
provider = await AIProviderFactory.create_provider(model, kwargs.get('config'))
File "/app/app/services/ai_providers/factory.py", line 150, in create_provider
raise ValueError
ValueError: 模型 gpt-4o-mini 不存在或未激活
根本原因
- Celery 使用 ForkPoolWorker:每个任务在独立的子进程中运行
- Event Loop 不共享:主进程在
worker_ready信号中创建的 Event Loop 无法在子进程中使用 - Redis 连接失效:全局 Redis 连接(通过
get_redis()获取)在子进程中不可用 - 异步操作失败:尝试在已关闭的 Event Loop 中执行异步 Redis 操作
技术细节
# ❌ 错误:使用全局 Redis 连接(在子进程中失效)
async def get_cached_provider(cls, model_name: str):
redis = await get_redis() # 全局单例,Event Loop 已关闭
cached_data = await redis.get(cls.CACHE_KEY) # RuntimeError
修复内容
修改文件
文件: server/app/services/ai_providers/factory.py
方法: AIProviderFactory.get_cached_provider
修复方案
在每次调用时创建新的 Redis 连接,避免使用全局单例:
# ✅ 正确:在 Celery 任务中创建临时 Redis 连接
@classmethod
async def get_cached_provider(cls, model_name: str) -> Optional[int]:
"""从 Redis 缓存获取 Provider ID(支持 Celery 任务)"""
try:
import redis.asyncio as redis
from app.core.config import get_settings
settings = get_settings()
# 创建临时 Redis 客户端(不使用全局单例)
redis_client = await redis.from_url(
settings.REDIS_URL,
encoding="utf-8",
decode_responses=True
)
try:
cached_data = await redis_client.get(cls.CACHE_KEY)
if not cached_data:
logger.warning("⚠️ Redis 缓存为空,可能需要初始化")
return None
cache_dict = json.loads(cached_data)
return cache_dict.get(model_name)
finally:
# 关闭临时连接
await redis_client.close()
except Exception as e:
logger.error("❌ 从 Redis 读取缓存失败: %s", e, exc_info=True)
return None
关键改进
- 独立连接:每次调用创建新的 Redis 连接,避免 Event Loop 冲突
- 自动清理:使用
try...finally确保连接正确关闭 - 错误处理:捕获异常并返回 None,允许任务重试
技术背景
Celery + asyncio 的挑战
Celery 默认使用 prefork 模式(ForkPoolWorker),每个任务在独立的子进程中运行:
主进程 (MainProcess)
├─ worker_ready 信号 → 创建 Event Loop A
└─ 子进程 (ForkPoolWorker-1)
└─ 任务执行 → 尝试使用 Event Loop A(已关闭)❌
为什么全局 Redis 连接失败?
- Event Loop 绑定:asyncio Redis 连接绑定到创建它的 Event Loop
- 进程隔离:子进程无法访问父进程的 Event Loop
- 连接状态:Redis 连接在子进程中处于"已关闭"状态
解决方案对比
| 方案 | 优点 | 缺点 | 选择 |
|---|---|---|---|
| 临时连接 | 简单、可靠、无状态 | 每次创建连接有开销 | ✅ 采用 |
| 同步 Redis | 避免 Event Loop 问题 | 需要重构大量代码 | ❌ |
| 连接池 | 性能最优 | 复杂度高,需要管理生命周期 | ❌ |
| Solo Worker | 避免进程问题 | 性能差,无法并发 | ❌ |
性能影响
连接开销
- 每次任务创建一个 Redis 连接
- 连接时间:~10ms(本地网络)
- 相比任务总时间(通常 >1s)可忽略
优化建议
如果未来需要优化性能,可以考虑:
- 连接池:在每个 Worker 进程中维护独立的连接池
- 缓存预热:在 Worker 启动时将缓存加载到内存
- 同步 Redis:使用
redis-py(同步版本)避免 Event Loop 问题
测试验证
测试步骤
-
重启 Celery Worker:
docker restart jointo-server-celery-ai -
触发剧本解析任务:
POST /api/v1/screenplays/{screenplay_id}/parse -
检查日志:
docker logs -f jointo-server-celery-ai | grep "Provider"
预期结果
[INFO] 创建 AIHubMix Provider: model=gpt-4o-mini
[INFO] 剧本解析任务已提交: job_id=xxx
错误日志(修复前)
[ERROR] 从 Redis 读取缓存失败: Event loop is closed
[ERROR] 剧本解析任务失败: 模型 gpt-4o-mini 不存在或未激活
相关问题
为什么不在 worker_ready 中修复?
worker_ready 信号在主进程中执行,无法为子进程创建 Event Loop。Celery 的 prefork 模式要求每个子进程独立管理资源。
其他地方是否有类似问题?
目前只有 AIProviderFactory.get_cached_provider 受影响,因为:
- 其他 Redis 操作在 FastAPI 请求中执行(有独立的 Event Loop)
- Celery 任务中的数据库操作使用
get_async_session()(每次创建新连接)
是否需要修改其他 Celery 任务?
不需要。其他任务(如导出任务)不使用 Redis 缓存,或者使用数据库连接(已正确处理)。
相关文件
server/app/services/ai_providers/factory.py- 修复 Redis 连接问题server/app/core/celery_app.py- Celery Worker 配置server/app/core/cache.py- Redis 缓存服务server/app/tasks/ai_tasks.py- AI 任务执行