celery-redbeat方案(动态定时任务、异步任务)


记一次工作上的问题

问题:项目上当前定时任务框架和服务端耦合,容易出现加载定时任务时间很长,影响后端服务启动,容易改动引发定时任务的问题。且能方便的动态的增加或删除定时任务而无需重启服务。
需求:将定时任务框架解耦,可以独立部署启动,不影响原来服务的功能,不会出现重复执行的定时任务和丢失定时任务
RedBeat是一个 Celery Beat 调度程序 ,它将计划任务和运行时元数据存储在Redis中。

为什么选择 RedBeat?

官方文档:https://redbeat.readthedocs.io/en/latest/intro.html
git仓库:https://github.com/sibson/redbeat

  1. 动态实时任务创建和修改,无需长时间停机
  2. 使用 Redis 绑定从任何语言外部管理任务
  3. 共享数据存储;Beat 不依赖于单个驱动器或机器
  4. 即使任务数量很多也能快速启动
  5. 防止意外运行多个 Beat 服务器
    用Celery-redbeat重构当前定时任务逻辑: Celery-redbeat是基于redis作为celery的中间件(broker)时的一个任务调度器(scheduler),有效提高调度器(scheduler)健壮性。

它最大的变化就是,它将调度器从原来Celery进程中的定时任务调度器(默认作为守护进程),改存在了Redis上。
因此,当任务创建或者发生变化的时候,调度器不再需要暂停和重载。利用Redis数据库的特性,我们只需要更新Redis中的键名,调度器就会随之发生改变。

方案

  1. 另起一个celery服务,不再跟随主服务启动
  2. 有一张数据库表用来针对动态定时任务做持久化存储以及做增删改查状态管理等。celery beat启动时会自动读取这张表的数据,将定时任务加载到redis中(每次会先清空再加载)
  3. 初始化完成后的redis数据, celery beat会根据redis里的scedule数据,如果满足执行条件,会将其推送到broker中,等待worker来消费
  4. 定时任务执行原理和之前可以保持一致,但不需要再往celery队列里插任务了,相当于直接执行celery队列中已有的任务,之前的锁逻辑还可以直接沿用
  5. 当用户对定时任务有任何更删改操作时,可以通过封装好的方法,先更新数据库中的值,然后直接更新redis中的值,从而同步更新队列中的定时任务,无需重启服务(此处需要保障数据库和redis的数据一致性,最后能有兜底or事务机制,要么都成功要么都失败)

坑事项记录

背景:发现线上服务会偶尔出现定时任务全部失效的情况,或者定时任务多次执行的情况
环境:k8s多pod部署服务来增加服务健壮性
分析线上日志,就是在某一个时间点突然报错:
LockNotOwnedError异常,表示无法释放或延长一个已经不再拥有的锁
所以一开始我们怀疑是redbeat存在bug,分析源码:
1、redbeat在启动时会触发 @beat_init.connect 装饰的 acquire_distributed_beat_lock 函数。使用 redis_client.lock() 方法创建一个分布式锁对象,通过 lock.acquire() 方法获取锁,此处会进行阻塞,直到成功获取到锁为止。最后,将获取到的锁对象赋值给调度器的 lock 属性。代码中生成的 time_task_:lock 锁用于在 Beat 启动时获取分布式锁,以实现在多个 Beat 进程或服务器之间协调任务调度的目的。这个锁对于 Beat 工作的用途是确保在分布式环境下只有一个 Beat 进程或服务器在执行任务调度。当多个 Beat 进程或服务器同时运行时,通过获取这个锁,只有获取到锁的进程或服务器可以执行任务调度,其他进程或服务器会被阻塞等待锁的释放。同时如果这个锁存在会给锁续期。(为了避免主进程 Readbeat 在执行任务时出现故障或长时间没有释放锁(例如进程崩溃、网络中断等情况),所以可能会有多个beat进程,虽然正常情况下永远只会有一个在工作。我们平台目前就只有一个beat服务)
2、然后Beat 服务会进入一个无限循环的进程,它会以一定的时间间隔进行轮询,检查是否需要执行任务。并且对time_task_:lock 锁进行续期。只有在进程终止时会释放

然后我们怀疑是,在某次续期时,这个锁会莫名其妙突然的消失,所以导致了报错问题
根据官方文档分析。time_task_:lock 锁仅仅是使用分布式锁来防止多个实例同时运行。我们当前仅设置一个实例服务,可以直接设置redbeat_lock_key = None关闭该功能。

本以为解决了问题,结果过了几天问题又再次出现,最后在偶然的灵机一动下发现
我们使用的k8s多pod服务,每个pod服务中的项目代码都是同一套一样的(不管是server还是celery还是beat),发现如果某个pod重启了,会将前面方案中的第二点中的预加载init函数(为了重置所有定时任务,防止异常数据残留)执行一次,就是会清空redis关于redbeat的数据然后重新加载数据,这就导致了所有数据和锁消失了~~

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Celery动态添加定时任务是指在运行中动态地添加新的定时任务,而不需要重启Celery服务。具体实现方法如下: 1. 在Django中创建一个任务模块,定义需要执行的任务函数。 2. 在Django中创建一个视图函数,用于接收前端传来的定时任务参数,如任务名称、执行时间、任务参数等。 3. 在视图函数中,使用Celery提供的add_periodic_task方法,将任务添加到Celery beat中。 4. 在Celery配置文件中,设置beat_schedule参数,指定需要执行的定时任务。 举个例子,假设我们需要添加一个每天早上9点执行的任务,具体步骤如下: 1. 在Django中创建一个任务模块,定义需要执行的任务函数my_task。 2. 在Django中创建一个视图函数add_task,用于接收前端传来的任务参数。 3. 在add_task函数中,使用Celery提供的add_periodic_task方法,将任务添加到Celery beat中: ``` from celery import shared_task from celery.schedules import crontab from myapp.tasks import my_task @shared_task def add_task(task_name, task_time): crontab_time = crontab(hour=9, minute=0) # 每天早上9点执行 task = my_task.s() # my_task为需要执行的任务函数 task_name = 'my_task_' + task_name # 任务名称 app.conf.beat_schedule[task_name] = { 'task': task, 'schedule': crontab_time, } ``` 4. 在Celery配置文件中,设置beat_schedule参数,指定需要执行的定时任务: ``` app.conf.beat_schedule = { 'my_task_1': { 'task': 'myapp.tasks.my_task', 'schedule': crontab(hour=9, minute=0), }, } ``` 这样,每次调用add_task函数,就可以动态地添加新的定时任务了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值