文章主要以自己現在寫的項目舉例,提供自定義異步任務數據持久化的一個思路,具體如何執行還要參考每個項目的具體情況。
版本
- 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);
持久化數據
有了表,那數據何時寫入呢?
以下提供兩種方案:
- 可以自己封裝一個新的調用異步任務的方法,以後在項目中使用統一格式的異步任務的調用。代碼如下:
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)
- 若項目中已有大量的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