celery 簡要概述

1 celery 簡要概述

Celery是一個簡單,靈活,可靠的分佈式系統,用於處理大量消息,同時爲操作提供維護此類系統所需的工具。

它是一個任務隊列,專注於實時處理,同時還支持任務調度。

celery 的優點

  1. 簡單:celery的 配置和使用還是比較簡單的, 非常容易使用和維護和不需要配置文件

  2. 高可用:當任務執行失敗或執行過程中發生連接中斷,celery 會自動嘗試重新執行任務

    如果連接丟失或發生故障,worker和client 將自動重試,並且一些代理通過主/主或主/副本複製方式支持HA。

  3. 快速:一個單進程的celery每分鐘可處理上百萬個任務

  4. 靈活: 幾乎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項目結構

image-20190613140106025

3.2 項目入口 文件 routes.py

image-20190613140011333

3.3 celery 實例創建,如何和flask綁定在一起呢

image-20190613140631927

說明 這裏 app.tasks 是在 app包下面創建的tasks 包

結構如下

image-20190613140854192

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()

celery_worker

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,
        }

整個接口調度邏輯

從視圖函數進來 的時候

定義的任務

image-20190613174558536

視圖函數 task_add

image-20190613174505155

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 裏面 .

image-20190614120039895

這裏 實際上是 celery.bin.celery 中的main 函數

image-20190614120153186

打開文件 就會發現這個 main 函數

image-20190614120300029

調用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

分享快樂,留住感動. '2019-07-29 22:34:23' --frank
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章