RabbitMQ+Celery
最近用到RabbitMQ+Celery框架进行开发分布式任务队列,这里进行一下整理总结,菜鸟欢迎批评指正。
RabbitMQ
一些术语
- 官网:http://www.rabbitmq.com/
- RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。A
- MQ:消息队列(Message Queue),应用程序A可以给应用程序B发送消息,能够有效解耦,支持高并发,支持异步操作等等。
- AMQP:Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。
- 生产者:消息的生产者,将消息发送到MQ
- 消费者:消息的消费者,从MQ接收消息并进行处理
- Exchange:交换器,将生产者的消息根据规则路由到不同的队列以便不同的消费者拿到对应的消息。
- RabbitMQ常用的Exchange Type有三种:fanout、direct、topic。
fanout:把所有发送到该Exchange的消息投递到所有与它绑定的队列中,广播。
direct:把消息投递到那些binding key与routing key完全匹配的队列中。
topic:将消息路由到binding key与routing key模式匹配的队列中。
- RabbitMQ支持消息确认,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。
- RabbitMQ支持消息持久化,可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(可以通过事务解决)。
- RabbitMQ支持RPC。
Demo
官网上有教程,包括简单队列,worker模式,Publish/Subscribe模式,Routing模式,Topics模式。
简单队列中,producer和consumer直接指定routing key 为queue,这种模式一般不用。
worker模式中,和上面比较像,只是消费者变为多个。这种模式下一条消息只能被一个消费者消费,消费快的消费者会消费更多的消息。
Publish/Subscribe模式中,需要指定exchange type为fanout,一条消息会被所有的消费者消费。
Routing模式中,指定exchange type为direct,同时需要指定routing key好指定路由到哪个队列上。
Topics模式中,在Routing基础上,将routing key可以设置为模糊匹配的模式。*可以匹配任一个单词,#可以匹配零个或多个单词。如test.*可以匹配test.a, test.b等。
这里只附上topics模式的demo,详细可登陆官网。
producer.py
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs',
exchange_type='topic')
routing_key = 'test.v' # *.a consumer1能收到,test.*consumer2能收到,test.a两者都能收到
message = '%s Hello World' % routing_key
channel.basic_publish(exchange='topic_logs',
routing_key=routing_key,
body=message)
print(" [x] Sent %r:%r" % (routing_key, message))
connection.close()
consumer1.py
import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost')) channel = connection.channel() channel.exchange_declare(exchange='topic_logs', exchange_type='topic') queue_name = 'test_topic_a' channel.queue_declare(queue=queue_name, durable=True) binding_keys = ['*.a'] for binding_key in binding_keys: channel.queue_bind(exchange='topic_logs', queue=queue_name, routing_key=binding_key) def callback(ch, method, properties, body): print("%s:%s" % (method.routing_key, body)) channel.basic_consume(callback, queue=queue_name, no_ack=True) channel.start_consuming()
在consumer2.py中,将queue_name = 'test_topic_b',binding_keys = ['test.*']即可。
Celery
Celery是一个简单,灵活,可靠的分布式系统,用于处理大量消息,同时为操作提供维护此类系统所需的工具。Celery是一个任务队列,专注于实时处理,同时还支持任务调度。
Celery的架构由三部分组成,消息中间件(message broker),任务执行单元(worker)和任务执行结果存储(task result store)组成。Celery作为MQ的消费者,根据接收的消息进行任务调度,同时执行任务的结果可以进行存储。如图:
Demo
这里使用kombu消息框架。
producer.py
# coding:utf8 import datetime from uuid import uuid4 from kombu import Connection, Exchange # RabbitMQ Config MQ_HOST = 'localhost' MQ_USER = 'guest' MQ_PASSWORD = 'guest' MQ_PORT = 5672 VIRTUAL_HOST = '/' EXCHANGE_NAME = 'test_celery' routing_key = 'celery.test' queue_name = 'test_celery_work_queue' message = 'time now is %s' % datetime.datetime.now() method_name = 'test_work' _url = 'amqp://{0}:{1}@{2}:{3}/{4}'.format(MQ_USER, MQ_PASSWORD, MQ_HOST, MQ_PORT, VIRTUAL_HOST) with Connection(_url) as conn: producer = conn.Producer(serializer='json') body_dict = {"args": [message], "id": str(uuid4()), "task": method_name # 要调用的work方法 } producer.publish( body=body_dict, routing_key=routing_key, exchange=Exchange(EXCHANGE_NAME, 'topic'), retry=True, )
task.py
# coding:utf8 import os from celery import Celery from kombu import Queue, Exchange # RabbitMQ config RABBITMQ_HOSTS = "localhost" RABBITMQ_PORT = 5672 RABBITMQ_VHOST = '/' RABBITMQ_USER = 'guest' RABBITMQ_PWD = 'guest' # celery config CELERY_IMPORTS = ('task',) BROKER_URL = 'amqp://%s:%s@%s:%d/%s' % (RABBITMQ_USER, RABBITMQ_PWD, RABBITMQ_HOSTS, RABBITMQ_PORT, RABBITMQ_VHOST) CELERY_TRACK_STARTED = True CELERYD_CONCURRENCY = 4 # 配置会启用执行任务的线程数 CELERYD_PREFETCH_MULTIPLIER = 1 # how many messages a time CELERY_TASK_RESULT_EXPIRES = 60 CELERYD_HIJACK_ROOT_LOGGER = False CELERY_DEFAULT_QUEUE = 'DEFAULT_QUEUE' # 只要配置好路由器和队列,使得celery能够接收到消息,其会根据方法签名自行选择方法进行执行 CELERY_QUEUES = ( Queue('test_celery_work_queue', Exchange('test_celery', type='topic'), routing_key='celery.test'), ) celery = Celery("task", broker=BROKER_URL) @celery.task(name="test_work", ignore_result=True, serializer="json") def ipa_unpack(message): print 'receive', message print 'doing my work!' if __name__ == "__main__": os.system( # 在工作目录下使用命令行执行 "python -m celery worker -A task -Q test_celery_work_queue --without-gossip --without-mingle --without-heartbeat -l INFO")
输出:
[2018-08-05 15:30:15,813: INFO/MainProcess] Received task: test_work[ef2cb7b0-ebae-4887-b329-de4c63806336]
[2018-08-05 15:30:15,815: WARNING/Worker-1] receive
[2018-08-05 15:30:15,815: WARNING/Worker-1] time now is 2018-08-05 15:30:15.759000
[2018-08-05 15:30:15,815: WARNING/Worker-1] doing my work!
[2018-08-05 15:30:15,822: INFO/MainProcess] Task test_work[ef2cb7b0-ebae-4887-b329-de4c63806336] succeeded in 0.0090000629425s: None