celery路由和交换的相关知识

0. 实际问题

假设我们拥有2个celery的task对象my_taskAmy_taskB,具体的两个函数逻辑如下:

@celery_app.task()
def my_taskA(a, b, c):
    print("doing my_taskA here...")
    time.sleep(0.01)
    return a + b + c


@celery_app.task()
def my_taskB(x, y):
    print("doing my_taskB here...")
    time.sleep(10)
    return x - y

默认来说,这两个任务都会在celery的默认队列(queue)‘default’中运行,那么存在这么一个问题,
如果同时添加两类任务中,消费的任务对象my_taskAmy_taskB并不会很快消耗完,那么很可能出现
一个现象,那就是很快这个队列中的任务都是my_taskB的任务了。

1. 如何处理?

一般来说,我们肯定想到的是,对于每个任务给它分配一个单独的broker,这样不久不会影响了,各自维护
各自的东西,事实上很多人也是这么做的。但这样,增加了维护的成本。试想一下,如果有20种任务,我们
难道要开辟20个broker去指定不同的任务吗?

但实际上,在阅读完celery的官方教程后,发现一个路由和交
换的概念。可以在一个broker上面开辟多个队列,每个队列绑定指定类型的任务,而对应的worker通过指定
的队列获取任务。这个概念就类系我们的双频路由器一样,2.4G的设备你们走常规的通道,5G的设备我有独享
的高速5G通道。完美!

2. 如何使用?

看到这里,接下来肯定是准备尝试了。但是说句实话,像我这样的白痴级别的任务,很难通过官方样例看懂,
所以我查询了一部分资料,然后实现了个demo。
先上目录结构:

untitled1
├── app.py
├── celeryconfig.py
└── send_task.py

做个简单说明:app.py实现具体的worker逻辑,send_task负责分发任务到队列中,
celeryconfig.py是基础配置。

  • app.py实现
#!/usr/bin/python2.7
# -*- coding: utf-8 -*-

import time

from celery import Celery
from celery.signals import task_postrun

celery_app = Celery('test_work', broker='amqp://sangfor:test@localhost/producer-vhost')
celery_app.config_from_object('celeryconfig')


@celery_app.task()
def my_taskA(a, b, c):
    print("doing my_taskA here...")
    time.sleep(0.01)
    return a + b + c


@celery_app.task()
def my_taskB(x, y):
    print("doing my_taskB here...")
    time.sleep(10)
    return x - y


@task_postrun.connect
def task_postrun_handler(sender=None, task_id=None, task=None, args=None, kwargs=None, retval=None, state=None, **kwds):
    """任务完成的信号处理函数"""
    print '''   Done!
    sender: %s
    task_id: %s
    task: %s
    retval: %s
    state: %s
    args:%s
    kwargs:%s
    kwds:%s''' % (sender, task_id, task, retval, state, args, kwargs, kwds,)
  • send_task.py的实现
#!/usr/bin/python2.7
# -*- coding: utf-8 -*-

import time
from celery import Celery

celery_app = Celery('send_work', broker='amqp://sangfor:test@localhost/producer-vhost')
# celery_app.config_from_object('celeryconfig')

celery_app.conf.update(
    CELERY_TASK_SERIALIZER='json',
    CELERY_RESULT_SERIALIZER='json',
    CELERY_ENABLE_UTC=True,
    CELERY_ROUTES={
        'app.my_taskA': {'queue': 'for_task_A'},
        'app.my_taskB': {'queue': 'for_task_B'},
    },
    CELERY_QUEUES={
        "for_task_A": {
            "exchange": "for_task_A"
        },
        "for_task_B": {
            "exchange": "for_task_B"
        }
    }
)


def run():
    while True:
        print 'Now send task A'
        celery_app.send_task('app.my_taskA', (1, 2, 3,))
        print 'Now send task B'
        celery_app.send_task('app.my_taskB', (9, 8))
        time.sleep(0.01)

if __name__ == '__main__':
    run()
  • celeryconfig.py的实现
#!/usr/bin/python2.7
# -*- coding: utf-8 -*-

CELERY_IMPORTS = ['app']

CELERY_ROUTES = {
    'app.my_taskA': {'queue': 'for_task_A'},
    'app.my_taskB': {'queue': 'for_task_B'},
}

CELERY_QUEUES = {
    "for_task_A": {
        "exchange": "for_task_A"
    },
    "for_task_B": {
        "exchange": "for_task_B"
    }
}

