文章目錄
1 celery 簡要概述
Celery是一個簡單,靈活,可靠的分佈式系統,用於處理大量消息,同時爲操作提供維護此類系統所需的工具。
它是一個任務隊列,專注於實時處理,同時還支持任務調度。
celery 的優點
-
簡單:celery的 配置和使用還是比較簡單的, 非常容易使用和維護和不需要配置文件
-
高可用:當任務執行失敗或執行過程中發生連接中斷,celery 會自動嘗試重新執行任務
如果連接丟失或發生故障,worker和client 將自動重試,並且一些代理通過主/主或主/副本複製方式支持HA。
-
快速:一個單進程的celery每分鐘可處理上百萬個任務
-
靈活: 幾乎celery的各個組件都可以被擴展及自定製
1.1 celery 可以做什麼?
典型的應用場景, 比如
- 異步發郵件 , 一般發郵件比較耗時的操作,需要及時返回給前端,這個時候 只需要提交任務給celery 就可以了.之後 由worker 進行發郵件的操作 .
- 比如有些 跑批接口的任務,需要耗時比較長,這個時候 也可以做成異步任務 .
- 定時調度任務等
2 celery 的核心模塊
2-1 celery 的5個角色
Task
就是任務,有異步任務和定時任務
Broker
中間人,接收生產者發來的消息即Task,將任務存入隊列。任務的消費者是Worker。
Celery本身不提供隊列服務,推薦用Redis或RabbitMQ實現隊列服務。
Worker
執行任務的單元,它實時監控消息隊列,如果有任務就獲取任務並執行它。
Beat
定時任務調度器,根據配置定時將任務發送給Broker。
Backend
用於存儲任務的執行結果。
注 圖片來自 https://foofish.net/images/584bbf78e1783.png
3 celery 和flask 如何結合起來
3.1項目結構
3.2 項目入口 文件 routes.py
3.3 celery 實例創建,如何和flask綁定在一起呢
說明 這裏 app.tasks 是在 app包下面創建的tasks 包
結構如下
flask 實例的創建 如下圖:
def create_app():
app = Flask(__name__)
# 加載app配置文件
app.config.from_object('config.DB')
# 註冊藍圖
register_blueprint(app)
from app.recall_models.models.dbbase import db
# db 初始化
db.init_app(app)
with app.app_context():
# 創建表
db.create_all()
return app
3.3 task 如何定義
可以從 celery.Task 繼承,如果要想實現回調, task執行成功後, 要發起一個回調的話, 最好要繼承 Task 實現 on_success , on_failure 這兩個方法
from celery import Task
class MyTask(Task):
def on_success(self, retval, task_id, args, kwargs):
"""
任務 成功到時候 ,發起一個回調
# 更新狀態, 更新完成時間
:param retval:
:param task_id:
:param args:
:param kwargs:
:return:
"""
logger.info(f"on_success recall task[{task_id}] success.")
def on_failure(self, exc, task_id, args, kwargs, einfo):
"""
任務失敗的時候,發起一個回調
:param exc:
:param task_id:
:param args:
:param kwargs:
:param einfo:
:return:
"""
logger.info(f"on_failure recall task[{task_id}] failure. exc:{exc} ")
回溯任務 可以直接定義一個函數 ,這裏的任務可以是一些比較耗時的操作, 可能需要跑批數據等等這種情況.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time : 2019/5/17 16:47
@File : recall_online_model.py
@Author : [email protected]
"""
from datetime import datetime
import logging
from celery import Task
from celery.exceptions import CeleryError
from app.recall_models.base import ModelAppearance
from app.recall_models.models.dbbase import RecallRecord
from app.app_init_variables import db_config, db_name
from app import celery
from app.recall_models.base import ReCallReader
from app.recall_models.models.dbbase import db
from config.APP import MODEL_SUFFIX
from util.transfer import str_fmt
logger = logging.getLogger(__name__)
@celery.task(bind=True, base=MyTask)
def recall_model(self, model_name, sql, input_java_factors, input_python_factors, score, prob):
logger.info(f"self.request.id:{self.request.id}")
task_id = self.request.id
recall = ModelAppearance(
model_name=model_name,
sql=sql,
score=score,
prob=prob,
python_factors=input_python_factors,
java_factors=input_java_factors,
task_id=task_id
)
# 模擬耗時操作
# time.sleep(5)
try:
# 這裏是一些耗時任務
return recall()
except CeleryError as e:
logger.error(e)
self.retry(exc=e, countdown=1 * 60, max_retries=3)
raise e
except Exception as e:
logger.error(e)
raise e
3.3.1 綁定任務
有的時候 可能需要綁定 任務,拿到任務的相關的信息.
一個任務綁定 意味着第一個參數 是任務本身的實例 ,這類似與python 中 綁定的方法. self 就是實例本身一樣
參考 官方文檔 http://docs.celeryproject.org/en/latest/userguide/tasks.html
@celery.task(bind=True, base=MyTask)
def recall_model(self, model_name, sql, score, prob):
# 比如需要拿到 任務請求的id
task_id = self.request.id
pass
不綁定任務 就是這樣 的
@celery.task(base=MyTask)
def recall_model( model_name, sql, score, prob):
# 任務處理邏輯
pass
3.4 worker 啓動入口
啓動 Worker,監聽 Broker 中是否有任務
如何啓動 worker 可以通過 命令:
celery worker -A celery_worker.celery --concurrency=2 -l INFO
線上配置 可以 使用 celeryd 配置文件
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Time : 2019/5/14 17:10
@File : celery_worker.py
@Author : [email protected]
項目的根目錄下,有個 celery_worker.py 的文件,
這個文件的作用類似於 wsgi.py,是啓動 Celery worker 的入口。
# 啓動worker
celery worker -A celery_worker.celery -l INFO
# test
celery worker -A celery_worker.celery --concurrency=2 -l INFO
celery 啓動參數
-A 啓動app 的位置
-l 日誌級別Der
"""
import logging
from app import create_app, celery
logger = logging.getLogger(__name__)
app = create_app()
app.app_context().push()
3.5 消費者如何工作
3.5.1 消費者如何消費數據呢?
worker 如何工作呢?
3.6 如何通過task_id 去獲取任務狀態呢
from app import celery
@celery.task(bind=True, base=MyTask)
def recall_model(self, model_name, sql, input_java_factors, input_python_factors, score, prob):
logger.info(f"self.request.id:{self.request.id}")
task_id = self.request.id
recall = ModelAppearance(
model_name=model_name,
sql=sql,
score=score,
prob=prob,
python_factors=input_python_factors,
java_factors=input_java_factors,
task_id=task_id
)
# 模擬耗時操作
# time.sleep(5)
try:
return recall()
except CeleryError as e:
logger.error(e)
self.retry(exc=e, countdown=1 * 60, max_retries=3)
raise e
except Exception as e:
logger.error(e)
raise e
注意這裏 recall_model 是一個celery.task 修飾的函數名稱. 通過 下面的方式就可以拿到 result
result = recall_model.AsyncResult(task_id)
status = result.status
result._get_task_meta() # 這樣就可以拿到task 的狀態信息
4 源碼解析
4.1 celery 的工作流
在 celery.app.amqp.py 模塊裏面 這個 AMQP 類起了關鍵的作用
創建消息,發送消息,消費消息
這裏 生產者 ,消費者 是由 kombu 框架來實現的.
from kombu import Connection, Consumer, Exchange, Producer, Queue, pools
class AMQP(object):
"""App AMQP API: app.amqp."""
Connection = Connection
Consumer = Consumer
Producer = Producer
#: compat alias to Connection
BrokerConnection = Connection
queues_cls = Queues
#: Cached and prepared routing table.
_rtable = None
#: Underlying producer pool instance automatically
#: set by the :attr:`producer_pool`.
_producer_pool = None
# Exchange class/function used when defining automatic queues.
# For example, you can use ``autoexchange = lambda n: None`` to use the
# AMQP default exchange: a shortcut to bypass routing
# and instead send directly to the queue named in the routing key.
autoexchange = None
#: Max size of positional argument representation used for
#: logging purposes.
argsrepr_maxsize = 1024
#: Max size of keyword argument representation used for logging purposes.
kwargsrepr_maxsize = 1024
def __init__(self, app):
self.app = app
self.task_protocols = {
1: self.as_task_v1,
2: self.as_task_v2,
}
整個接口調度邏輯
從視圖函數進來 的時候
定義的任務
視圖函數 task_add
Task.delay() --> apply_async --> send_task --> amqp.create_task_message --> amqp.send_task_message --> result=AsyncResult(task_id) 返回 result
delay 之後 調用實際上是 apply_async 之後 調用的send_task 之後開始創建任務,發送任務, 然後生成一個異步對象. 把這個結果返回.
4.2 celery 的入口
celery 啓動的worker 的入口 , __ main__.py 裏面 .
這裏 實際上是 celery.bin.celery 中的main 函數
打開文件 就會發現這個 main 函數
調用command.execute_from_commandline(argv)
def execute_from_commandline(self, argv=None):
argv = sys.argv if argv is None else argv
if 'multi' in argv[1:3]: # Issue 1008
self.respects_app_option = False
try:
sys.exit(determine_exit_status(
super(CeleryCommand, self).execute_from_commandline(argv)))
except KeyboardInterrupt:
sys.exit(EX_FAILURE)
調用 的是 celery.bin.base.Command 類的方法
self.setup_app_from_commandline 核心調用的是這個 方法
celery.bin.celery.CeleryCommand
def execute_from_commandline(self, argv=None):
"""Execute application from command-line.
Arguments:
argv (List[str]): The list of command-line arguments.
Defaults to ``sys.argv``.
"""
if argv is None:
argv = list(sys.argv)
# Should we load any special concurrency environment?
self.maybe_patch_concurrency(argv)
self.on_concurrency_setup()
# Dump version and exit if '--version' arg set.
self.early_version(argv)
try:
argv = self.setup_app_from_commandline(argv)
except ModuleNotFoundError as e:
self.on_error(UNABLE_TO_LOAD_APP_MODULE_NOT_FOUND.format(e.name))
return EX_FAILURE
except AttributeError as e:
msg = e.args[0].capitalize()
self.on_error(UNABLE_TO_LOAD_APP_APP_MISSING.format(msg))
return EX_FAILURE
self.prog_name = os.path.basename(argv[0])
return self.handle_argv(self.prog_name, argv[1:])
def setup_app_from_commandline(self, argv):
preload_options = self.parse_preload_options(argv)
quiet = preload_options.get('quiet')
if quiet is not None:
self.quiet = quiet
try:
self.no_color = preload_options['no_color']
except KeyError:
pass
workdir = preload_options.get('workdir')
if workdir:
os.chdir(workdir)
app = (preload_options.get('app') or
os.environ.get('CELERY_APP') or
self.app)
preload_loader = preload_options.get('loader')
if preload_loader:
# Default app takes loader from this env (Issue #1066).
os.environ['CELERY_LOADER'] = preload_loader
loader = (preload_loader,
os.environ.get('CELERY_LOADER') or
'default')
broker = preload_options.get('broker', None)
if broker:
os.environ['CELERY_BROKER_URL'] = broker
result_backend = preload_options.get('result_backend', None)
if result_backend:
os.environ['CELERY_RESULT_BACKEND'] = result_backend
config = preload_options.get('config')
if config:
os.environ['CELERY_CONFIG_MODULE'] = config
if self.respects_app_option:
if app:
self.app = self.find_app(app)
elif self.app is None:
self.app = self.get_app(loader=loader)
if self.enable_config_from_cmdline:
argv = self.process_cmdline_config(argv)
else:
self.app = Celery(fixups=[])
self._handle_user_preload_options(argv)
return argv
5 總結
本文簡單介紹了 celery 的基本的功能 , 以及celery 能夠處理的任務特點,以及可以和 flask 結合起來使用. 簡單分析了 celery 的工作機制 . 當然 如果想要深入瞭解 celery,可以 參考 celery的官方文檔.
6 參考鏈接
1 celery 文檔 http://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html
2 project layout http://docs.celeryproject.org/en/latest/getting-started/next-steps.html#project-layout
2-1 celery 的 配置介紹 http://docs.celeryproject.org/en/latest/userguide/configuration.html#configuration
3 可以設置任務的類型 http://docs.jinkan.org/docs/celery/_modules/celery/app/task.html#Task.apply_async
4 kombu Messaging library for Python https://kombu.readthedocs.io/en/stable/
4-1 kombu github 地址 https://github.com/celery/kombu
4-2 komub producer https://kombu.readthedocs.io/en/stable/userguide/producers.html
5 Celery 最佳實踐(轉) https://rookiefly.cn/detail/229
6 celery community http://www.celeryproject.org/community/
7 celery 通過 task_id 拿到任務的狀態 http://docs.celeryproject.org/en/master/faq.html#how-do-i-get-the-result-of-a-task-if-i-have-the-id-that-points-there
8 python celery 任務隊列 https://www.pyfdtic.com/2018/03/16/python-celery-%E4%BB%BB%E5%8A%A1%E9%98%9F%E5%88%97/
9 worker 相關 http://docs.celeryproject.org/en/latest/userguide/workers.html
10 Celery 簡介 http://docs.jinkan.org/docs/celery/getting-started/introduction.html