1.简介
概述
- Celery 是一款非常简单、灵活、可靠的分布式系统,可用于处理大量消息,并且提供了一整套操作此系统的一系列工具。
- Celery 是一款消息队列工具,可用于处理实时数据以及任务调度。
- Celery 是由python写的,本身不提供队列服务,对接用Redis或RabbitMQ实现队列服务。
优点
- 简单:celery的 配置和使用还是比较简单的, 非常容易使用和维护和不需要配置文件
- 高可用:当任务执行失败或执行过程中发生连接中断,celery 会自动尝试重新执行任务。如果连接丢失或发生故障,worker和client 将自动重试,并且一些代理通过主/主或主/副本复制方式支持HA。
- 快速:一个单进程的celery每分钟可处理上百万个任务
- 灵活: 几乎celery的各个组件都可以被扩展及自定制
5大角色
| 角色 | 职责 |
|---|---|
| Task | 就是任务,有异步任务和定时任务。 |
| Broker | 中间人,接收生产者发来的消息即Task,将任务存入队列。任务的消费者是Worker,Celery本身不提供队列服务,推荐用Redis或RabbitMQ实现队列服务。 |
| Worker | 执行任务的单元,它实时监控消息队列,如果有任务就获取任务并执行它。 |
| Beat | 定时任务调度器,根据配置定时将任务发送给Broker。 |
| Backend | 用于存储任务的执行结果。 |
调度流程
2.安装
通过pip安装
# 本地python版本3.7
pip install celery
# 验证安装 查看版本(5.2.7)
celery --version
redis作为backend和broker
pip install redis
3.目录结构
celery对目录要求严格,如果不在目录下加入__init__.py,worker执行任务可能会出现NotRegistered的情况
demo1
- __init__.py
- tasks.py
- run.py
4.初始化对象
注意broker和backend使用redis不能使用同一个仓库有坑
from celery import Celery
celery_app = Celery(settings.CELERY_APP_NAME, broker=settings.BROKER_REDIS_SERVER_URL, backend=settings.BACKEND_REDIS_SERVER_URL)
5.任务队列
celery_app.conf.task_routes = {
'app.worker.*_api_schedule_.*': {'queue': 'sche'},
}
6.定时任务
from celery.schedules import crontab
celery_app.conf.timezone = 'Asia/Shanghai'
celery_app.conf.enable_utc = True
celery_app.conf.beat_schedule = {
'fastapi_update_device_status_preminute_schedule': { # 任务名称(随意)
'task': 'app.worker.fast_api_test_worker.run_fast_api_schedule_update_devices_status', # 调用方法 路径+函数名称
'schedule': 60.0, # 每分钟
'args': (), # 调用传参
'options': {
'queue': 'sche' # 执行队列
}
},
'fastapi_distribute_clear_log_schedule': { # 任务名称(随意)
'task': 'app.worker.fast_api_test_worker.run_fast_api_schedule_log_clear', # 调用方法 路径+函数名称
'schedule': crontab(hour=6,minute=30), # 每天6:30
'args': (), # 调用传参
'options': {
'queue': 'sche' # 执行队列
}
},
}
7.任务
配置
from app.core import celery_app
from app.tool.klog import klog
from app.tool.BaseMobile import BaseMobile
from .base_task import BaseTask
from app.config.settings import settings
@celery_app.task(acks_late=True,base=BaseTask)
def run_fast_api_task(taskname,packagename,packagepath,rerunTimes,features,componenttype,taskid=-1,device_id="null",notice=None):
self = run_fast_api_task
klog.enable_log()
klog.log("执行Fastapi平台接口测试任务-" + taskname)
mobile = BaseMobile()
return self.runTask(taskid=str(taskid),celeryid=celery_app.current_task.request.id,taskname=taskname,packagepath=packagepath,packagename=packagename,rerunTimes=rerunTimes,withUI=False,cleanBefore=True,port=mobile.getTargetPort(), \
device_id=device_id,feature_info=features,notice_info=notice,hostname=celery_app.current_task.request.hostname,componenttype=componenttype)
继承
class BaseTask(celery_app.Task):
def on_failure(self, exc, task_id, args, kwargs, einfo):
klog.log_e('{0!r} on_failure: {1!r} {2!r} {3!r}'.format(task_id, exc, args, kwargs))
return super(BaseTask, self).on_failure(exc, task_id, args, kwargs,einfo)
def on_success(self, exc, task_id, args, kwargs):
klog.log('{0!r} on_success: {1!r}'.format(task_id, exc))
return super(BaseTask, self).on_success(exc, task_id, args, kwargs)
def on_retry(self, exc, task_id, args, kwargs, einfo):
klog.log('{0!r} on_retry: {1!r}'.format(task_id, exc))
return super(BaseTask, self).on_retry(exc, task_id, args, kwargs,einfo)
def after_return(self, status, retval, task_id, args, kwargs, einfo):
klog.log('{0!r} after_return: {1!r}'.format(task_id, retval))
return super(BaseTask, self).after_return(status, retval, task_id, args, kwargs, einfo)
执行
@celery_app.task(acks_late=True,base=BaseTask)
def run_fast_api_schedule_task_distribution():
self = run_fast_api_schedule_task_distribution
klog.disable_log()
result = 0
scheduler_info = self.get_distribute_scheduler_info()
if scheduler_info:
job_info = json.loads(scheduler_info.extra)
device_info = self.get_device_by_id(job_info["device_id"])
if device_info and device_info.status == "idle":
run_fast_api_task.apply_async((job_info["taskname"],job_info["packagename"],job_info["packagepath"],job_info["rerunTimes"],job_info["features"],job_info["componenttype"]),\
{'taskid': job_info["taskid"],"device_id":job_info["device_id"],"notice":job_info["notice"]}, queue=job_info["componenttype"])
self.update_scheduler_status_by_id(id=scheduler_info.id,status=0)
return result
8.启动命令
# 启动执行worker
celery -A app.worker.fast_api_test_worker worker -c 1 -l info -n $worker_name --max-tasks-per-child 1 -Q $queue_name
# 启动定时任务beat
celery -A app.worker.fast_api_test_worker beat -l info
9.其他
任务执行过长导致重复执行问题
celery对ETA/countdown/retry等要求具体时间执行的任务支持并不完整. 指定执行时间,与celery自身的失效重传机制有所冲突. celery在没有收到任务被worker正常执行的时候就会发起重传. celery的默认重传timeout是1个小时(Visibility timeout).因此理论上在ETA时间没有到之前,celery每过一个小时便重复提交一个任务给worker
celery_app.conf.broker_transport_options = {'visibility_timeout': 3600*10} # 10 hours
实现一个任务存储一个日志
import os
from logging import StreamHandler
from celery import current_task
from celery.signals import task_prerun, task_postrun
class CeleryTaskLoggerHandler(StreamHandler):
terminator = '\r\n'
def __init__(self, *args, **kwargs):
self.task_id_fd_mapper = {}
super().__init__(*args, **kwargs)
# 使用 celery的task信号,设置任务开始和结束时的执行的东西
# 主要是获取task_id 然后创建对应的独立任务日志文件
task_prerun.connect(self.on_task_start)
task_postrun.connect(self.on_start_end)
@staticmethod
def get_current_task_id():
# celery 内置提供方法获取task_id
if not current_task:
return
task_id = current_task.request.id
return task_id
def on_task_start(self, sender, task_id, **kwargs):
# 这里是根据task_id 定义每个任务的日志文件存放
log_path = os.path.join('app/storage/logs/', f"{task_id}.log")
f = open(log_path, 'a')
self.task_id_fd_mapper[task_id] = f
def on_start_end(self, sender, task_id, **kwargs):
f = self.task_id_fd_mapper.pop(task_id, None)
if f and not f.closed:
f.close()
self.task_id_fd_mapper.pop(task_id, None)
def emit(self, record):
# 自定义Handler必须要重写的一个方法
task_id = self.get_current_task_id()
if not task_id:
return
try:
f = self.task_id_fd_mapper.get(task_id)
self.write_task_log(f, record)
self.flush()
except Exception:
self.handleError(record)
def write_task_log(self, f, record):
# 日志的实际写入
if not f:
# raise ValueError('Not found thread task file')
return
msg = self.format(record)
f.write(msg)
f.write(self.terminator)
f.flush()
def flush(self):
for f in self.task_id_fd_mapper.values():
if f:
f.flush()
先定义信号回调处理函数add_celery_logger_handler, 然后进行信号的绑定,绑定一般是采用装饰器的方式
当然也可以不采用这种方式,然后在需要使用信号的地方,进行单独绑定配置(after_setup_logger.connect(add_celery_logger_handler))
from celery.signals import after_setup_logger
from app.core.logging_app import CeleryTaskLoggerHandler
import logging
@after_setup_logger.connect
def add_celery_logger_handler(sender=None, logger=None, loglevel=None, format=None, **kwargs):
if not logger:
return
task_handler = CeleryTaskLoggerHandler()
task_handler.setLevel(settings.LOG_LEVEL)
formatter = logging.Formatter(format)
task_handler.setFormatter(formatter)
logger.addHandler(task_handler)
logger.info("Here call the celery logging signal - after_setup_logger")
推荐文献
GitHub地址:github.com/celery/cele…
中文手册:www.celerycn.io/