Python定时任务库Celery——分布式任务队列

本文详细介绍了Celery的安装、配置和使用,包括实例化、任务定义、任务执行、定时任务设置、任务状态跟踪、递归调用等。还对比了Celery与其他定时任务库,并提供了动态添加定时任务的解决方案。此外,文章还涵盖了Celery在Flask中的异步任务执行和常见问题解决。
摘要由CSDN通过智能技术生成

定时任务库对比

推荐阅读 Python timing task - schedule vs. Celery vs. APScheduler

大小优点缺点适用场景
Schedule轻量级易用无配置不能动态添加任务或持久化任务简单任务
Celery重量级①任务队列
②分布式
①不能动态添加定时任务到系统中,如Flask(Django可以)
②设置起来较累赘
任务队列
APScheduler相对重量级①灵活,可动态增删定时任务并持久化
②支持多种存储后端
③集成框架多,用户广
重量级,学习成本大通用
Rocketry轻量级易用功能强尚未成熟,文档不清晰通用

Celery不适合动态添加定时任务,但本人认为可以通过数据库+递归调用自身实现




简介

Celery 是一款简单灵活可靠的分布式任务执行框架,支持大量任务的并发执行。

Celery 采用典型生产者和消费者模型。生产者提交任务到任务队列,众多消费者从任务队列中取任务执行。

在这里插入图片描述

  • 提交任务给 Broker 队列
  • 如果是异步任务,Worker 会立即从队列中取出任务并执行,执行结果保存在 Backend 中
  • 如果是定时任务,任务由 Celery Beat 进程周期性地将任务发往 Broker 队列,Worker 实时监视消息队列获取队列中的任务执行

应用场景

  • 长时间任务的异步执行, 如上传大文件
  • 实时任务执行,支持集群部署,如支持高并发的机器学习推理
  • 定时任务执行,如定时发送邮件




安装

本文使用 Redis 作为 Broker 即消息队列

pip install celery
pip install redis

需要持久化任务的话,Broker 使用 RabbitMQ 并设置持久化队列。
官方建议生产环境首选 RabbitMQ ,突然停止或断电 Redis 可能会数据丢失。

注意!从 Celery 4.x 开始官方不再支持Windows。




初试

Celery 的开发主要有四个步骤:

  1. 实例化 Celery
  2. 定义任务
  3. 启动任务 Worker
  4. 调用任务



启动 Redis

redis-server

实例化 Celery 和 定义任务

tasks.py

import time
from celery import Celery

celery = Celery('tasks', broker='redis://localhost:6379/0', backend='redis://localhost:6379/1')  # 实例化 Celery


@celery.task
def sendmail(mail):  # 定义任务。使用@task装饰器
    print('sending mail to %s...' % mail['to'])
    time.sleep(2.0)
    print('mail sent.')
    return True

启动任务 Worker

celery -A tasks worker --loglevel=info --pool=solo

调用任务

from tasks import sendmail

result = sendmail.delay(dict(to='celery@python.org'))
value = result.get()
print(value)  # 运算结果值
print(result.successful())  # 是否成功
# print(result.fail())  # 是否失败
print(result.ready())  # 是否执行完成
print(result.state)  # 状态 PENDING -> STARTED -> SUCCESS/FAILURE

结果
在这里插入图片描述




进阶

项目结构

.
└─proj
        config.py       # 配置文件
        __init__.py     # Celery实例化
        tasks.py        # 实时任务
        period_task.py  # 定时任务



配置文件

config.py

BROKER_URL = 'redis://localhost:6379/0'  # Broker,中间件,进行消息传输,使用Redis
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'  # Backend,结果后端,使用Redis
CELERY_RESULT_SERIALIZER = 'json'  # 结果序列化方案
CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24  # 任务过期时间
CELERY_TIMEZONE = 'Asia/Shanghai'  # 时区配置
CELERY_IMPORTS = (  # 导入的任务模块
    'proj.tasks',
    'proj.period_task'
)

