python系列之 RabbitMQ - work queues

本节我们创建一个工作队列( work queue )用来在多个workers之间分发消息
工作队列(又名:任务队列)的主要思想是避免在资源密集型的任务处理中不得不等待它的完成,相反,我们安排这个任务稍后完成。我们把这任务作为一个消息封装起来并发送到一个队列中,一个后台工作进程将这个任务取出并最终执行这个任务,当你运行多个任务时,多个消费者将共享这些任务。
这个概念在网页应用中对于在HTTP短连接请求中处理复杂任务时尤其有用。

预备

前面的部分我们发送了一个消息内容“hello world", 现在我们要发送复杂任务的字符串。我们没有真实世界的任务,比如重新调整一个图片大小或者渲染一个PDF文件,我们通过time.sleep()函数假装消息接收后任务非常繁忙,需要消耗一定的时间,我们通过字符串中小数点的个数来描述任务的复杂性,每个点代表“work"要耗费1秒,例如:假设一个任务描述 "Hello..." 将要耗费3秒钟。

我们修改之前的 send.py 代码,允许通过命令行来发送任意消息。这个程序将要处理任务到工作队列。我们命名为 new_task.py
import pika
import sys

message = ' '.join(sys.argv[1:]) or "Hello World"
channel.basic_publish(exchange='',
                      routing_key='worker',
                      body=message,
                      properties=pika.BasicProperties(delivery_mode = 2,)
                      )
print(" [x] Send %r " % message)

之前老的 receive.py 脚本也需要一些改变,我们对处理模块 callback 函数进行一些修改:它假装对消息中的每个小数点需要1秒时间进行处理,它将会从消息队列中pop一个消息然后执行任务,我们用 worker.py 来命名这个文件

import time

def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)
    time.sleep(body.count(b'.'))
    print(" [x] Done")
    ch.basic_ack(delivery_tag = method.delivery_tag)

循环调度(Round-robin dispatching)

使用任务队列(tack queue)的优点是很容易的进行并行工作的能力,如果我们的工作队列产生一定的积压,我们可以创建多个worker来接收并处理消息,这样很容易扩展
首先,我们试着同时运行两个worker.py 脚本,它们都可以从消息队列中获取消息,你需要开启两个终端,运行两个 worker.py , 当做两个Consumer: C1 和 C2
shell1$ python worker.py
 [*] Waiting for messages. To exit press CTRL+C

shell2$ python worker.py
 [*] Waiting for messages. To exit press CTRL+C

再打开一个终端,运行 new_task.py ,执行多个任务
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.....

让我们看看两个worker端接收的消息:
shell1$ python worker.py
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'First message.'
 [x] Received 'Third message...'
 [x] Received 'Fifth message.....'

shell2$ python worker.py
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'Second message..'
 [x] Received 'Fourth message....'

默认,RabbitMQ将循环的发送每个消息到下一个Consumer , 平均每个Consumer都会收到同样数量的消息。 这种分发消息的方式成为 循环调度(round-robin)

上述完整代码
new_task.py
import pika
import sys

connec = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connec.channel()

channel.queue_declare(queue='worker')

message = ' '.join(sys.argv[1:]) or "Hello World"
channel.basic_publish(exchange='',
                      routing_key='worker',
                      body=message,
                      properties=pika.BasicProperties(delivery_mode = 2,)
                      )
print(" [x] Send %r " % message)

worker.py
import time
import pika

connect = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connect.channel()

channel.queue_declare('worker')

def callback(ch, method, properties,body):
    print(" [x] Received %r" % body)
    time.sleep(body.count(b'.'))
    print(" [x] Done")
    ch.basic_ack(delivery_tag = method.delivery_tag)

channel.basic_consume(callback,
                      queue='worker',
                      )
channel.start_consuming()


消息确认(Message acknowledgment)