3.如何执行

  • celery -A app worker -n workerA -Q for_task_A
  • celery -A app worker -n workerA -Q for_task_B
  • python send_task.py
    注意,这里的-Q是精髓,为了指定执行特定通道中的任务。这个指令干啥的,自己--help吧。
    运行一下试试吧,其他的不解释了。

4.几点知识补充

1.Exchange有几类type:
(1) direct(default):

direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing
key完全匹配的Queue中。

例子:

CELERY_QUEUES = (
    Queue('for_adds',Exchange('for_adds',type='direct'), routing_key='adds'),
    Queue('for_send_emails', Exchange('for_adds',type='direct'), routing_key='email'),
    Queue('add', Exchange('for_adds',type='direct'), routing_key='add'),
)
CELERY_ROUTES = {
    'celery_test.tasks.add': {'exchange':'for_adds','routing_key':'add'},
    'celery_test.tasks.send_mail': {'exchange':'for_adds','routing_key':'email'},
    'celery_test.tasks.adds': {'exchange':'for_adds','routing_key':'add'},
}

(2)topic:

topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到
binding key与routing key相匹配的Queue中。可以看出和上面那个区别的地方,这里面不
是强匹配。它引入了两个通配符#*前者匹配多个单词(可以为0),后者匹配一个单词。

例子:

CELERY_QUEUES = (
    Queue('for_adds',Exchange('for_adds',type='topic'), routing_key='*.task.*'),
    Queue('for_send_emails', Exchange('for_adds',type='topic'), routing_key='*.*.email'),
    Queue('add', Exchange('for_adds',type='topic'), routing_key='*.add'),
)
CELERY_ROUTES = {
    'celery_test.tasks.add': {'exchange':'for_adds','routing_key':'q.task.email'},
    'celery_test.tasks.send_mail': {'exchange':'for_adds','routing_key':'a.task.e'},
    'celery_test.tasks.adds': {'exchange':'for_adds','routing_key':'b.add'},
}

(3)fanout:

fanout类型就是传说中的广播形式,它没有参数绑定,就是不需要指定上面的routing_key之类的东西,
只要和该交换绑定的queue,统统发送出去。类似于通过交换口,就广播发出。

例子:

CELERY_QUEUES = (
    Queue('for_adds',Exchange('for_adds',type='fanout')),
    Queue('for_send_emails', Exchange('for_adds',type='fanout')),
    Queue('add', Exchange('for_adds',type='fanout')),
)
CELERY_ROUTES = {
    'celery_test.tasks.add': {'exchange':'for_adds'},
    'celery_test.tasks.send_mail': {'exchange':'for_adds',},
    'celery_test.tasks.adds': {'exchange':'for_adds',},
}

(4)headers:

headers类型中,通过一个参数表(包含header和values(可选)),队列被绑定到交换
还有一个名为“x-match”的特殊参数确定两者之间的匹配算法,可以使用两个关键字进行表示。
其中“all”意味着一个AND(所有对必须匹配),“any”意味着OR(至少一对必须匹配)。
例子(google没找到合适的例子,自己看资料写了个,不一定对,有知道的欢迎指正):

CELERY_QUEUES = (
    Queue('for_adds',Exchange('for_adds',type='header', arguments = {'ham': 'good', 'x-match':'any'})),
)

5.参考文献

  1. Using pika to create headers exchanges with RabbitMQ in python
  2. RabbitMQ Exchanges, routing keys and bindings
  3. celery 、rabbitmq的exchange三种方式的实现

推荐阅读更多精彩内容

  • 序言第1章 并行和分布式计算介绍第2章 异步编程第3章 Python的并行计算第4章 Celery分布式应用第5章...
    SeanCheney阅读 12,040评论 3 35
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 132,654评论 18 139
  • 来源 RabbitMQ是用Erlang实现的一个高并发高可靠AMQP消息队列服务器。支持消息的持久化、事务、拥塞控...
    jiangmo阅读 10,071评论 2 34
  • 1.定义: Celery是一个异步的任务队列(也叫做分布式任务队列) 2.工作结构 Celery分为3个部...
    四号公园_2016阅读 28,429评论 5 61
  • 在学习Celery之前,我先简单的去了解了一下什么是生产者消费者模式。 生产者消费者模式 在实际的软件开发过程中,...
    c2db9ba35639阅读 3,331评论 0 8