流行使用RabbitMQ作为Broker中间件,Redis作为结果后端。



Celery实例化

__init__.py

from celery import Celery

app = Celery('proj')  # 创建Celery实例
app.config_from_object('proj.config')  # 从配置文件中读取配置



实时任务

tasks.py

from proj import app


@app.task
def add(x, y):
    return x + y


@app.task
def mul(x, y):
    return x * y


@app.task
def show(a):
    return a

启动任务Worker

celery worker -A proj -l info -c 4 -P solo



定时任务

period_task.py

from proj import app
from celery.schedules import crontab


@app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
    """按频率执行定时任务"""

    # 每5秒执行一次tostring('Hello')
    sender.add_periodic_task(5.0, tostring.s('Hello'), name='tostring')

    # 每周一07:30执行tostring('Happy Mondays!')
    sender.add_periodic_task(
        crontab(hour=7, minute=30, day_of_week=1),
        tostring.s('Happy Mondays!'),
    )

    # 每分钟执行一次
    sender.add_periodic_task(
        crontab(minute='*/1'),
        tostring.s('A minute'),
        name='A minute'
    )


@app.task
def tostring(s):
    return s

先忽略这部分

更细粒度定时设置查阅:

  1. Crontab schedules
  2. crontab 命令详解
  3. Periodic Tasks

定时任务配置也可以这样设置

from proj import app
from celery.schedules import crontab


@app.task
def tostring(s):
    return s


app.conf.beat_schedule.update(
    hello={
        'task': tostring.name,
        'schedule': 5.0,  # 每5秒执行一次tostring('Hello')
        'args': ('Hello',)
    },
    happy_mondays={
        'task': tostring.name,
        'schedule': crontab(hour=7, minute=30, day_of_week=1),  # 每周一07:30执行tostring('Happy Mondays!')
        'args': ('Happy Mondays!',)
    },
    a_minute={
        'task': tostring.name,
        'schedule': crontab(minute='*/1'),  # 每分钟执行一次
        'args': ('A minute',)
    },
)



调用任务

常规任务

  • delay():直接调用任务,是 apply_async() 的封装
  • apply_async():通过发送异步消息调用任务,可指定倒计时 countdown ,执行时间 eta ,过期时间 expires 等参数
  • signature():创建签名,可传递任务签名给别的进程使用,或作为其他函数的参数
  • s():创建签名的快捷方式
from wedo.tasks import mul

result = mul.delay(1, 2)  # 直接调用
print(result.get())

result = mul.apply_async((1, 2), countdown=2)  # 2s后执行
print(result.get())

t1 = mul.signature((1, 2), countdown=2)  # 签名Signatures,可传递任务签名给别的进程使用,或作为其他函数的参数
result = t1.delay()
print(result.get())

t1 = mul.s(1, 2).set(countdown=2)  # 创建签名的快捷方式
result = t1.delay()
print(result.get())




组合任务

  • group():组合,接受一个可并行调用的任务列表
  • chain():串联,将签名连接在一起,一个接一个调用(前一个签名的结果作为下一个签名的第一个参数)
  • chord():和弦,类似 group() 但包含回调,在所有任务执行完后再调用任务
  • map():将参数列表应用于该任务
  • starmap():将复合参数列表应用于该任务
  • chunks():将一个很长的参数列表分块成若干部分
from proj.tasks import add, mul, show
from celery import group, chain, chord

result = group(add.s(i, i) for i in range(5))()  # 组合
print(result.get())  # [0, 2, 4, 6, 8]

result = chain(add.s(1, 2), add.s(3), mul.s(3))()  # 串联
print(result.get())  # ((1+2)+3)*3=18

result = chord((add.s(i, i) for i in range(5)), show.s())()  # 和弦
print(result.get())  # [0, 2, 4, 6, 8]

result = ~show.map(['Hello', 'World'])
print(result)  # ['Hello', 'World']

