繁体   English   中英

如何将 Celery 与 asyncio 结合使用?

[英]How to combine Celery with asyncio?

如何创建一个使 celery 任务看起来像asyncio.Task的包装器? 或者有没有更好的方法将 Celery 与asyncio集成?

@asksol,Celery 的创建者

使用 Celery 作为异步 I/O 框架之上的分布式层是很常见的(重要提示:将 CPU 绑定任务路由到 prefork worker 意味着它们不会阻塞您的事件循环)。

但是我找不到任何专门针对asyncio框架的代码示例。

编辑:2021 年 1 月 12 日以前的答案(在底部找到)没有很好地老化,因此我添加了可能的解决方案组合,这些解决方案可能会满足那些仍然在寻找如何共同使用 asyncio 和 Celery 的人

让我们先快速分解用例(更深入的分析: 异步和协程 vs 任务队列):

  • 如果任务是 I/O 绑定的,那么使用协程和异步会更好。
  • 如果任务受 CPU 限制,那么使用 Celery 或其他类似的任务管理系统往往会更好。

因此,在 Python 的“做一件事并做好”的上下文中,不要尝试将 asyncio 和 celery 混合在一起是有意义的。

但是,如果我们希望能够以异步方式和异步任务的形式运行方法,会发生什么情况? 那么我们有一些选择要考虑:

  • 我能找到的最好的例子如下: https ://johnfraney.ca/posts/2018/12/20/writing-unit-tests-celery-tasks-async-functions/(我刚刚发现这是@Franey 的回应):

    1. 定义你的异步方法。

    2. 使用asgirefsync.async_to_sync模块来包装 async 方法并在 celery 任务中同步运行它:

       # tasks.py import asyncio from asgiref.sync import async_to_sync from celery import Celery app = Celery('async_test', broker='a_broker_url_goes_here') async def return_hello(): await asyncio.sleep(1) return 'hello' @app.task(name="sync_task") def sync_task(): async_to_sync(return_hello)()
  • 我在FastAPI应用程序中遇到的一个用例与前面的示例相反:

    1. 一个密集的 CPU 绑定进程正在占用异步端点。

    2. 解决方案是将异步 CPU 绑定进程重构为一个 celery 任务,并从 Celery 队列中传递一个任务实例以执行。

    3. 该案例可视化的最小示例:

       import asyncio import uvicorn from celery import Celery from fastapi import FastAPI app = FastAPI(title='Example') worker = Celery('worker', broker='a_broker_url_goes_here') @worker.task(name='cpu_boun') def cpu_bound_task(): # Does stuff but let's simplify it print([n for n in range(1000)]) @app.get('/calculate') async def calculate(): cpu_bound_task.delay() if __name__ == "__main__": uvicorn.run('main:app', host='0.0.0.0', port=8000)
  • 另一个解决方案似乎是@juanra@danius在他们的答案中提出的,但我们必须记住,当我们混合同步和异步执行时,性能往往会受到影响,因此在我们决定使用之前,这些答案需要监控他们在生产环境中。

最后,有一些现成的解决方案,我不能推荐(因为我自己没有使用过),但我会在这里列出它们:

  • Celery Pool AsyncIO似乎完全解决了 Celery 5.0 没有解决的问题,但请记住,它似乎有点实验性(今天 0.2.0 版 01/12/2021)
  • aiotasks声称是“一个类似于 Celery 的分发 Asyncio 协程的任务管理器”,但似乎有点陈旧(大约 2 年前的最新提交)

好吧,它的年龄没有那么好,是吗? Celery 5.0 版没有实现异步兼容性,因此我们不知道何时以及是否会实现......出于响应遗留原因(因为它是当时的答案)和评论继续,将其留在这里。

如官方网站所述,这可以从 Celery 5.0 版中实现:

http://docs.celeryproject.org/en/4.0/whatsnew-4.0.html#preface

  1. Celery 的下一个主要版本将仅支持 Python 3.5,我们计划在其中利用新的 asyncio 库。
  2. 放弃对 Python 2 的支持将使我们能够删除大量的兼容性代码,而使用 Python 3.5 使我们能够利用打字、异步/等待、异步和类似概念,在旧版本中没有其他选择。

以上内容来自上一个链接。

所以最好的办法就是等待5.0 版本发布!

与此同时,快乐的编码:)

这种简单的方法对我来说效果很好:

import asyncio
from celery import Celery

app = Celery('tasks')

async def async_function(param1, param2):
    # more async stuff...
    pass

@app.task(name='tasks.task_name', queue='queue_name')
def task_name(param1, param2):
    asyncio.run(async_function(param1, param2))

您可以使用run_in_executor将任何阻塞调用包装到任务中,如文档中所述,我还在示例中添加了自定义超时

def run_async_task(
    target,
    *args,
    timeout = 60,
    **keywords
) -> Future:
    loop = asyncio.get_event_loop()
    return asyncio.wait_for(
        loop.run_in_executor(
            executor,
            functools.partial(target, *args, **keywords)
        ),
        timeout=timeout,
        loop=loop
    )
loop = asyncio.get_event_loop()
async_result = loop.run_until_complete(
    run_async_task, your_task.delay, some_arg, some_karg="" 
)
result = loop.run_until_complete(
    run_async_task, async_result.result 
)

这是一个简单的助手,您可以使用它来使 Celery 任务可等待:

import asyncio
from asgiref.sync import sync_to_async

