引言
在开发 Web 应用时,我们经常遇到需要长时间执行的任务:发送邮件、处理大文件、生成报表、训练机器学习模型等。如果这些任务在 HTTP 请求中同步执行,会导致用户等待很久,甚至超时。
Celery 就是解决这个问题的利器------一个强大的分布式任务队列系统。本文将深入浅出地介绍 Celery 的核心概念和实战应用,特别是如何实现文件下载进度实时追踪这个常见需求。
一、Celery 是什么?
1.1 核心架构
Celery 由三个核心组件组成:
scss
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Main 程序 │ ───> │ Message Broker│ ───> │Celery Worker │
│ (Flask/Django)│ │ (Redis) │ │ (执行任务) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
│ ┌──────────────┐ │
└─────────────>│Result Backend│<────────────┘
│ (Redis) │
└──────────────┘
- Main 程序: 发送任务的客户端(你的 Web 应用)
- Message Broker: 消息队列,存储待执行的任务(常用 Redis)
- Celery Worker: 执行任务的工作进程
- Result Backend: 存储任务结果和状态(可选,常用 Redis)
1.2 快速开始
ini
# 安装
pip install celery redis
# 定义任务 (tasks.py)
from celery import Celery
app = Celery('myapp',
broker='redis://localhost:6379/0',
backend='redis://localhost:6379/0')
@app.task
def add(x, y):
return x + y
# 启动 Worker
# celery -A tasks worker --loglevel=info
# 调用任务 (main.py)
result = add.delay(4, 6)
print(result.get()) # 输出: 10
二、task_id:任务的唯一标识符
2.1 什么是 task_id?
task_id 是每个任务的唯一标识符,类似于快递单号。通过它,Main 程序和 Worker 都能追踪同一个任务。
2.2 生成与传递机制
ini
# Main 程序端
result = add.apply_async(args=[10, 20])
task_id = result.id # Celery 自动生成 UUID
print(f"Task ID: {task_id}") # 3c8a2c9a-7b5d-4e3f...
关键流程:
ini
Main 程序 Redis (Broker) Worker
│ │ │
│ ① 生成 task_id="abc-123" │ │
│ ② 发送消息 │ │
│ {id:"abc-123", args:[]} │ │
├────────────────────────────>│ │
│ │ ③ Worker 拉取消息 │
│ ├───────────────────────>│
│ │ 解析 task_id │
│ │ │
│ │ Redis (Backend) │
│ ④ 查询状态 │ │ │
│ 用 task_id 查询 │ │ ⑤ 写入状态 │
│<────────────────────────────┼─────────┼──────────────│
重点:task_id 在 Main 程序生成,通过消息传递给 Worker,双方都用它作为 Redis Backend 的键名来读写状态。
2.3 自定义 task_id
ini
# 自定义 task_id(业务相关)
custom_id = f"download_{user_id}_{file_id}_{uuid.uuid4().hex[:8]}"
result = download_file.apply_async(
args=['https://example.com/file.zip', '/tmp/file.zip'],
task_id=custom_id # 指定自定义 ID
)
# 后续可以随时查询
from celery.result import AsyncResult
status = AsyncResult(custom_id, app=app)
print(status.state)
好处:
- 便于数据库存储和查询
- 实现幂等性(避免重复任务)
- 便于日志追踪和调试
三、AsyncResult:任务结果的代理对象
3.1 什么是 AsyncResult?
AsyncResult 是一个代表任务结果的对象,就像"提货单"------你可以随时用它查询任务状态。
ini
# 方式 1: 任务调用时直接获得
result = add.delay(4, 6)
print(type(result)) # <class 'celery.result.AsyncResult'>
# 方式 2: 通过 task_id 重新创建
result = AsyncResult('task-id-from-database', app=app)
3.2 核心属性和方法
bash
result = long_task.delay(100)
# 任务标识
print(result.id) # task_id
# 任务状态
print(result.state) # PENDING, STARTED, SUCCESS, FAILURE...
# 检查状态
print(result.ready()) # 是否完成 (True/False)
print(result.successful()) # 是否成功
print(result.failed()) # 是否失败
# 获取结果
value = result.get(timeout=10) # 阻塞等待,最多 10 秒
# 非阻塞获取
if result.ready():
print(result.result) # 获取结果或异常
3.3 访问任务元数据
python
@app.task(bind=True)
def task_with_progress(self, n):
for i in range(n):
self.update_state(
state='PROGRESS',
meta={'current': i, 'total': n}
)
return 'done'
result = task_with_progress.delay(10)
time.sleep(2)
print(result.info) # {'current': 5, 'total': 10}
注意:AsyncResult 本身不存储数据,它只是通过 task_id 去 Redis 查询数据的"查询器"。
四、delay() vs apply_async()
4.1 核心区别
ini
# delay() - 简化版本
result = task.delay(arg1, arg2, key=value)
# 等价于
result = task.apply_async(
args=[arg1, arg2],
kwargs={'key': value}
)
4.2 功能对比表
| 功能 | delay() | apply_async() |
|---|---|---|
| 传递任务参数 | ✅ | ✅ |
| 自定义 task_id | ❌ | ✅ |
| 延迟执行 | ❌ | ✅ |
| 定时执行 | ❌ | ✅ |
| 设置优先级 | ❌ | ✅ |
| 覆盖队列 | ❌ | ✅ |
| 任务链 | ❌ | ✅ |
4.3 使用建议
ini
# 简单场景 - 使用 delay()
send_email.delay("user@example.com", "Hello")
# 需要追踪或配置 - 使用 apply_async()
result = download_file.apply_async(
args=['https://example.com/file.zip'],
task_id=f"download_{user_id}_{timestamp}",
countdown=60, # 60 秒后执行
queue="download",
priority=9
)
原则 :默认用 delay() 保持简洁,需要高级功能时用 apply_async()。
五、bind=True:任务的自我感知
5.1 核心作用
bind=True 让任务函数接收 self 参数,代表任务实例本身,从而可以访问任务信息和控制任务行为。
python
# 不使用 bind=True
@app.task
def task1(x, y):
# ❌ 无法访问 task_id
# ❌ 无法更新状态
return x + y
# 使用 bind=True
@app.task(bind=True)
def task2(self, x, y):
# ✅ 访问 task_id
print(f"Task ID: {self.request.id}")
# ✅ 更新进度
self.update_state(
state='PROGRESS',
meta={'progress': 50}
)
return x + y
5.2 三大核心功能
1. 访问任务信息
python
@app.task(bind=True)
def inspect_task(self, data):
print(f"Task ID: {self.request.id}")
print(f"Task Name: {self.name}")
print(f"Args: {self.request.args}")
print(f"Retries: {self.request.retries}")
print(f"Hostname: {self.request.hostname}")
2. 更新任务状态
ini
@app.task(bind=True)
def download_file(self, url, save_path):
response = requests.get(url, stream=True)
total = int(response.headers.get('content-length', 0))
downloaded = 0
for chunk in response.iter_content(8192):
downloaded += len(chunk)
# 实时更新进度
self.update_state(
state='PROGRESS',
meta={
'downloaded': downloaded,
'total': total,
'percent': int(downloaded / total * 100)
}
)
3. 手动重试任务
python
@app.task(bind=True, max_retries=3)
def unreliable_api_call(self, url):
try:
response = requests.get(url, timeout=5)
return response.json()
except requests.RequestException as exc:
# 指数退避重试
countdown = 2 ** self.request.retries
raise self.retry(exc=exc, countdown=countdown)
5.3 使用场景
必须使用 bind=True:
- ✅ 需要实时更新任务进度
- ✅ 需要记录 task_id 到日志
- ✅ 需要实现自定义重试逻辑
- ✅ 文件下载、数据导入、长时间运行的任务
不需要 bind=True:
- ✅ 简单的"发送并忘记"任务
- ✅ 纯粹的计算任务
- ✅ 不需要追踪进度的后台任务
六、Queue:任务的分类与隔离
6.1 为什么需要队列?
想象一个场景:你的应用有两种任务
- 发送邮件:快速,每个任务 1 秒
- 视频转码:慢速,每个任务 5 分钟
不使用队列隔离:
ini
单一队列: [邮件1s] [邮件1s] [视频5m] [邮件1s]
↑
第3封邮件要等5分钟!
使用队列隔离:
less
Email队列: [邮件] [邮件] [邮件] ← Email Worker (快速)
Video队列: [视频] [视频] ← Video Worker (慢速)
结果: 邮件立即发送,视频慢慢处理,互不影响!
6.2 定义和使用队列
python
# 在装饰器中指定队列
@shared_task(queue="email")
def send_email(to, subject, body):
return f"Email sent to {to}"
@shared_task(queue="video")
def process_video(video_id):
return f"Video {video_id} processed"
@shared_task(queue="dataset")
def process_dataset(dataset_id):
return f"Dataset {dataset_id} processed"
# 调用时覆盖队列
result = send_email.apply_async(
args=["user@example.com", "Hello", "Welcome"],
queue="high-priority" # 覆盖默认的 "email" 队列
)
6.3 启动 Worker 监听队列
ini
# 只处理 email 队列
celery -A myapp worker -Q email --loglevel=info
# 处理多个队列
celery -A myapp worker -Q email,video,dataset --loglevel=info
# 多个 Worker 分别处理不同队列
# Terminal 1: 快速任务
celery -A myapp worker -Q email,api -n fast_worker
# Terminal 2: 慢速任务
celery -A myapp worker -Q video,dataset -n slow_worker
6.4 队列设计原则
按任务类型分类:
less
@shared_task(queue="email") # 邮件任务
@shared_task(queue="sms") # 短信任务
@shared_task(queue="report") # 报表任务
按执行速度分类:
less
@shared_task(queue="fast") # < 1秒
@shared_task(queue="normal") # 1-10秒
@shared_task(queue="slow") # > 10秒
按优先级分类:
less
@shared_task(queue="high-priority")
@shared_task(queue="normal")
@shared_task(queue="low-priority")
按资源需求分类:
less
@shared_task(queue="cpu-intensive") # CPU 密集型
@shared_task(queue="io-intensive") # IO 密集型
@shared_task(queue="memory-heavy") # 内存密集型
6.5 生产环境部署架构
css
# 服务器 1 (8核 CPU) - 快速任务
celery -A myapp worker -Q high-priority,api,email -c 4 -n server1
# 服务器 2 (16核 CPU) - 重型任务
celery -A myapp worker -Q video,dataset,ml -c 8 -n server2
# 服务器 3 (4核 CPU) - 普通任务
celery -A myapp worker -Q default,background -c 4 -n server3
参数说明:
-Q: 监听的队列列表-c: 并发数(Worker 进程数)-n: Worker 名称
七、实战案例:文件下载进度实时追踪
现在我们把所有知识点整合,实现一个完整的文件下载进度追踪系统。
7.1 Celery 任务定义
python
# celery_app.py
from celery import Celery
import requests
app = Celery(
'tasks',
broker='redis://localhost:6379/0',
backend='redis://localhost:6379/0'
)
@app.task(bind=True, queue="download")
def download_file(self, url, save_path):
"""
下载文件任务,实时更新进度
关键点:
1. bind=True - 获取 self 来更新状态
2. queue="download" - 使用专门的下载队列
"""
try:
response = requests.get(url, stream=True)
total_size = int(response.headers.get('content-length', 0))
downloaded_size = 0
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded_size += len(chunk)
# 计算进度百分比
progress = int((downloaded_size / total_size) * 100) if total_size > 0 else 0
# 关键:更新任务状态和进度信息
self.update_state(
state='PROGRESS',
meta={
'current': downloaded_size,
'total': total_size,
'progress': progress,
'status': f'Downloading... {progress}%'
}
)
# 任务完成
return {
'current': total_size,
'total': total_size,
'progress': 100,
'status': 'Download completed',
'file_path': save_path
}
except Exception as e:
# 任务失败
self.update_state(
state='FAILURE',
meta={
'status': 'Download failed',
'error': str(e)
}
)
raise
7.2 Flask API 接口
python
# flask_server.py
from flask import Flask, jsonify, request
from celery.result import AsyncResult
from celery_app import download_file, app as celery_app
import uuid
app = Flask(__name__)
@app.route('/start_download', methods=['POST'])
def start_download():
"""
启动下载任务
请求体:
{
"url": "https://example.com/file.zip",
"save_path": "/tmp/file.zip",
"user_id": "user_123"
}
"""
data = request.json
url = data.get('url')
save_path = data.get('save_path')
user_id = data.get('user_id')
if not url or not save_path:
return jsonify({'error': 'url and save_path are required'}), 400
# 生成业务相关的 task_id
task_id = f"download_{user_id}_{uuid.uuid4().hex[:8]}"
# 启动异步任务,指定自定义 task_id
task = download_file.apply_async(
args=[url, save_path],
task_id=task_id,
queue="download" # 可以覆盖装饰器中的队列
)
# 可以将 task_id 保存到数据库
# save_to_database(user_id, task_id, url)
return jsonify({
'task_id': task.id,
'status': 'Task started',
'progress_url': f'/download_progress/{task.id}'
}), 202
@app.route('/download_progress/<task_id>', methods=['GET'])
def get_download_progress(task_id):
"""
获取下载进度
返回示例:
{
"state": "PROGRESS",
"current": 1024000,
"total": 10240000,
"progress": 10,
"status": "Downloading... 10%"
}
"""
# 通过 task_id 创建 AsyncResult 对象
task = AsyncResult(task_id, app=celery_app)
if task.state == 'PENDING':
response = {
'state': task.state,
'current': 0,
'total': 1,
'progress': 0,
'status': 'Task is waiting...'
}
elif task.state == 'PROGRESS':
response = {
'state': task.state,
'current': task.info.get('current', 0),
'total': task.info.get('total', 1),
'progress': task.info.get('progress', 0),
'status': task.info.get('status', '')
}
elif task.state == 'SUCCESS':
response = {
'state': task.state,
'current': task.info.get('current', 1),
'total': task.info.get('total', 1),
'progress': 100,
'status': task.info.get('status', 'Completed'),
'result': task.info
}
elif task.state == 'FAILURE':
response = {
'state': task.state,
'current': 0,
'total': 1,
'progress': 0,
'status': str(task.info),
'error': str(task.info)
}
else:
response = {
'state': task.state,
'status': 'Unknown state'
}
return jsonify(response)
@app.route('/cancel_download/<task_id>', methods=['POST'])
def cancel_download(task_id):
"""取消下载任务"""
task = AsyncResult(task_id, app=celery_app)
task.revoke(terminate=True)
return jsonify({
'status': 'Task cancelled',
'task_id': task_id
})
if __name__ == '__main__':
app.run(debug=True, port=5000)
7.3 前端调用示例
javascript
// 启动下载
fetch('http://localhost:5000/start_download', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
url: 'https://example.com/large-file.zip',
save_path: '/tmp/downloaded-file.zip',
user_id: 'user_123'
})
})
.then(res => res.json())
.then(data => {
const taskId = data.task_id;
// 轮询获取进度
const interval = setInterval(() => {
fetch(`http://localhost:5000/download_progress/${taskId}`)
.then(res => res.json())
.then(progress => {
console.log(`Progress: ${progress.progress}%`);
console.log(`Status: ${progress.status}`);
// 更新进度条
document.getElementById('progress-bar').style.width =
`${progress.progress}%`;
// 任务完成或失败,停止轮询
if (progress.state === 'SUCCESS' ||
progress.state === 'FAILURE') {
clearInterval(interval);
console.log('Download finished!');
}
});
}, 1000); // 每秒查询一次
});
7.4 部署步骤
makefile
# 1. 安装依赖
pip install celery redis flask requests
# 2. 启动 Redis
redis-server
# 3. 启动 Celery Worker(专门处理下载队列)
celery -A celery_app worker -Q download --loglevel=info
# 4. 启动 Flask 服务器
python flask_server.py
# 5. 测试
curl -X POST http://localhost:5000/start_download \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/file.zip","save_path":"/tmp/file.zip","user_id":"user_123"}'
八、核心概念总结
| 概念 | 作用 | 何时使用 |
|---|---|---|
| task_id | 任务唯一标识符 | 需要追踪任务状态时 |
| AsyncResult | 任务结果代理对象 | 查询任务状态和结果 |
| delay() | 简化的任务调用 | 简单场景,不需要配置 |
| apply_async() | 完整的任务调用 | 需要 task_id、延迟、优先级等 |
| bind=True | 任务自我感知 | 需要更新进度、重试、访问元数据 |
| queue | 任务分类隔离 | 不同类型任务需要独立处理 |
九、最佳实践建议
9.1 任务设计
- 粒度适中:任务不要太大(避免超时)也不要太小(避免开销)
- 幂等性:任务应该可以安全重试,多次执行结果相同
- 错误处理:使用 try-except 捕获异常,避免任务崩溃
- 超时设置:为长时间任务设置合理的超时时间
python
@app.task(bind=True, time_limit=3600, soft_time_limit=3000)
def long_task(self, data):
# time_limit: 硬超时(强制终止)
# soft_time_limit: 软超时(抛出异常)
pass
9.2 队列设计
- 按业务分类:email, sms, report, video, dataset
- 按优先级分类:high, normal, low
- 按资源需求分类:cpu-intensive, io-intensive
- 合理配置 Worker:快速任务多并发,慢速任务少并发
9.3 监控和调试
shell
# 使用 Flower 监控工具
pip install flower
celery -A myapp flower
# 访问 http://localhost:5555 查看:
# - 各队列的任务数量
# - Worker 的运行状态
# - 任务执行历史
# - 实时监控图表
9.4 生产环境配置
ini
# celery_config.py
from kombu import Queue
# 任务路由
app.conf.task_routes = {
'myapp.tasks.send_email': {'queue': 'email'},
'myapp.tasks.process_video': {'queue': 'video'},
}
# 结果过期时间
app.conf.result_expires = 3600 # 1小时
# 任务序列化
app.conf.task_serializer = 'json'
app.conf.result_serializer = 'json'
app.conf.accept_content = ['json']
# 时区设置
app.conf.timezone = 'Asia/Shanghai'
app.conf.enable_utc = True
# Worker 预取设置
app.conf.worker_prefetch_multiplier = 4
# 任务确认
app.conf.task_acks_late = True
十、常见问题解答
Q1: 任务执行失败了怎么办?
python
@app.task(bind=True, max_retries=3, default_retry_delay=60)
def retry_task(self, data):
try:
# 可能失败的操作
risky_operation(data)
except Exception as exc:
# 自动重试
raise self.retry(exc=exc)
Q2: 如何避免重复任务?
ini
# 使用固定的 task_id
task_id = f"process_user_{user_id}"
result = process_user.apply_async(
args=[user_id],
task_id=task_id
)
# 如果 task_id 已存在,Celery 会拒绝或忽略
Q3: 如何实现定时任务?
python
from celery.schedules import crontab
# 配置定时任务
app.conf.beat_schedule = {
'cleanup-every-day': {
'task': 'tasks.cleanup',
'schedule': crontab(hour=3, minute=0), # 每天凌晨3点
},
}
# 启动 Beat 调度器
# celery -A myapp beat --loglevel=info
Q4: Worker 数量如何设置?
r
# CPU 密集型:Worker 数 = CPU 核心数
celery -A myapp worker -c 4
# IO 密集型:Worker 数 = CPU 核心数 * 2-4
celery -A myapp worker -c 16
# 混合型:根据实际负载调整
结语
Celery 是 Python 生态中最成熟的异步任务队列系统,掌握它的核心概念------task_id、AsyncResult、bind、queue------能让你轻松应对各种异步任务场景。
参考资源
- Celery 官方文档
- Celery GitHub
-
Flower 监控工具\]([flower.readthe](https://link.juejin.cn?target=https%3A%2F%2Fflower.readthe "https://flower.readthe")