result = ~add.starmap([(2, 2), (4, 4)])
print(result)  # [4, 8]

res = add.chunks(zip(range(10), range(10)), 2)()
print(res.get())  # [[0, 2], [4, 6], [8, 10], [12, 14], [16, 18]]

详细查阅:

  1. 签名Signatures
  2. Celery函数



启动定时任务

启动定时任务Beat

celery beat -A proj.period_task -l info

结果
在这里插入图片描述




任务状态跟踪

项目结构

.
└─monitor
        config.py       # 配置文件
        __init__.py     # Celery实例化
        tasks.py        # 实时任务
        main.py         # 调用任务

config.py

BROKER_URL = 'redis://localhost:6379/0'  # Broker,中间件,进行消息传输,使用Redis
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'  # Backend,结果后端,使用Redis
CELERY_RESULT_SERIALIZER = 'json'  # 结果序列化方案
CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24  # 任务过期时间
CELERY_TIMEZONE = 'Asia/Shanghai'  # 时区配置
CELERY_IMPORTS = (  # 导入的任务模块
    'monitor.tasks'
)

__init__.py

from celery import Celery, Task
from celery.utils.log import get_task_logger

logger = get_task_logger(__name__)  # 日志

app = Celery('monitor')  # 创建Celery实例
app.config_from_object('monitor.config')  # 从配置文件中读取配置


class TaskMonitor(Task):
    def on_success(self, retval, task_id, args, kwargs):
        """success时回调"""
        logger.info('task id:{} , arg:{} , successful !'.format(task_id, args))

    def on_retry(self, exc, task_id, args, kwargs, einfo):
        """retry时回调"""
        logger.info('task id:{} , arg:{} , retry !  einfo: {}'.format(task_id, args, exc))

    def on_failure(self, exc, task_id, args, kwargs, einfo):
        """failure时回调"""
        logger.info('task id: {0!r} failed: {1!r}'.format(task_id, exc))

tasks.py

from monitor import app, TaskMonitor


@app.task(base=TaskMonitor)
def success():
    return 1


@app.task(bind=True, base=TaskMonitor)
def retry(self):
    try:
        raise Exception
    except Exception as exc:
        self.retry(exc=exc)


@app.task(base=TaskMonitor)
def failure():
    raise Exception

main.py

from monitor.tasks import success, retry, failure

result = success.delay()
print(result.successful())  # 是否成功
print(result.ready())  # 是否执行完成
print(result.state)  # 状态 PENDING -> STARTED -> SUCCESS/FAILURE
print()

result = retry.delay()
print(result.successful())
print(result.ready())
print(result.state)
print()

result = failure.delay()
print(result.successful())
print(result.ready())
print(result.state)

效果
在这里插入图片描述




递归调用

tasks.py

from celery import Celery

app = Celery(
    'tasks',
    broker='redis://localhost:6379/0',
    backend='redis://localhost:6379/1',
    imports=['tasks']
)
app.conf.timezone = 'Asia/Shanghai'
i = 0


@app.task
def show(a):
    global i
    i = i + 1
    s = '{} {}'.format(a, i)
    print(s)
    if i >= 20:
        i = 0
        return
    show.apply_async(args=(a,), countdown=i)  # 递归

调用任务

from tasks import show

show.apply_async(args=('Hi',))

结果
在这里插入图片描述




Celery配置

可以直接通过代码配置而不用 Celery.config_from_object()

app = Celery(
    'tasks',
    broker='redis://localhost:6379/0',
    backend='redis://localhost:6379/1',
    # imports=['tasks']
)
app.conf.imports = ['tasks']

详细查阅:Configuration and defaults




命令行参数

参数含义全称
-A指定模块
-l日志level–loglevel
-c进程数–concurrency
-Q指定队列–queue
-B周期性任务–beat
-P池的实现–pool

详细查阅:Command Line Interface




分布式集群部署

部署过程和单机启动一样,使用相同项目代码和启动命令。

实现原理是共享 Broker 队列。