执行一个任务能消耗几秒. 你可能想知道当一个consumer在执行一个艰巨任务或执行到一半是死掉了会发生什么。就我们当前的代码而言,一旦RabbitMQ 的分发完消息给 consumer后 就立即从内存中移除该消息。这样的话,如果一个worker刚启动你就结束掉,那么消息就丢失了。那么所有发送给这个 worker 的还没有处理完成的消息也将丢失。
但是我们不想丢失任何任务,如果worker死掉了,我们希望这个任务能够发送给其它的worker
为了确保一个消息不会丢失,RabbitMQ支持消息的 acknowlegements , 一个 ack(nowlegement) 是从consumer端发送一个回执去告诉RabbitMQ 消息已经接收了、处理了,RabbitMQ可以释放并删除掉了。
如果一个consumer 死掉了(channel关闭、connection关闭、或者TCP连接断开了)而没有发送ack,RabbitMQ 就会知道这个消息没有被完全处理并会重新发送到消息队列中,如果同时有另外一个consumer在线,将会很快转发到另外一个consumer中。 那样的话你就能确保虽然worker死掉,但消息不会丢失。
这个是没有超时的,当消费方(consumer)死掉后RabbitMQ会重新转发消息,即使处理这个消息需要很长很长时间也没有问题
消息的 acknowlegments 默认是打开的,在前面的例子中关闭了: no_ack = True . 现在删除这个标识 然后 发送一个 acknowledgment。
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')

使用这个代码我们能确保即使在程序运行中使用CTRL+C结束worker进程也不会有消息丢失。之后当worker死掉之后所有未确认的消息将会重新进行转发。

忘了 acknowlegement
 忘记设置basic_ack是一个经常犯也很容易犯的错误,但后果是很严重的。当客户端退出后消息将会重新转发,但RabbitMQ会因为不能释放那些没有回复的消息而消耗越来越多的内存
为了调试(debug)这种类型的错误,你可以使用 rabbitmqctl 打印 message_unacknowledged 字段:
$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello    0       0
...done

消息持久化(Message durability)

我们已经学习了即使客户端死掉了任务也不会丢失。但是如果RabbitMQ服务停止了的话,我们的任务还是会丢失。
当RabbitMQ退出或宕掉的话将会丢失queues和消息信息,除非你进行设置告诉服务器队列不能丢失。要确保消息不会丢失需要做两件事: 我们需要将队列和消息标记为 durable
首先:
我们需要确保RabbitMQ 永远不会丢失队列,为了确保这个,我们需要定义队列为durable:
channel.queue_declare(queue='hello', durable=True

尽管此命令本身定义是正确的,但我们设置后还是不会工作。因为我们已经定义了个名为 hello ,但不是durable属性的队列。RabbitMQ不允许你重新定义一个已经存在、但属性不同的queue。RabbitMQ 将会给定义这个属性的程序返回一个错误。但这里有一个快速的解决方法:让我们定义个不同名称的队列,比如 task_queue:
channel.queue_declare(queue='task_queue', durable=True)

这个 queue_declare 需要在 生产者(producer) 和消费方(consumer) 代码中都进行设置。
基于这一点, 我们能够确保 task_queue 队列即使RabbitMQ重启也不会丢失

现在我们需要标记我们的消息为持久化的 - 通过设置 delivery_mode 属性为 2
channel.basic_publish(exchange='',
                      routing_key="task_queue",
                      body=message,
                      properties=pika.BasicProperties(
                         delivery_mode = 2, # make message persistent
                      ))

消息持久化的注意点
标记消息为持久化的并不能完全保证消息不会丢失,尽管告诉RabbitMQ保存消息到磁盘,当RabbitMQ接收到消息还没有保存的时候仍然有一个短暂的时间窗口. RabbitMQ不会对每个消息都执行同步fsync(2) --- 可能只是保存到缓存cache还没有写入到磁盘中,这个持久化保证不是很强,但这比我们简单的任务queue要好很多,如果你想很强的保证你可以使用 publisher confirms

公平调度(Fair dispatch)

你可能已经注意到分发仍然不能完全符合我们想要进行的工作。比如有两个worker的一种情况,当所有基数的消息比较重要,偶数的消息相对不重要,一个worker相对处理比较繁忙而另一个几乎不怎么工作。但是对于RabbitMQ而言,它对此一无所知并仍然均匀的分发消息。
发生这样的情况是由于RabbitMQ只是当消息来是进行分发,它并不考虑消费方(consuer)回复的ack消息,它只是一味地分发每个消息到各个消费方

为了解决这个问题我们可以使用 basic.qos 方法使用 prefetch_count = 1 设置, 这样告诉RabbitMQ不要同时将多条消息分发到一个worker, 换句话说,在一个worker未处理完之前的消息之前不要分发新的消息给它。 换言之,会将这个消息分发给另一个不是很忙的worker进行处理。
channel.basic_qos(prefetch_count=1)

代码汇总

new_task.py 脚本的全部代码为:
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()

new_task.py 脚本
#!/usr/bin/env 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(b'.'))
    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://www.rabbitmq.com/tutorials/tutorial-two-python.html
展开阅读全文

没有更多推荐了,返回首页