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.
11 KiB
11 KiB
异步任务处理
文档版本:v1.0
最后更新:2025-01-27
目录
异步任务概述
为什么需要异步任务
Jointo平台中有大量耗时操作:
- AI 生成:图片生成(10-30秒)、视频生成(1-5分钟)
- 视频导出:视频合成、转码(5-30分钟)
- 文件处理:大文件上传、转换
- 批量操作:批量导入、批量处理
这些操作如果同步执行会:
- 阻塞 API 响应
- 占用服务器资源
- 影响用户体验
异步任务架构
┌─────────────┐
│ API 服务 │
│ │
│ 创建任务 │
└──────┬──────┘
│
▼
┌─────────────┐
│ RabbitMQ │ 消息队列
│ │
└──────┬──────┘
│
▼
┌─────────────┐
│ Celery │ 任务调度
│ Worker │
│ │
│ 执行任务 │
└──────┬──────┘
│
▼
┌─────────────┐
│ Redis │ 结果存储
│ │
└─────────────┘
Celery 配置
安装依赖
pip install celery[redis]
pip install celery[amqp] # 如果使用 RabbitMQ
Celery 应用配置
# app/tasks/celery_app.py
from celery import Celery
from app.config import settings
celery_app = Celery(
"jointo",
broker=settings.CELERY_BROKER_URL,
backend=settings.CELERY_RESULT_BACKEND,
include=['app.tasks.ai_tasks', 'app.tasks.export_tasks']
)
# Celery 配置
celery_app.conf.update(
# 序列化
task_serializer='json',
accept_content=['json'],
result_serializer='json',
# 时区
timezone='UTC',
enable_utc=True,
# 任务配置
task_track_started=True,
task_time_limit=3600, # 1 小时
task_soft_time_limit=3300, # 55 分钟
# 结果过期时间
result_expires=3600,
# 任务路由
task_routes={
'app.tasks.ai_tasks.*': {'queue': 'ai'},
'app.tasks.export_tasks.*': {'queue': 'export'},
},
# 并发配置
worker_prefetch_multiplier=1,
worker_max_tasks_per_child=1000,
)
环境配置
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# Celery 配置
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/0"
# 或使用 RabbitMQ
# CELERY_BROKER_URL: str = "amqp://guest:guest@localhost:5672//"
class Config:
env_file = ".env"
settings = Settings()
AI 生成任务
图片生成任务
# app/tasks/ai_tasks.py
from app.tasks.celery_app import celery_app
from app.services.ai_service import AIService
from app.repositories.ai_job_repository import AIJobRepository
from app.core.database import SessionLocal
from app.core.storage import StorageService
import httpx
@celery_app.task(bind=True, max_retries=3)
def generate_image_task(self, prompt: str, model: str, **kwargs):
"""生成图片任务"""
db = SessionLocal()
try:
job_id = self.request.id
# 更新任务状态
job_repo = AIJobRepository(db)
job_repo.update_status(job_id, 'processing', progress=10)
# 调用 AI API
async with httpx.AsyncClient() as client:
response = await client.post(
f"{settings.AI_API_URL}/generate-image",
json={"prompt": prompt, "model": model, **kwargs},
headers={"Authorization": f"Bearer {settings.AI_API_KEY}"},
timeout=300
)
result = response.json()
# 更新进度
job_repo.update_status(job_id, 'processing', progress=50)
# 下载并上传到对象存储
image_url = result['image_url']
storage = StorageService()
# 下载图片
async with httpx.AsyncClient() as client:
image_response = await client.get(image_url)
image_data = image_response.content
# 上传到对象存储
object_name = f"ai-generated/images/{job_id}.png"
final_url = await storage.upload_bytes(
image_data,
object_name,
content_type="image/png"
)
# 更新任务完成
job_repo.update_status(
job_id,
'completed',
progress=100,
output_data={'image_url': final_url}
)
return {'image_url': final_url}
except Exception as exc:
job_repo.update_status(
job_id,
'failed',
error_message=str(exc)
)
# 重试:60秒后重试
raise self.retry(exc=exc, countdown=60)
finally:
db.close()
视频生成任务
@celery_app.task(bind=True, max_retries=3)
def generate_video_task(
self,
video_type: str,
prompt: Optional[str] = None,
image_url: Optional[str] = None,
**kwargs
):
"""生成视频任务"""
db = SessionLocal()
try:
job_id = self.request.id
job_repo = AIJobRepository(db)
# 更新任务状态
job_repo.update_status(job_id, 'processing', progress=10)
# 根据视频类型调用不同的 AI 服务
if video_type == 'text2video':
result = await _generate_text2video(prompt, **kwargs)
elif video_type == 'img2video':
result = await _generate_img2video(image_url, **kwargs)
else:
raise ValueError(f"Unsupported video type: {video_type}")
# 更新进度
job_repo.update_status(job_id, 'processing', progress=50)
# 下载并上传视频
video_url = result['video_url']
storage = StorageService()
async with httpx.AsyncClient() as client:
video_response = await client.get(video_url)
video_data = video_response.content
object_name = f"ai-generated/videos/{job_id}.mp4"
final_url = await storage.upload_bytes(
video_data,
object_name,
content_type="video/mp4"
)
# 更新任务完成
job_repo.update_status(
job_id,
'completed',
progress=100,
output_data={'video_url': final_url}
)
return {'video_url': final_url}
except Exception as exc:
job_repo.update_status(job_id, 'failed', error_message=str(exc))
raise self.retry(exc=exc, countdown=120)
finally:
db.close()
视频导出任务
视频合成任务
# app/tasks/export_tasks.py
from app.tasks.celery_app import celery_app
from app.services.export_service import ExportService
from app.repositories.export_job_repository import ExportJobRepository
from app.core.database import SessionLocal
import ffmpeg
@celery_app.task(bind=True, max_retries=2)
def export_video_task(
self,
project_id: str,
format: str,
quality: str,
**kwargs
):
"""导出视频任务"""
db = SessionLocal()
try:
job_id = self.request.id
export_repo = ExportJobRepository(db)
# 更新任务状态
export_repo.update_status(job_id, 'processing', progress=0)
# 获取项目所有素材
export_service = ExportService()
materials = await export_service.get_project_materials(project_id)
# 更新进度
export_repo.update_status(job_id, 'processing', progress=20)
# 使用 FFmpeg 合成视频
output_path = f"/tmp/{job_id}.{format}"
# 构建 FFmpeg 命令
inputs = []
for material in materials:
inputs.append(ffmpeg.input(material['url']))
# 合成视频
stream = ffmpeg.concat(*inputs, v=1, a=1)
stream = ffmpeg.output(
stream,
output_path,
vcodec='libx264',
acodec='aac',
**_get_quality_params(quality)
)
# 执行 FFmpeg
ffmpeg.run(stream, capture_stdout=True, capture_stderr=True)
# 更新进度
export_repo.update_status(job_id, 'processing', progress=80)
# 上传到对象存储
storage = StorageService()
object_name = f"exports/{project_id}/{job_id}.{format}"
output_url = await storage.upload_file(
output_path,
object_name,
content_type=f"video/{format}"
)
# 清理临时文件
os.remove(output_path)
# 更新任务完成
export_repo.update_status(
job_id,
'completed',
progress=100,
output_url=output_url
)
return {'output_url': output_url}
except Exception as exc:
export_repo.update_status(job_id, 'failed', error_message=str(exc))
# 5 分钟后重试
raise self.retry(exc=exc, countdown=300)
finally:
db.close()
def _get_quality_params(quality: str) -> dict:
"""获取质量参数"""
quality_map = {
'low': {'crf': 28, 'preset': 'fast'},
'medium': {'crf': 23, 'preset': 'medium'},
'high': {'crf': 18, 'preset': 'slow'}
}
return quality_map.get(quality, quality_map['medium'])
任务监控
启动 Celery Worker
# 启动 AI 任务队列
celery -A app.tasks.celery_app worker -Q ai --loglevel=info
# 启动导出任务队列
celery -A app.tasks.celery_app worker -Q export --loglevel=info
# 启动所有队列
celery -A app.tasks.celery_app worker --loglevel=info
启动 Celery Beat(定时任务)
celery -A app.tasks.celery_app beat --loglevel=info
使用 Flower 监控
# 安装 Flower
pip install flower
# 启动 Flower
celery -A app.tasks.celery_app flower --port=5555
访问 http://localhost:5555 查看任务监控面板。
查询任务状态
# app/services/ai_service.py
async def get_job_status(self, job_id: str) -> Dict[str, Any]:
"""查询任务状态"""
from app.tasks.celery_app import celery_app
task = celery_app.AsyncResult(job_id)
return {
'job_id': job_id,
'status': task.state,
'progress': task.info.get('progress', 0) if task.info else 0,
'result': task.result if task.ready() else None,
'error': str(task.info) if task.failed() else None
}
相关文档
文档版本:v1.0
最后更新:2025-01-27