老程序员速学Celery笔记
目的
在经济下行的情况下,只能多多拓展自我技术栈,提高竞争能力
结构图
1、Broker:一般就是Redis或者MQ,存放任务队列的
2、Worker:消费者
3、Backend:结果存放队列,也是Redis或者MQ
4、Task:个人感觉就是生产者
安装
pip install celery
个人直接用的5.3.1,一上手就是最新版本,导致遇到个坑,看别人以前的教程会有问题。
第一个坑
Traceback (most recent call last):
File "C:\Python39\lib\site-packages\billiard\pool.py", line 362, in workloop
result = (True, prepare_result(fun(*args, **kwargs)))
File "C:\Python39\lib\site-packages\celery\app\trace.py", line 635, in fast_trace_task
tasks, accept, hostname = _loc
ValueError: not enough values to unpack (expected 3, got 0)
在通过your_func.delay()任务给celery去执行的时候就会报这个错。问GPT被绕进去了,后面还是自行网上找了下解决方案,下面直接抄作业
解决
- Windows 10系统
看别人描述大概就是说win10上运行celery4.x就会出现这个问题,解决办法如下,原理未知:先安装一个 eventlet
pip install eventlet
然后启动worker的时候加一个参数,如下:
celery -A celery_tasks.main worker -l info -P eventlet
然后就可以正常的调用了
基础使用
创建一个python文件,命名:my_task.py
import celery
import time
backend='redis://127.0.0.1:6379/1'
broker='redis://127.0.0.1:6379/2'
cel=celery.Celery('first_task',backend=backend,broker=broker)
@cel.task
def send_email(name):
print("向%s发送邮件..."%name)
time.sleep(5)
print("向%s发送邮件完成"%name)
return "ok"
@cel.task
def send_msg(name):
print("向%s发送短信..."%name)
time.sleep(5)
print("向%s发送短信完成"%name)
return "ok"
看以上的命名my_task,cel,frist_task尽量区别化命名,方便后面运行调试信息输出时,可以对比哪些参数是哪些
运行
D:\代码\my_code\study\study_celery>celery -A my_task worker -l info -P eventlet
-------------- celery@WINDOWS-3T5S0CL v5.1.2 (sun-harmonics)
--- ***** -----
-- ******* ---- Windows-10-10.0.10586-SP0 2023-07-02 20:24:05
- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app: first_task:0x2b9d54582e0
- ** ---------- .> transport: redis://:**@120.79.235.7:6379/63
- ** ---------- .> results: redis://:**@120.79.235.7:6379/63
- *** --- * --- .> concurrency: 8 (eventlet)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. my_task.send_email
. my_task.send_msg
[2023-07-02 20:24:06,312: INFO/MainProcess] Connected to redis://:**@120.79.235.7:6379/63
[2023-07-02 20:24:06,394: INFO/MainProcess] mingle: searching for neighbors
[2023-07-02 20:24:08,250: INFO/MainProcess] mingle: sync with 1 nodes
[2023-07-02 20:24:08,254: INFO/MainProcess] mingle: sync complete
[2023-07-02 20:24:08,368: INFO/MainProcess] pidbox: Connected to redis://:**@120.79.235.7:6379/63.
[2023-07-02 20:24:08,500: INFO/MainProcess] celery@WINDOWS-3T5S0CL ready.
上面提示过多创建不一样的名字,方便我们区分每个名称是代表什么
-
celery -A my_task worker -l info -P eventlet
启动指令中my_task就是文件名。启动指令中不要带.py -
app: first_task:0x2b9d54582e0
app的名称就是你celery.Celery生成app时的第一个传参 -
transport: redis://:@120.79.235.7:6379/63
results: redis://:@120.79.235.7:6379/63
你的中间redis和结果redis,如果你项目多了,测试,正式环境多了,启动时记得多检查 -
[tasks]
. my_task.send_email
. my_task.send_msg
这里可以清晰的看到你这个celery app里面有哪些Task -
celery@WINDOWS-3T5S0CL ready.
看到这条信息,redis才是正常运行
不要小看启动信息,在每次启动的时候请多检查里面的信息
让你的celery app执行起来
先创建一个文件,produce_task.py:
from my_task import send_email, send_msg
email_result = send_email.delay("hello")
print(email_result.id)
msg_result = send_msg.delay("world")
print(msg_result.id)
执行以上代码,即对启动着的celery发送两个执行命令,分别执行官send_msg和send_email,delay里直接传参就是send_msg的传参
踩坑
这里又踩坑,broker和backend在同一个redis的话,会导致两个delay,只执行一个,问题点找不到,然后换成两个就好了。问题根源后面再找,现在不要打算学习进度
查看执行结果
在backend的redis里面就可以查看到所有执行结果,成功的,失败的,是以string型进行存储。直接存DB根目录
获取执行结果
# 成功结果
async_succ_result = AsyncResult(id="23c7ebfd-5bf6-4f83-b7c8-9c870b168d17", app=cel)
# 失败结果
async_fail_result = AsyncResult(id="923360d1-caa4-41ca-9887-3bc404ff0803", app=cel)
# 判断是否成功
success_flag1 = async_succ_result.successful()
success_flag2 = async_fail_result.successful()
# 判断是否失败
failure_flag1 = async_succ_result.failed()
failure_flag2 = async_fail_result.failed()
# 获取结果,如果是成功,则返回之前函数里的return值
result = async_succ_result.get()
# 获取结果,如果是失败,则会报异常
try:
result = async_fail_result.get()
except Exception as e:
print(e)
运行状态
# 运行结果状态
if async_succ_result.status == 'PENDING':
print('任务等待中被执行')
elif async_succ_result.status == 'RETRY':
print('任务异常后正在重试')
elif async_succ_result.status == 'STARTED':
print('任务已经开始被执行')
elif async_succ_result.status == 'FAILURE':
print('任务执行失败')
elif async_succ_result.status == 'SUCCESS':
print('任务执行成功')
函数里的备注只有这五个状态
删除结果
async_succ_result.forget()
注意如果删除了结果,则获取的时候会卡住。
多任务结构
- 当你需要执行的任务很多的时候需要对任务进行结构性分层,此时则需要进行多文件的区分,以便维护
新建一个目录:my_multi_tasks,作为我们的多任务根目录
- 创建celery.py文件
import celery
backend='redis://127.0.0.1:6379/1'
broker='redis://127.0.0.1:6379/2'
second_cel = celery.Celery(
'second_tasks',
backend=backend,
broker=broker,
include={
'my_multi_tasks.my_task_01',
'my_multi_tasks.my_task_02'
}
)
此处文件名必须为celery,因为celery.exe控制器执行命令时,就是检测你的项目底下的celery文件,去配置你的多任务结构。如果名字不为celery,则会报错如下:
D:\代码\my_code\study\study_celery>celery -A my_multi_tasks worker -l info -P eventlet
Usage: celery [OPTIONS] COMMAND [ARGS]...
Error: Invalid value for '-A' / '--app':
Unable to load celery application.
Module 'my_multi_tasks' has no attribute 'celery'
Module ‘my_multi_tasks’ has no attribute ‘celery’,说明了my_multi_tasks没有celery属性,可以看出默认程序是去.celery了
- 创建my_task_01.py文件
import time
from my_multi_tasks.elery import second_cel
@second_cel.task
def second_send_email(name):
print(f"向{name}发送邮件".center(30, '1'))
time.sleep(5)
print(f"向{name}发送邮件完成".center(30, '1'))
return "ok"
- 创建my_task_02.py文件
import time
from my_multi_tasks.elery import second_cel
@second_cel.task
def second_send_msg(name):
print(f"向{name}发送短信".center(30, '2'))
time.sleep(5)
print(f"向{name}发送短信完成".center(30, '2'))
return "ok"
现在将send_email和send_msg分别放在task01和task02,记得在函数前面加上该多任务结构的celery
- 创建produce_task_second.py文件
from my_multi_tasks.my_task_01 import second_send_email
from my_multi_tasks.my_task_02 import second_send_msg
email_result = second_send_email.delay('second_hello')
msg_result = second_send_msg.delay('second_world')
- 执行指令celery -A my_multi_tasks worker -l info -P eventlet
启动celery后,使用produce_task_second.py文件进行任务的发送。可以查看任务执行的过程
定时任务
任务结构体还是直接照搬上面多任务结构的任务体。针对p添加多一个produce文件进行定时任务的讲解
from datetime import datetime
from my_multi_tasks.my_task_01 import second_send_email
from my_multi_tasks.my_task_02 import second_send_msg
# 指定时间发送
time_temp = datetime(2023, 7, 4, 6, 19, 00)
time_temp_utc = datetime.utcfromtimestamp(time_temp.timestamp())
result = second_send_email.apply_async(args=['kola', ], eta=time_temp_utc)
print(result.id)
# 延迟发送
from datetime import timedelta
time_now = datetime.now()
time_now_utc = datetime.utcfromtimestamp(time_now.timestamp())
time_now_utc_new = time_now_utc + timedelta(seconds=20)
result2 = second_send_msg.apply_async(args=['cat', ], eta=time_now_utc_new)
print(result2.id)
- 注意:
1、celery的定时任务默认是以国际标准UTC为准,所以中国时间还需要做一下UTC转换
2、在执行以上语句后脚本即结束,任务已丢入队列。等待时间到达后进行执行
3、注意args的传参,它是[‘kola’, ],有个逗号,意味着传进去的是一个tuple
通过配置信息和beat,启动定时任务
beat是celery很重要的一个生产者。它是一个脚本服务,跟worker一样,启动后是一直在跑的一个脚本程序,现在进行配置
import celery
from celery.schedules import crontab
backend='redis://127.0.0.1:6379/1'
broker='redis://127.0.0.1:6379/2'
second_cel = celery.Celery(
'second_tasks',
backend=backend,
broker=broker,
include={
'my_multi_tasks.my_task_01',
'my_multi_tasks.my_task_02'
}
)
# 设置定时任务的执行时间的时区,改为中国,关闭utc
second_cel.conf.timezone = 'Asia/Shanghai'
second_cel.conf.enable_utc = False
# 发送任务的调度器
second_cel.conf.beat_schedule = {
# 随意命名
'add-task-every-1-minute': {
# 传参
'args': ('张三',),
# 执行函数的路径,指定second_send_email函数
'task': 'my_multi_tasks.my_task_01.second_send_email',
# 调度时间
# 'schedule': 10.0, # 每几秒,执行一次
# 'schedule': timedelta(seconds=6), # 每几秒,执行一次,但timedelta更丰富,比如day,hour
'schedule': crontab(minute="*/1"), # 每一分钟,执行一次
},
'add-task-birthday': {
'args': ('李四',),
'task': 'my_multi_tasks.my_task_02.second_send_msg',
# 每年4月11号,8点42分执行
'schedule': crontab(minute=10, hour=5, day_of_month=5, month_of_year=7),
},
}
celery支持直接修改执行任务的默认时区:second_cel.conf.timezone
通过配置的形式生成定时任务清单:second_cel.conf.beat_schedule
- 启动指令:celery -A my_multi_tasks beat -l info
注意这里不需要加-P eventlet,因为-P是指定消费者的执行器。这个生产者,所以不需要。
在执行并观察后,可以发现生产者beat和消费者worker是完全分离的。beat也是一个程序,通过命令行启动后只管按照配置表设置的时间往中间件broker的一个键celery丢定时任务。以下为一个样例
{
"body": "W1siXHU2NzRlXHU1NmRiIl0sIHt9LCB7ImNhbGxiYWNrcyI6IG51bGwsICJlcnJiYWNrcyI6IG51bGwsICJjaGFpbiI6IG51bGwsICJjaG9yZCI6IG51bGx9XQ==",
"content-encoding": "utf-8",
"content-type": "application/json",
"headers": {
"lang": "py",
"task": "my_multi_tasks.my_task_02.second_send_msg",
"id": "1dce9df1-da1e-4c4e-ac6b-be170b815400",
"shadow": null,
"eta": null,
"expires": null,
"group": null,
"group_index": null,
"retries": 0,
"timelimit": [null, null],
"root_id": "1dce9df1-da1e-4c4e-ac6b-be170b815400",
"parent_id": null,
"argsrepr": "['\u674e\u56db']",
"kwargsrepr": "{}",
"origin": "gen12600@WINDOWS-3T5S0CL",
"ignore_result": false
},
"properties": {
"correlation_id": "1dce9df1-da1e-4c4e-ac6b-be170b815400",
"reply_to": "3f3250fb-6ed4-3d13-83db-a7d8e30c8ee9",
"delivery_mode": 2,
"delivery_info": {
"exchange": "",
"routing_key": "celery"
},
"priority": 0,
"body_encoding": "base64",
"delivery_tag": "88d2a3dd-5612-4ef7-ae36-0d2484b9a468"
}
}
感觉就两个参数值有观察意义,一个是任务函数,一个是传参
“task”: “my_multi_tasks.my_task_02.second_send_msg”,
“argsrepr”: “[‘\u674e\u56db’]”,
- 注意在启动了beat后,如果不及时消费任务会越堆积越多。到时候worker一启动,这些任务全部都要被执行。可能需要一些过期不执行机制
Celery应用在Django中
新建一个Django项目,在More Settings里面Application name中命名一个app01,在创建项目的时候就会顺带创建一个app
在urls里面创建一个test路径指向app01的视图
再在app01的views视图函数里面添加test函数,简单测试一下网站访问127.0.0.1/test的页面,确认返回ok
- 随后在Django项目根目录底下创建一个my_celery文件夹,用于存放你的celery
目录的配置如上,一个email任务,一个sms任务,config是配置文件,main是主程序 - config.py(配置文件,这里特别注意broker_url和result_backend这两个变量名不能错,必须不能错)
broker_url = 'redis://127.0.0.1:6379/15'
result_backend = 'redis://127.0.0.1:6379/14'
- tasks.py(任务文件,celery的任务必须写在tasks.py的文件中,别的文件名称不识别)
import time
import logging
from my_celery.main import app
log = logging.getLogger("django")
@app.task # name表示设置任务的名称,如果不填写,则默认使用函数名做为任务名
def send_sms(mobile):
"""发送短信"""
print("向手机号%s发送短信成功!"%mobile)
time.sleep(5)
return "send_sms OK"
@app.task # name表示设置任务的名称,如果不填写,则默认使用函数名做为任务名
def send_sms2(mobile):
print("向手机号%s发送短信成功!" % mobile)
time.sleep(5)
return "send_sms2 OK"
- main.py(主程序,创建celery的app和加载Django配置文件)
import os
from celery import Celery
# 创建celery的实例对象,做解耦,不在这里传参broker和backend,通过配置文件进行设置
app = Celery('my_django_celery')
# 把celery和Django进行结合,识别和加载Django的配置文件,注意这里是在环境中加入Django的配置
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'celeryPros.settings.dev')
# 通过app对象加载配置,把config.py里面的配置读到celery里面去
app.config_from_object('my_celery.config')
# 加载任务,注意!默认会去目录底下的tasks.py文件里面找,所以tasks.py这个文件命名有特殊意义
app.autodiscover_tasks([
'my_celery.my_email',
'my_celery.my_sms',
])
# 启动Celery的命令
# 强烈建议切换目录到mycelery根目录下启动
# celery -A my_celery.main worker --loglevel=info
- 启动指令:celery -A my_celery.main worker --loglevel=info
注意这里尽量要在celery文件夹的外面路径,然后执行指令。可以通过my_celery.main去定位路径
以上都配置完成后,再回到Django的app01项目中的views.py函数中去触发你的任务
from django.shortcuts import render, HttpResponse
from my_celery.my_sms.tasks import send_sms
from my_celery.my_sms.tasks import send_sms2
from my_celery.my_email.tasks import send_email
from my_celery.my_email.tasks import send_email2
# Create your views here.
def test(request):
# 异步任务
# 1. 声明一个和celery一模一样的任务函数,但是我们可以导包来解决
# send_sms.delay("110")
# send_sms2.delay("119")
# # send_sms.delay() #如果调用的任务函数没有参数,则不需要填写任何内容
# 定时任务
from datetime import timedelta, datetime
time_now = datetime.now()
time_now_utc = datetime.utcfromtimestamp(time_now.timestamp())
time_now_utc_new = time_now_utc + timedelta(seconds=20)
send_email.apply_async(args=['cat', ], eta=time_now_utc_new)
return HttpResponse('ok')
启动你的Django项目,然后在浏览器里访问路径http://127.0.0.1:8000/test/,即可通过访问vies视图函数进行任务的触发