celery 自定义数据持久化方案

文章主要以自己现在写的项目举例,提供自定义异步任务数据持久化的一个思路,具体如何执行还要参考每个项目的具体情况。

版本

  • Flask==0.10.1
  • celery==3.1.18

celery 自定义数据持久化方案

  • 由于当前使用MySQL存储任务即可完成日常检索需求,故使用MySQL来做任务的持久化。
  • 如果有复杂检索的需求,可以使用 elasticsearch 来存储数据,可以更方便的实现 UI 界面搜索,比如elasticsearch-head,Kibana, ElasticHD 等。


以下是MySQL的表结构:

create table mq_task_log
(
    id             int auto_increment comment '主键id'
        primary key,
    task_id        varchar(36)  default '' not null comment 'mq的task_id',
    task_name      varchar(128) default '' not null comment '调用的task',
    queue          varchar(36)  default '' not null comment '进入的queue',
    payload        text                    null,
    properties     varchar(256) default '' null,
    status         tinyint      default 0  not null comment '任务执行状态,0 初始 1 执行成功',
    is_compensated tinyint      default 0  not null comment '任务是否被补偿执行,0 否 1 是',
    relation_id    varchar(64)  default '' not null comment '关联的relation_id表,便于后期排查问题',
    relation_type  varchar(64)  default '' not null comment 'relation_id的关联关系',
    created_time   int          default 0  not null comment '创建时间',
    updated_time   int          default 0  not null comment '修改时间',
    constraint task_id
        unique (task_id)
)
    comment 'mq任务log表';

create index ix_mq_task_log_created_time
    on mq_task_log (created_time);

create index ix_mq_task_log_relation_id
    on mq_task_log (relation_id);

持久化数据

有了表,那数据何时写入呢?

以下提供两种方案:

  1. 可以自己封装一个新的调用异步任务的方法,以后在项目中使用统一格式的异步任务的调用。代码如下:
def high_priority_task(func, args=None, kwargs=None, queue=None, producer=None, link=None, link_error=None,
custom_relation_id='', custom_relation_type='',
**options):
    task_id = str(uuid.uuid4())
    # 执行已封装的,将任务信息写入数据库的function
    create_mq_task_log(func.__name__, 
        args=args, kwargs=kwargs,
        task_id=task_id, 
        queue=queue,
        custom_relation_id=custom_relation_id, custom_relation_type=custom_relation_type,
        )
    # 调用异步任务
    func.apply_async(
        args=args, kwargs=kwargs, 
        task_id=task_id, 
        producer=producer, 
        link=link, 
        link_error=link_error,
        queue=queue,
        **options)
  1. 若项目中已有大量的delay和apply_async,全部重写调用方式成本较大,此时可以选择重写 celery.app.task.Task.delay 和 celery.app.task.Task.apply_async,或给 delay/apply_async 添加自定义的插入log的装饰器,重写delay的代码参考如下:
class ContextTask(celery.Task):
    def __call__(self, *args, **kwargs):
        with app.app_context():
            return TaskBase.__call__(self, *args, **kwargs)
        
    def delay(self, *args, queue=None, custom_relation_id='', custom_relation_type='', **kwargs):
        task_id = str(uuid.uuid4())
        # 执行已封装的,将任务信息写入数据库的function
        create_mq_task_log(self.__name__, 
            args=args, kwargs=kwargs,
            task_id=task_id, 
            queue=queue,
            custom_relation_id=custom_relation_id, custom_relation_type=custom_relation_type,
            )
        # 调用异步任务
        func.apply_async(
            args=args, kwargs=kwargs, 
            task_id=task_id, 
            queue=queue)

# 实例化 Celery 之后,为实例指定新的Task类
celery = Celery(app.import_name, broker=app.config['CELERY_BROKER_URL'])
celery.Task = ContextTask
# 或 实例化celery的时候直接指定新的Task类
celery = Celery(app.import_name, broker=app.config['CELERY_BROKER_URL'], task_cls=ContextTask)

问:为什么选择在执行 apply_async 之前写入log呢?

答:此时,mq_task_log.is_compensated 字段就显得很重要了,该字段代表了任务是否真正的进入了 RabbitMq ,可以避免由于 RabbitMq 连接异常等原因造成的任务丢失。

如果是正常业务队列,可以实时消费掉,可以写一个定时脚本去校验是否出现任务丢失的情况,并更新is_compensated=1,然后进行任务的补偿执行。

task 执行结果的保存

  • 为了方便统一管理,这里对celery的task进行重写,统一处理任务处理结果的更新和任务异常的重试方案
  • 如果不想重写 task, 可以重写 celery.app.task.Task.on_failure 和 celery.app.task.Task.on_succes 来实现。


以下是我们项目中对celery.app.base.Celery.task 的重新封装,具体逻辑请看注释

class Celery(_celery.Celery):
    
    # 重写task的执行
    def task(self, *args_task, **opts_task):

        def decorator(func):
            sup = super(Celery, self).task

            @sup(*args_task, **opts_task)
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                from init import redis_store
                from models.mq_task_log_model import MqTaskLog
                try:
                    # 在执行任务之前验证任务的执行结果,并加锁,保障任务只会执行一次
                    task_id = wrapper.request.id
                    with RedLock(task_id, connection_details=[redis_store], ttl=60):
                        mq_task_log = MqTaskLog.find(task_id=task_id).first()
                        # 若任务已经执行,直接结束
                        if mq_task_log and mq_task_log.is_done:
                            return
                        # 执行任务
                        func(*args, **kwargs)
                except RedLockError:
                    return
                except Exception as exc:
                    # 任务重试方案,最多重试3次,每次间隔5秒钟
                    wrapper.retry(exc=exc, args=args, kwargs=kwargs, countdown=5, max_retries=3)
                else:
                    if mq_task_log:
                        # 这里是在model上实现了task_done方法,将status更新为1,表示任务执行成功
                        mq_task_log.task_done()

            return wrapper

        return decorator
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章