# Converts a Celery tasks to an async function
def task_to_async(task):
    async def wrapper(*args, **kwargs):
        delay = 0.1
        async_result = await sync_to_async(task.delay)(*args, **kwargs)
        while not async_result.ready():
            await asyncio.sleep(delay)
            delay = min(delay * 1.5, 2)  # exponential backoff, max 2 seconds
        return async_result.get()
    return wrapper

sync_to_async一样,它可以用作直接包装器:

@shared_task
def get_answer():
    sleep(10) # simulate long computation
    return 42    

result = await task_to_async(get_answer)()

...作为装饰者:

@task_to_async
@shared_task
def get_answer():
    sleep(10) # simulate long computation
    return 42    

result = await get_answer()

当然,这不是一个完美的解决方案,因为它依赖于polling 但是,在Celery 官方提供更好的解决方案之前,从 Django 异步视图调用 Celery 任务应该是一个很好的解决方法。

编辑 2021/03/02:添加了对sync_to_async的调用以支持急切模式

我发现这样做的最干净的方法是将async函数包装在asgiref.sync.async_to_sync (来自asgiref ):

from asgiref.sync import async_to_sync
from celery.task import periodic_task


async def return_hello():
    await sleep(1)
    return 'hello'


@periodic_task(
    run_every=2,
    name='return_hello',
)
def task_return_hello():
    async_to_sync(return_hello)()

我从我写的一篇博客文章中提取了这个例子。

我通过在celery-pool-asyncio库中结合 Celery 和 asyncio 解决了问题。

使用 asyncio 实现 Celery 的好方法:

import asyncio
from celery import Celery

app = Celery()

async def async_function(param):
    print('do something')

@app.task()
def celery_task(param):
    loop = asyncio.get_event_loop()
    return loop.run_until_complete(async_function(param))

这是我在必要时处理异步协程的 Celery 实现:

包装 Celery 类以扩展其功能:

from celery import Celery
from inspect import isawaitable
import asyncio


class AsyncCelery(Celery):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.patch_task()

        if 'app' in kwargs:
            self.init_app(kwargs['app'])

    def patch_task(self):
        TaskBase = self.Task

        class ContextTask(TaskBase):
            abstract = True

            async def _run(self, *args, **kwargs):
                result = TaskBase.__call__(self, *args, **kwargs)
                if isawaitable(result):
                    await result

            def __call__(self, *args, **kwargs):
                asyncio.run(self._run(*args, **kwargs))

        self.Task = ContextTask

    def init_app(self, app):
        self.app = app

        conf = {}
        for key in app.config.keys():
            if key[0:7] == 'CELERY_':
                conf[key[7:].lower()] = app.config[key]

        if 'broker_transport_options' not in conf and conf.get('broker_url', '')[0:4] == 'sqs:':
            conf['broker_transport_options'] = {'region': 'eu-west-1'}

        self.config_from_object(conf)


celery = AsyncCelery()

最简单的方法(恕我直言)对我有用,使用的 python 版本是3.9.6

TL,DR:在不同的线程中永远运行 asyncio eventloop,为每个 celery 工作线程启动 id 和停止。 通过异步任务分配可等待的东西。 阻塞等待 Future API 执行异步任务

import asyncio
from threading import Thread
from celery import Celery

app = Celery("workers", backend="rpc://", broker="pyamqp://guest:guest@localhost/")


# 1. define new thread,  loop which would be launched in new thread


def thread_function(loop: asyncio.AbstractEventLoop):
    asyncio.set_event_loop(loop)
    loop.run_forever()


loop = asyncio.get_event_loop()
thread = Thread(target=thread_function, args=(loop,))
# NOTE: it's not started yet

# 2. define your async infinite tasks (optional)

# this is async iteration example
async def my_iterate_async_infinite():
    count = 0
    while True:
        await asyncio.sleep(1)
        print(f"async iteration {count=}")
        count += 1


# 3. subscribe for celery worker signals to start and stop thread

from celery.signals import worker_process_init, worker_process_shutdown


@worker_process_init.connect
def handle_worker_process_init(sender, **_):
    thread.start()
    asyncio.run_coroutine_threadsafe(my_iterate_async_infinite(), loop)


@worker_process_shutdown.connect
def handle(sender, **_):
    # uncomment and use if you need async teardown
    # asyncio.run_coroutine_threadsafe(example_call_async_teardown_function(), loop).result()

    loop.stop()
    thread.join()


# 4. call async function and wait for it's result, within task


@app.task()
def my_task():
    print("Call async function within task")

    async def do_async_func():
        await asyncio.sleep(1)
        return "my_async_result"

    res = asyncio.run_coroutine_threadsafe(do_async_func(), loop=loop).result()
    print(f"{res=}")  # would be "me_async_result"

对于偶然发现此问题的任何人,特别是在异步sqlalchemy (即使用asyncio扩展)和 Celery 任务方面寻求帮助,明确处置引擎将解决问题。 这个特定的示例与asyncpg一起使用。

例子:

from sqlalchemy.ext.asyncio import (
    AsyncSession,
    create_async_engine,
)
from sqlalchemy.orm import sessionmaker
from asgiref.sync import async_to_sync


engine = create_async_engine("some_uri", future=True)
async_session_factory = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)


@celery_app.task(name="task-name")
def sync_func() -> None:
    async_to_sync(some_func)()


async def some_func() -> None:
    async with get_db_session() as session:
        result = await some_db_query(session)
    # engine.dispose will be called on exit


@contextlib.asynccontextmanager
async def get_db_session() -> AsyncGenerator:
    try:
        db = async_session_factory()
        yield db
    finally:
        await db.close()
        await engine.dispose()

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM