使用 Celery RedBeat 进行周期性任务的智能动态调度
在没有 Django 的情况下使用 Celery 时,一个一直困扰着我的问题是 —如何禁用和重新启用定期任务?在本文中,我们将了解如何使用 Celery-RedBeat 添加、删除和更新任务计划。代码可在此处获取。
ChatGPT-4 生成的图像
动机
您可能想问自己的第一个问题是:
为什么有人想要在 Celery 中不断禁用并重新启用定期任务?
好吧,在我们的工作中,我们有一些 Celery 链(我们称之为管道),用于收集、转换和存储一些数据。这些管道可以通过一些数据类进行配置。在过去的几个月里,我们一直在将这些配置与代码库分开。我们这样做是为了让利益相关者可以随意更新/添加/删除配置。这里的一个重要点是,每个配置都代表某个特定管道的一次运行。
考虑到上述情况,不难想象利益相关者会随心所欲地添加和删除配置。但是,如果删除了配置,我们不应该为其运行管道。
撤销(禁用)和重新启用定期任务的想法促使我研究如何做到这一点。
当然,另一种方法是安排一个任务定期加载配置并使用 进行调度apply_async
。但是,这样做有什么乐趣呢?
芹菜红节拍
Celery-RedBeat 是一个自定义的 Celery beat 调度程序,它使用 Redis 来存储安排定期任务所需的信息。如果您已经使用 Redis 作为代理,那么它会非常方便。
django-celery-scheduler
如果您的服务/应用程序不是 Web 应用程序,它也更方便。
配置 Redbeat 非常简单。我们可以通过 1) 命令行或 2) Celery 应用程序的配置来告诉调度程序使用 RedBeat。
我喜欢的方法是这样的:
exec celery -A reset_schedule.beat beat -S redbeat.RedBeatScheduler --loglevel debug
我有一个entrypoint.sh
由运行 Celery 的 docker 服务使用的脚本:
#!/bin/sh
echo $1
rm logs.txt
如果[ " $1 " = 'scheduler' ]
然后
exec celery -A reset_schedule.beat beat -S redbeat.RedBeatScheduler --loglevel debug
# exec celery -A reset_schedule.beat beat --loglevel debug
elif [ " $1 " = 'worker' ]
然后
exec celery -A reset_schedule.worker worker -Q default --loglevel info
elif [ " $1 " = 'flower' ]
然后
exec celery -A reset_schedule.worker --broker=redis://redis:6379/0 flower --conf=/config/flowerconfig.py
fi
exec " $@ "
我们不会在这个故事中使用花。
或者,如果您有一个包含所有 celery 配置的 Python 文件,您可以简单地添加:
beat_scheduler = 'redbeat.RedBeatScheduler''redbeat.RedBeatScheduler'
我的项目架构设置如下:
。
│ └──
reschedule │ │ └── reschedule.py └── test_tasks │ └── __init__.py │ └── print_task.py └── simple_pipeline.py
好的,那么,考虑到这一点,让我们开始定义定期任务。
定义定期任务
我们使用实例来定义周期性任务RedBeatScheduleEntry
。我们定义两个实例:
entry1 = RedBeatSchedulerEntry(
"print_task", print_task.pipeline.s().task, 20, app=app“print_task”,print_task.pipeline.s().task,20,app=app
)
entry2 = RedBeatSchedulerEntry(
“simple_pipeline”,
simple_pipeline.pipeline.s().task,
# 15,
crontab(分钟= “0”),
app=app,
)
entry1.save()
entry2.save()
每个实例都存储到 Redis。请注意,我是在on_after_finalize
信号触发的函数中执行此操作的。
接下来,我们要验证时间表是否确实存储到了 Redis 中。为此,我在helper_functions
包中创建了一个函数。
获取时间表
以下是从 Redis 获取时间表的完整函数:
def get_schedules () -> dict:
config = RedBeatConfig(app)
schedule_key = config.schedule_key
redis = redbeat.schedulers.get_redis(app)
elements = redis.zrange(schedule_key,0, - 1,withscores= False )
entry = {el:RedBeatSchedulerEntry.from_key(key=el,app=app) for el in elements}
返回条目
我们首先创建类的一个实例RedBeatConfig
。我需要配置来获取schedule_key
。默认情况下,RedBeat 会将所有周期性任务名称存储在键下redbeat::schedule
。在这里,我在运行时获取键。该键位于实例schedule_key
的属性下RedBeatConfig
。
这schedule_key
是 Redis ZSET,即有序集合。它存储实际周期性任务条目的键。我们首先从配置中获取 Redis 客户端(我们可能自己创建了一个 Redis 客户端,但还没有尝试过),然后使用该zrange
方法从集合中加载所有键。
使用这些键,我们可以RedBeatSchedulerEntry.from_key
从 Redis 中加载实际条目作为RedBeatScheduleEntry
实例。值列表将转换为字典,其中键是 Redis 键,值是相应的条目实例。
然后我们可以在终端中打印时间表来检查一切是否符合预期:
celery-reschedule-scheduler | INFO:reset_schedule.beat:
{'redbeat:print_task':<RedBeatSchedulerEntry:print_task reset_schedule.worker.test_tasks.print_task.pipeline() <频率:20.00 秒>,'redbeat:print_task':<RedBeatSchedulerEntry:print_task reset_schedule.worker.test_tasks.print_task.pipeline() <频率:20.00 秒>,
'redbeat:simple_pipeline':<RedBeatSchedulerEntry:simple_pipeline reset_schedule.worker.test_tasks.simple_pipeline.pipeline() <crontab:0 * * * * (m/h/dM/MY/d)>}
运行 Redis 和 Scheduler 服务就足以实现这一点。
以下是来自beat > init.py模块的代码:
从celery.schedules导入crontab
从celery.utils.log导入get_task_logger
从redbeat导入RedBeatSchedulerEntry
从reset_schedule.app导入config
从reset_schedule.celeryapp导入app
从reset_schedule.worker.helper_functions导入get_schedules
从reset_schedule.worker.test_tasks导入print_task、simple_pipeline
logger = get_task_logger(__name__)
如果config.is_dev():
导入日志记录
logs.basicConfig(level=logging.INFO)
@app.on_after_finalize.connect
def setup_periodic_tasks ( sender,**kwargs ):
尝试:
logger.info( “SETUP PERIODIC TASK” )
entry1 = RedBeatSchedulerEntry(
“print_task”,print_task.pipeline.s().task,20,app=app
)
entry2 = RedBeatSchedulerEntry(
"simple_pipeline" ,
simple_pipeline.pipeline.s().task,
crontab(minute= "0" ),
app=app,
)
entry1.save()
entry2.save()
logger.info(get_schedules())
except Exception as e:
logger.error( f"发生异常:{e} " )
这里的一个关键点是注意周期性任务的调度。simple_pipeline计划每小时 0 分钟运行一次,print_task**计划每 20 秒运行一次。
我们实际上安排了什么?
定期任务
本演示使用了两个周期性任务。第一个计划的任务是print_task:
从reset_schedule.celeryapp导入app
从reset_schedule.worker.helper_functions导入log_schedule_execution
@app.task( bind= True )
def Printer ( self,task_arg ):
打印(task_arg)
log_schedule_execution(self)
@app.task
def some_task ( *arg,a= None,b= None,c= None ):
打印( f "打印任务参数:a:{a},b:{b},c:{c} " )
@app.task
def Pipeline ():
返回Printer.s(some_task.s([ 1 , 2 , 3 ],a= 15 )).apply_async()
这实际上是我之前在 Celery 上一篇文章中遗留下来的任务。它的作用基本上是这样的 — 该printer
任务获取任务的签名some_task
并打印出签名。它还调用该log_schedule_execution
函数。我稍后会讲到这个函数。
我启动的另一个任务是simple_pipeline:
@app.task( bind= True )
def Printer ( self ):
print ( "来自打印机的问候 :-)" )
log_schedule_execution(self)
@app.task
def Pipeline ():
return Printer.s().apply_async()
这个更简单。它只是启动一个子任务,打印出一条消息并调用一些日志记录函数。
好的,让我们继续使用管道来演示更新和删除定期任务。
重新安排管道
重新调度管道定义如下:
@app.task
def reschedule_task(tasks_to_remove:list [ str ],tasks_to_schedule:list [ dict ]):
返回(
print_schedule.s()
| remove_periodic_tasks.si(task_names=tasks_to_remove)
| schedule_periodic_tasks.si(tasks=tasks_to_schedule)
| print_schedule.si()
)。apply_async()
可以看出,管道接受要删除的周期性任务名称列表和包含安排新任务的详细信息的字典列表。该链由 4 个任务组成,其中三个是唯一的(print_schedule
使用了两次)。我们可以想象这些参数是由外部系统提供的。
我们来看看这个print_schedule
任务:
@app.task( bind= True )
def print_schedule ( self ):
schedules = get_schedules()
打印(schedules)
log_schedule_execution( self )
可以看出,该任务只是从我们之前定义的函数中获取计划,打印它们,并调用一些日志函数。
第二个任务更有趣:
@app.task( bind= True )
def remove_periodic_tasks ( self,task_names:list [ str ] ):
redbeat_scheduler = RedBeatScheduler(app=app)
key_prefix = get_redbeat_key_prefix()
对于task_names中的任务:
key = f“ {key_prefix} : {task} “
entry = redbeat_scheduler.Entry.from_key(key=key,app=app)
entry.delete()
log_schedule_execution( self )
我们创建一个RedBeatScheduler
实例,并从 RedBeat 配置中获取 Redis 键前缀。然后我们迭代参数task_names
。对于列表的每个元素,我们使用redbeat_scheduler.Entry.from_key
加载所需的条目并将其从 Redis 中删除。
第三个任务非常相似:
@app.task(bind = True)
def schedule_periodic_tasks(self,tasks:list [ dict ]):
redbeat_scheduler = redbeat.RedBeatScheduler(app = app)
entries = {}
for task_details in task:
schedule = task_details [ “schedule” ]
task = task_details [ “task” ]
name = task_details [ “name” ]
entrys [name] = RedBeatSchedulerEntry(name,task,schedule,app = app)
redbeat_scheduler.update_from_dict(entries)
log_schedule_execution(self)
我们首先再次创建一个 RedBeat 调度程序实例。然后我们遍历字典列表。对于每个字典,我们根据提供的名称、任务和计划创建一个 RedBeat 调度程序条目。该名称也用作字典的键entries
。
我们使用update_from_dict
RedBeat 调度程序实例上定义的方法更新 Redis 中的数据。
最后,调用日志函数。
最后一个任务是print_schedule
再次。好的,那么,让我们看看如何安排重新调度管道。
调度重新调度管道
管道在文件worker > init.py中调度。首先,我们不想对管道进行两次调度(启动调度程序和工作程序时)。因此,我们检查是工作程序还是调度程序,并清除 Redis 中可能已存在的任何消息:
如果config.is_scheduler():
返回
app.control.purge()
接下来,我们定义要删除的任务以及要安排的任务:
:复制代码
最后,我们安排管道:
reschedule.reschedule_task.apply_async(
kwargs={
“tasks_to_remove”: task_to_remove,“tasks_to_remove”:tasks_to_remove,
“tasks_to_schedule”:tasks_to_schedule,
},
倒计时= 5,
)
就像调度程序的情况一样,我在一个监听信号的函数内部执行此操作on_after_finalize
:
@app.on_after_finalize.connect
def setup_periodic_tasks(发送者,**kwargs):
...
日志记录功能
最后,这是日志功能的实现:
def get_definition(entry:RedBeatSchedulerEntry)-> dict:
return {
“name”:entry.name,
“task”:entry.task,
“args”:entry.args,
“kwargs”:entry.kwargs,
“options”:entry.options,
“schedule”:str(entry.schedule),
“enabled”:entry.enabled,
}
def log_schedule_execution(task:Task):
schedule = get_schedules()
name = str(task)
d_schedule = {}
for key in schedule:
d_schedule [key] = get_definition(schedule [key])
使用 open(“logs.txt”,“a”)作为文件:
data = {
“time”:datetime.utcnow()。isoformat(),
“task”:name,
“schedule”:d_schedule,
}
json.dump(数据,fp =文件,缩进= 4,sort_keys = True)
file.write(“\ n”)
此日志记录功能仅用于帮助追踪管道执行后发生的情况。数据将存储在 中log.txt
。
结果
我们可以使用docker compose up
或 来运行此示例,make run
因为我添加了 Makefile。本场景中有一些我们不感兴趣的任务的输出。但是,这就是文件的作用所在log.txt
。
让示例运行大约 40 秒后,以下是日志文件的内容:
{
“schedule” : {
“redbeat:print_task” : {
“args” : null,
“enabled” : true,
“kwargs” : { } ,
“name” : “print_task” ,
“options” : { } ,
“schedule” : “<freq:20.00 秒>” ,
“task” : “reset_schedule.worker.test_tasks.print_task.pipeline”
} ,
“redbeat:simple_pipeline” : {
“args” : null,
“enabled” : true,
“kwargs” : { } ,
“name” : “simple_pipeline” ,
“options” : { } ,
“schedule” : “<crontab:0 * * * * (m/h/dM/MY/d)>” ,
“task” : , “任务” :“<@task: reset_schedule.worker.reschedule_tasks.reschedule.print_schedule of reset_schedule at 0x7f6331cb90d0>” , “时间”
:“2024-02-15T22:07:41.602288” } { “调度” :{ “redbeat:simple_pipeline” :{ “args” :null,“ enabled” :true,“kwargs” :{ } ,“名称” :“simple_pipeline” ,“选项” :{ } ,“调度” :“<crontab:0 * * * * (m/h/dM/MY/d)>” ,“任务” :“reset_schedule.worker.test_tasks.simple_pipeline.pipeline” } } ,“任务” :“<@task:reset_schedule.worker.reschedule_tasks.reschedule.remove_periodic_tasks of reset_schedule at 0x7f6331cb90d0>” ,“时间” :“2024-02-15T22:07:41。618172“ } { “schedule” :{ “redbeat:simple_pipeline” :{ “args” :null,“enabled” :true,“kwargs” :{ } ,“name”
: “simple_pipeline” ,
“options” : { } ,
“schedule” : “<freq:5.00 秒>” ,
“task” : “reset_schedule.worker.test_tasks.simple_pipeline.pipeline”
}
} ,
“task” : “<@task:reset_schedule.worker.reschedule_tasks.reschedule.schedule_periodic_tasks of reset_schedule at 0x7f6331cb90d0>” ,
“time” : “2024-02-15T22:07:41.633047”
}
{
“schedule” : {
“redbeat:simple_pipeline” : {
“args” : null,
“enabled” : true,
“kwargs” : { } ,
“name” : “simple_pipeline” ,
“options” : { } ,
“schedule” : “<freq:5.00 秒>” ,
“task” : “reset_schedule.worker.test_tasks.simple_pipeline.pipeline”
}
} ,
“task” : “<@task:reset_schedule.worker.reschedule_tasks.reschedule.print_schedule of reset_schedule at 0x7f6331cb90d0>” ,
“time” : “2024-02-15T22:07:41.646036”
}
{
“schedule” : {
“redbeat:simple_pipeline” : {
“args” : null,
“enabled” : true,
“kwargs” : { } ,
“name” : “simple_pipeline” ,
“选项” : { } ,
“时间表” : “<频率:5.00 秒>” ,
“任务” : “reset_schedule.worker.test_tasks.simple_pipeline.pipeline”
}
} ,
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:07:56。394385“
}
{
“schedule” : {
“redbeat:simple_pipeline” : {
“args” : null,
“enabled” : true,
“kwargs” : { } ,
“name” : “simple_pipeline” ,
“options” : { } ,
“schedule” : “<freq:5.00 秒>” ,
“task” : “reset_schedule.worker.test_tasks.simple_pipeline.pipeline”
}
} ,
“task” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“time” : “2024-02-15T22:08:01.397953”
}
{
“schedule” : {
“redbeat:simple_pipeline” : {
“args” : null,
“enabled” : true,
“kwargs” : { } ,
“name” : “simple_pipeline” ,
“options” : { } ,
“时间表” : “<频率:5.00 秒>” ,
“任务” : “reset_schedule.worker.test_tasks.simple_pipeline.pipeline”
}
} ,
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:06.402599”
}
{
“时间表” : {
“redbeat:simple_pipeline” : {
“args” : null,
“enabled” : true,
“kwargs” : { } ,
“name” : “simple_pipeline” ,
“options” : { } ,
“时间表” : “<频率:5.00 秒>” ,
“任务” : “reset_schedule.worker.test_tasks.simple_pipeline.pipeline”
}
} ,
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:11.407355”
}
{
“进度” : {
“redbeat:simple_pipeline” : {
“args” : null,
“enabled” : true,
“kwargs” : { } ,
“name” : “simple_pipeline” ,
“options”: { } ,
“时间表” : “<频率:5.00 秒>” ,
“任务” : “reset_schedule.worker.test_tasks.simple_pipeline.pipeline”
}
} ,
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:16.422847”
}
您可以注意到,在运行第一个打印计划任务时,Redis 中有两个 RedBeat 计划条目。我们既有 simple_pipeline *,它使用 crontab 计划每小时 0 分钟运行一次,也有print_task ,*它每 20 秒运行一次。
任务remove_periodic_tasks
在删除条目后,在返回之前调用 log 函数。这可以从上面的 JSON 中看到。第二个元素与此任务相关,我们可以看到我们只有一个条目:simple_pipeline,仍然通过 crontab 进行调度。
第三个 JSON 元素是在任务返回之前记录的。simple_pipeline的schedule_periodic_tasks
调度现在已经改变:
"schedule" : "<频率:5.00 秒>" ,
现在simple_pipeline计划每 5 秒运行一次。我们可以跳过下一个 json 元素。
其余元素由simple_pipeline打印机任务记录。
您可以通过查看task
密钥来验证这一点:
“任务” : “<@task: reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>”
如果你看一下时间,你会发现它们相隔约 5 秒:
{
...
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:07:56.394385”
}
{
...
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:01.397953”
}
{
...
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:06.402599”
}
{
...
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:11.407355”
}
{
...
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:16.422847”
}
看起来我们成功重新安排了管道。
概括
Celery-RedBeat 提供了一种非常方便的方式来定义定期任务,特别是当您使用 Redis 时,而无需设置数据库后端或 Django 的开销。
从外观上看,它支持我和我的同事可能需要的所有用例。
imple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:06.402599”
}
{
...
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:11.407355”
}
{
...
“任务” : “<@task:reset_schedule.worker.test_tasks.simple_pipeline.printer of reset_schedule at 0x7f6331cb90d0>” ,
“时间” : “2024-02-15T22:08:16.422847”
}
看起来我们成功重新安排了管道。
概括
Celery-RedBeat 提供了一种非常方便的方式来定义定期任务,特别是当您使用 Redis 时,而无需设置数据库后端或 Django 的开销。
从外观上看,它支持我和我的同事可能需要的所有用例。