任务队列监控

flower 是一款 Celery 的监控工具

安装

pip install flower

启动

flower -A wedo --port=5555

在这里插入图片描述



Celery 版本大于4.4.7可能会报错




动态定时任务

Celery不适合动态添加定时任务,但本人认为可以通过数据库+递归调用自身实现

最好用 APSchedule 实现,因为动态定时任务需要用到长时间的 countdown 或 eta,若这样的定时任务过多,会大量占用内存,导致重启和执行非延迟任务会很耗时。

并且使用Redis作为Broker并且异步任务执行时间延迟超过1小时,Celery会重复发布任务,导致任务重复执行

更好的解决办法是每天定时跑!!即实现from celery.schedules import crontab

celery.conf.beat_schedule.update

tasks.py

import sqlite3
from celery import Celery
from celery.utils.log import get_task_logger

logger = get_task_logger(__name__)  # 日志

app = Celery(
    'tasks',
    broker='redis://localhost:6379/0',
    backend='redis://localhost:6379/1',
    imports=['tasks'],
)
app.conf.timezone = 'Asia/Shanghai'

conn = sqlite3.connect('database.db', check_same_thread=False)
c = conn.cursor()
sql = '''
CREATE TABLE IF NOT EXISTS `tasks` 
(
   `id` INTEGER UNIQUE PRIMARY KEY AUTOINCREMENT,
   `name` TEXT,
   `countdown` INTEGER
);
'''
c.execute(sql)  # 创建数据库


def create(name='job', countdown=5):
    """创建定时任务"""
    sql = 'INSERT INTO `tasks` (`name`, `countdown`) VALUES (?, ?)'
    c.execute(sql, (name, countdown))
    conn.commit()
    return c.lastrowid


def read(id=None, verbose=False):
    """查询定时任务"""
    sql = 'SELECT * FROM `tasks` '
    if id:
        sql = 'SELECT * FROM `tasks` WHERE `id`={}'.format(id)
    all_rows = c.execute(sql).fetchall()
    if verbose:
        print(all_rows)
    return all_rows


def update(id, countdown):
    """修改定时任务"""
    sql = 'UPDATE `tasks` SET `countdown`=? WHERE `id`=?'
    c.execute(sql, (countdown, id))
    conn.commit()


def delete(id, verbose=False):
    """删除定时任务"""
    sql = 'DELETE FROM `tasks` WHERE `id`=?'
    affected_rows = c.execute(sql, (id,)).rowcount
    if verbose:
        print('已删除{}行数据'.format(affected_rows))
    conn.commit()


@app.task
def job(id):
    # 读取定时任务数据
    id = read(id)
    if id:
        id, name, countdown = id[0]
    else:
        logger.info('stop')
        return

    # 需要进行的任务
    logger.warning('id={}'.format(id))
    logger.warning('name={}'.format(name))
    logger.warning('countdown={}'.format(countdown))

    # 递归调用
    job.apply_async(args=(id,), countdown=countdown)

main.py

from tasks import *

print('创建定时任务')
id = create(name='job', countdown=5)
job(id)  # 立即运行
# job.apply_async((id,), countdown=5)  # 5s后运行

print('查询定时任务:', read())

input('回车修改定时任务')
update(id, countdown=1)

input('回车删除定时任务')
delete(id, verbose=True)

启动

celery -A tasks worker --loglevel=info --pool=solo

效果
在这里插入图片描述




动态定时任务(指定时间)

参数 eta 受时区影响,本人感觉较麻烦,可以直接使用 countdown

from datetime import datetime, timedelta


def next_weekday(weekday, d=datetime.now()):
    """ 获取下周几日期

    :param weekday: weekday取值1-7
    :param d: 原日期,默认当前时间
    :return: datetime.datetime
    """
    delta = weekday - d.isoweekday()
    if delta == 0:
        delta = 7
    return d + timedelta(delta)


