python系列之 RabbitMQ - work queues

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

预备

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

我们修改之前的 send.py 代码,允许通过命令行来发送任意消息。这个程序将要处理任务到工作队列。我们命名为 new_task.py
import pikaimport sysmessage = ' '.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 timedef 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 pikaimport sysconnec = 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 timeimport pikaconnect = 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_unacknowledgedListing 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 pikaimport sysconnection = 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 pythonimport pikaimport timeconnection = 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
0 0
原创粉丝点击