接上一篇文章:
http://john88wang.blog.51cto.com/2165294/1670904
Work Queues
在上一篇文章中,send.py程序向名为hello的队列发送消息,receive.py程序向名为hello的队列接收消息。这一节中,我们将创建一个Work Queue用于将那些比较耗时的任务分布到多个worker上。
Work Queues工作队列或者叫做Task Queues任务队列的主要概念就是为了避免立刻执行一个耗费资源的任务并且不得不等待它执行完成。取而代之的是,我们将这个任务调度到以后去执行。
我们封装一个任务为一个消息并发送这个消息到队列。一个work process工作进程会在后台运行,将从这个队列中取出任务最后执行。当运行多个workers时,任务将在这些workers间共享。
这个概念特别是对于那些需要在一个简短的HTTP请求窗口时间内处理复杂的任务时有用。
将之前的send.py更名为new_task.py并添加一些代码
import sys message = ' '.join(sys.argv[1:]) or "Hello World!" channel.basic_publish(exchange='', routing_key='hello', body=message) print " [x] Sent %r" % (message,)
代码更好后可以传递任意参数,如果没有参数则默认输出Hello World!
之前的receive.py更名为worker.py
import time def callback(ch, method, properties, body): print " [x] Received %r" % (body,) time.sleep( body.count('.') ) print " [x] Done"
Round-robin dispatching
使用任务队列Task Queues的其中一个好处就是可以很容易地实现paralleise work并行工作。如果有一堆积压的工作任务需要处理,可以增加更多的worker进行扩展
打开两个窗口运行worker.py,打开一个窗口运行new_task.py
shell1$ python worker.py [*] Waiting for messages. To exit press CTRL+C
shell2$ python worker.py [*] Waiting for messages. To exit press CTRL+C
shell3$ python new_task.py First message. shell3$ python new_task.py Second message.. shell3$ python new_task.py Third message... shell3$ python new_task.py Fourth message.... shell3$ python new_task.py Fifth message.....
shell1$ python worker.py [*] Waiting for messages. To exit press CTRL+C [x] Received 'First message.' [x] Received 'Third message...' [x] Received 'Fifth message.....'
shell1$ python worker.py [*] Waiting for messages. To exit press CTRL+C [x] Received 'First message.' [x] Received 'Third message...' [x] Received 'Fifth message.....'
默认情况下,RabbitMQ将依次顺序发送每条消息到下个consumer消费者,平均上,每个consumer收到的消息数量相同,这种分布消息的方式就叫做轮训,Round-robin。
Message acknowledgment
执行一个任务将花费时间,使用当前的代码RabbitMQ传递一条消息给消费者后立刻从内存中移除这条消息。在这种情况下,如果杀死一个worker,我们将会丢失这个worker正在处理的这条消息。我们也会丢失所有分发到这个worker还没有被处理的所有的消息
但是我们不想丢失任何任务,如果一个worker死掉,我们希望这个任务可以被传送到另一个worker。
为了确保一条消息永不丢失,RabbitMQ支持消息确认message acknowledgment.
一条特定的消息如果已经被消费者接收并处理,消费者会发送一个ack到RabbitMQ,然后RabbitMQ将会删除这条消息。
如果消费者死掉时没有返回ack,RabbitMQ会明白一条消息没有被完整地处理,它将会传递这条消息到另一个消费者。这种方式下,可以确保没有消息丢失,即使worker偶尔死掉。
没有任何消息超时设置,RabbitMQ只会当woker连接死掉时才重新投递消息。
消息确认默认被开启。
def callback(ch, method, properties, body): print " [x] Received %r" % (body,) time.sleep( body.count('.') ) print " [x] Done" ch.basic_ack(delivery_tag = method.delivery_tag) channel.basic_consume(callback, queue='hello')
Message durability
前面已经了解到即使consumer死掉如何确保任务不丢失。但是如果RabbitMQ停止,任务还是会丢失。
当RabbitMQ推出或者崩溃,如果不告诉它,它会忘记存储队列和消息。需要做两件事来确保消息不丢失。需要标记队列和消息都为持久的durable.
首先,需要确保RabbitMQ不会丢失队列
channel.queue_declare(queue='hello', durable=True)
由于在之前的程序中已经定义过hello队列,RabbitMQ不允许以不同的参数来重新定义同一个队列
所以改个名字重新定义
new_task.py和woker.py都需要重新定义
channel.queue_declare(queue='task_queue', durable=True)
然后是标记消息可持久化,添加一个delivery_mode=2
channel.basic_publish(exchange='', routing_key="task_queue", body=message, properties=pika.BasicProperties( delivery_mode = 2, # make message persistent ))
Fair dispatch
通过上面的分析,我们发现Round-robin的消息分发方式还不足以满足我们的需求,举个例子,在一种情况下,我们有两个worker,奇数编号的消息是些繁重的任务,偶数编号的消息是些轻量的任务。这样的话,一个worker就会一直很繁忙,另一个worker几乎不会处理一些繁重的任务。RabbitMQ不会知道这些,仍然继续平均地分发消息到这两个worker上
发生这种情况是因为RabbitMQ只是当一条消息进入队列时分发这条消息。它不去查看一个消费者还没有被确认的消息的数量。
为了解决这个问题,可以使用basic_qos 带上 prefetch_count=1设置,这样告诉RabbitMQ不要一次给一个worker多余1个的消息。或者,用另外的话说就是,不要分发一条新的消息给一个worker直到这个worker已经处理完并确认好之前的那条消息。
channel.basic_qos(prefetch_count=1)
Note about the queue siz 注意一下队列大小
如果所有的worker都很繁忙,队列很容易被塞满,需要关注下队列长度,添加更多的worker或者采取其他优化措施
new_task.py完整代码
#!/usr/bin/python import pika import sys connection=pika.BlockingConnection(pika.ConnectionParameters(host='localhost')) channel=connection.channel() channel.queue_declare(queue='task_queue',durable=True) message=' '.join(sys.argv[1:]) or "Hello World!" channel.basic_publish(exchange='', routing_key='task_queue', body=message, properties=pika.BasicProperties(delivery_mode=2,)) print "[x] Sent %r" % (message,) connection.close()
worker.py完整代码
#!/usr/bin/python import pika import time connection=pika.BlockingConnection(pika.ConnectionParameters(host='localhost')) channel=connection.channel() channel.queue_declare(queue='task_queue',durable=True) print '[*] Waiting for messages. To exit press CTRL+C' def callback(ch,method,properties,body): print "[x] Received %r" % (body,) time.sleep(body.count('.')) print "[x] Done" ch.basic_ack(delivery_tag=method.delivery_tag) channel.basic_qos(prefetch_count=1) channel.basic_consume(callback,queue='task_queue') channel.start_consuming()
参考资料:
http://previous.rabbitmq.com/v3_3_x/tutorials/tutorial-two-python.html