d1 = datetime.now()
d2 = next_weekday(1, d1)  # 下周一
delta = d2 - d1
countdown = delta.total_seconds()
print(countdown)  # 604800.0

Redis默认不过期

长时间的countdown可能会过期,考虑 Redis 设为不过期或使用 RabbitMQ

redis-cli
KEYS *
TTL _kombu.binding.celery

在这里插入图片描述




Python Web:Flask异步执行任务

使用Celery+Redis实现,异步执行任务,获取执行状态




遇到的坑

1. 报错 ValueError: not enough values to unpack (expected 3, got 0)

启动 Celery 添加参数 --pool=solo

2. 报错 Cannot connect to redis://localhost:6379/0: Error 11002 connecting to localhost:6379. Lookup timed out

参考这篇文章

3. 重启Celery后任务丢失

task_reject_on_worker_lost = True
task_acks_late = True

or

celery.conf.CELERY_REJECT_ON_WORKER_LOST = True
celery.conf.CELERY_ACKS_LATE = True

4. 使用Redis作为Broker并且异步任务执行时间延迟超过1小时,Celery会重复发布任务,导致任务重复执行

将Celery升级到4.5以上并增大visibility_timeout的时间

默认visibility_timeout为3600秒即一小时

BROKER_TRANSPORT_OPTIONS = {'visibility_timeout': 43200}
或者
app.conf.broker_transport_options = {'visibility_timeout': 43200}

还可以通过 MySQL 的唯一索引特性实现锁:在业务中添加锁,能插入就执行




参考文献

  1. Celery Documentation
  2. Celery 中文手册
  3. Celery GitHub
  4. 任务调度利器:Celery
  5. Celery 从入门到进阶
  6. Flower Documentation
  7. Flower GitHub
  8. Celery 常见问题
  9. Python动态处理定时任务的生态与深坑
  10. Celery 报错 Windows 平台:ValueError: not enough values to unpack (expected 3, got 0)
  11. How to dynamically add / remove periodic tasks to Celery (celerybeat)
  12. Common Issues Using Celery
  13. 解决Celery进程重启后,正在进行中的任务丢失或者标记为失败
  14. Celery重复执行同一个任务
  15. celery异步任务重复执行的解决办法
  16. Visibility timeout
  17. 使用Celery踩过的坑
  18. Celery重复执行一个Task的解决方案
  • 6
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Python Celery是一个分布式任务队列框架,它可以让你在分布式系统中异步执行任务。它支持定时任务的调度,可以让你在指定的时间间隔或者特定的时间点执行任务。 要使用Celery进行定时任务调度,你需要先安装Celery。可以使用pip命令进行安装: ``` pip install celery ``` 安装完成后,你需要创建一个Celery实例,并配置它的任务队列和调度器。下面是一个简单的示例: ```python from celery import Celery from datetime import timedelta # 创建Celery实例 app = Celery('tasks', broker='redis://localhost:6379/0') # 配置定时任务调度器 app.conf.beat_schedule = { 'task-name': { 'task': 'tasks.task_name', # 任务函数的路径 'schedule': timedelta(seconds=10), # 任务执行的时间间隔 }, } # 定义任务函数 @app.task def task_name(): # 任务的具体逻辑 pass ``` 在上面的示例中,我们创建了一个名为`tasks`的Celery实例,并配置了Redis作为任务队列的中间件。然后,我们使用`app.conf.beat_schedule`配置了一个定时任务调度器,其中`task-name`是任务的名称,`task`是任务函数的路径,`schedule`是任务执行的时间间隔,这里设置为每10秒执行一次。最后,我们定义了一个名为`task_name`的任务函数,你可以在这个函数中编写具体的任务逻辑。 为了启动Celery定时任务调度器,你需要在终端中执行以下命令: ``` celery -A tasks beat ``` 这样,Celery就会按照你配置的时间间隔或者时间点执行定时任务。 希望以上内容能够帮助到你,如果有任何疑问,请随时提问。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XerCis

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值