当有Consumer需要大量的运算时,RabbitMQ Server需要一定的分发机制来balance每个Consumer的load。试想一下,对于web application来说,在一个很多的HTTP request里是没有时间来处理复杂的运算的,只能通过后台的一些工作线程来完成。接下来我们分布讲解。

  应用场景就是RabbitMQ Server会将queue的Message分发给不同的Consumer以处理计算密集型的任务:

1. 准备

  在上一篇文章中,我们简单在Message中包含了一个字符串"Hello World"。现在为了是Consumer做的是计算密集型的工作,那就不能简单的字符串了。在现实应用中,Consumer有可能做的是一个图片的resize,或者是pdf文件的渲染或者内容提取。但是作为Demo,还是用字符串模拟吧:通过字符串中的.的数量来决定计算的复杂度,每个.都会消耗1s,即sleep(1)。

  还是复用上篇文章中的code,根据“计算密集型”做一下简单的修改,为了辨别,我们把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,)

  同样的道理,把receive.py的名字换成worker.py,并且根据Message中的.的数量进行计算密集型模拟:

import time

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

2. Round-robin dispatching 循环分发

  RabbitMQ的分发机制非常适合扩展,而且它是专门为并发程序设计的。如果现在load加重,那么只需要创建更多的Consumer来进行任务处理即可。当然了,对于负载还要加大怎么办?我没有遇到过这种情况,那就可以创建多个virtual Host,细化不同的通信类别了。

  首先开启两个Consumer,即运行两个worker.py。

Console1:

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

Consule2:

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

Producer new_task.py要Publish Message了:

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.....

注意一下:.代表的sleep(1)。接着开一下Consumer worker.py收到了什么:

Console1:

shell1$ python worker.py
[*] Waiting for messages. To exit press CTRL+C
[x] Received 'First message.'
[x] Received 'Third message...'
[x] Received 'Fifth message.....'

Console2:

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

默认情况下,RabbitMQ 会顺序的分发每个Message。当每个收到ack后,会将该Message删除,然后将下一个Message分发到下一个Consumer。这种分发方式叫做round-robin。这种分发还有问题,接着向下读吧。

3. Message acknowledgment 消息确认

  每个Consumer可能需要一段时间才能处理完收到的数据。如果在这个过程中,Consumer出错了,异常退出了,而数据还没有处理完成,那么非常不幸,这段数据就丢失了。因为我们采用no-ack的方式进行确认,也就是说,每次Consumer接到数据后,而不管是否处理完成,RabbitMQ Server会立即把这个Message标记为完成,然后从queue中删除了。

  如果一个Consumer异常退出了,它处理的数据能够被另外的Consumer处理,这样数据在这种情况下就不会丢失了(注意是这种情况下)。

  为了保证数据不被丢失,RabbitMQ支持消息确认机制,即acknowledgments。为了保证数据能被正确处理而不仅仅是被Consumer收到,那么我们不能采用no-ack。而应该是在处理完数据后发送ack。

  在处理数据后发送的ack,就是告诉RabbitMQ数据已经被接收,处理完成,RabbitMQ可以去安全的删除它了。

  如果Consumer退出了但是没有发送ack,那么RabbitMQ就会把这个Message发送到下一个Consumer。这样就保证了在Consumer异常退出的情况下数据也不会丢失。

  这里并没有用到超时机制。RabbitMQ仅仅通过Consumer的连接中断来确认该Message并没有被正确处理。也就是说,RabbitMQ给了Consumer足够长的时间来做数据处理。

  默认情况下,消息确认是打开的(enabled)。在上篇文章中我们通过no_ack = True 关闭了ack。重新修改一下callback,以在消息处理完成后发送ack:

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')

  这样即使你通过Ctr-C中断了worker.py,那么Message也不会丢失了,它会被分发到下一个Consumer。

  如果忘记了ack,那么后果很严重。当Consumer退出时,Message会重新分发。然后RabbitMQ会占用越来越多的内存,由于RabbitMQ会长时间运行,因此这个“内存泄漏”是致命的。去调试这种错误,可以通过一下命令打印un-acked Messages:

$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello 0 0
...done.

4. Message durability消息持久化

  在上一节中我们知道了即使Consumer异常退出,Message也不会丢失。但是如果RabbitMQ Server退出呢?软件都有bug,即使RabbitMQ Server是完美毫无bug的(当然这是不可能的,是软件就有bug,没有bug的那不叫软件),它还是有可能退出的:被其它软件影响,或者系统重启了,系统panic了。。。

为了保证在RabbitMQ退出或者crash了数据仍没有丢失,需要将queue和Message都要持久化。

queue的持久化需要在声明时指定durable=True:

channel.queue_declare(queue='hello', durable=True)

上述语句执行不会有什么错误,但是确得不到我们想要的结果,原因就是RabbitMQ Server已经维护了一个叫hello的queue,那么上述执行不会有任何的作用,也就是hello的任何属性都不会被影响。这一点在上篇文章也讨论过

那么workaround也很简单,声明一个另外的名字的queue,比如名字定位task_queue:

channel.queue_declare(queue='task_queue', durable=True)

再次强调,Producer和Consumer都应该去创建这个queue,尽管只有一个地方的创建是真正起作用的:

接下来,需要持久化Message,即在Publish的时候指定一个properties,方式如下:

channel.basic_publish(exchange='',
routing_key="task_queue",
body=message,
properties=pika.BasicProperties(
delivery_mode = 2, # make message persistent
))

关于持久化的进一步讨论:

为了数据不丢失,我们采用了:

  1. 在数据处理结束后发送ack,这样RabbitMQ Server会认为Message Deliver 成功。
  2. 持久化queue,可以防止RabbitMQ Server 重启或者crash引起的数据丢失。
  3. 持久化Message,理由同上。

但是这样能保证数据100%不丢失吗?

  答案是否定的。问题就在与RabbitMQ需要时间去把这些信息存到磁盘上,这个time window虽然短,但是它的确还是有。在这个时间窗口内如果数据没有保存,数据还会丢失。还有另一个原因就是RabbitMQ并不是为每个Message都做fsync:它可能仅仅是把它保存到Cache里,还没来得及保存到物理磁盘上。

因此这个持久化还是有问题。但是对于大多数应用来说,这已经足够了。当然为了保持一致性,你可以把每次的publish放到一个transaction中。这个transaction的实现需要user defined codes。

那么商业系统会做什么呢?一种可能的方案是在系统panic时或者异常重启时或者断电时,应该给各个应用留出时间去flash cache,保证每个应用都能exit gracefully。

5. Fair dispatch 公平分发

  你可能也注意到了,分发机制不是那么优雅。默认状态下,RabbitMQ将第n个Message分发给第n个Consumer。当然n是取余后的。它不管Consumer是否还有unacked Message,只是按照这个默认机制进行分发。

  那么如果有个Consumer工作比较重,那么就会导致有的Consumer基本没事可做,有的Consumer却是毫无休息的机会。那么,RabbitMQ是如何处理这种问题呢?

  通过 basic.qos 方法设置prefetch_count=1 。这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。 设置方法如下:

channel.basic_qos(prefetch_count=1)

注意,这种方法可能会导致queue满。当然,这种情况下你可能需要添加更多的Consumer,或者创建更多的virtualHost来细化你的设计。

6. 最终版本

new_task.py script:

#!/usr/bin/env 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, # make message persistent
))
print " [x] Sent %r" % (message,)
connection.close()

worker.py script:

#!/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('.') )
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()

  

RabbitMQ三----'任务分发 '的更多相关文章

  1. RabbitMQ(三):消息持久化策略

    原文:RabbitMQ(三):消息持久化策略 一.前言 在正常的服务器运行过程中,时常会面临服务器宕机重启的情况,那么我们的消息此时会如何呢?很不幸的事情就是,我们的消息可能会消失,这肯定不是我们希望 ...

  2. 8、RabbitMQ三种Exchange模式(fanout,direct,topic)的性能比较

    RabbitMQ三种Exchange模式(fanout,direct,topic)的性能比较 RabbitMQ中,除了Simple Queue和Work Queue之外的所有生产者提交的消息都由Exc ...

  3. 【转】RabbitMQ三种Exchange模式

    [转]RabbitMQ三种Exchange模式 RabbitMQ中,所有生产者提交的消息都由Exchange来接受,然后Exchange按照特定的策略转发到Queue进行存储 RabbitMQ提供了四 ...

  4. RabbitMQ (三) 工作队列之轮询分发

    上一篇讲了简单队列,实际工作中,这种队列应该很少用到,因为生产者发送消息的耗时一般都很短,但是消费者收到消息后,往往伴随着对高消息的业务逻辑处理,是个耗时的过程,这势必会导致大量的消息积压在一个消费者 ...

  5. 快速掌握RabbitMQ(三)——消息确认、持久化、优先级的C#实现

    1 消息确认 在一些场合,如转账.付费时每一条消息都必须保证成功的被处理.AMQP是金融级的消息队列协议,有很高的可靠性,这里介绍在使用RabbitMQ时怎么保证消息被成功处理的.消息确认可以分为两种 ...

  6. RabbitMQ三种Exchange模式(fanout,direct,topic)的特性 -摘自网络

    RabbitMQ中,所有生产者提交的消息都由Exchange来接受,然后Exchange按照特定的策略转发到Queue进行存储 RabbitMQ提供了四种Exchange:fanout,direct, ...

  7. 学习RabbitMQ(三):AMQP事务机制

    本文转自:http://m.blog.csdn.net/article/details?id=54315940 在使用RabbitMQ的时候,我们可以通过消息持久化操作来解决因为服务器的异常奔溃导致的 ...

  8. Java使用RabbitMQ之订阅分发(Topic)

    使用RabbitMQ进行消息发布和订阅,生产者将消息发送给转发器(exchange),转发器根据路由键匹配已绑定的消息队列并转发消息,主题模式支持路由键的通配. 生产者代码: package org. ...

  9. RabbitMQ三种Exchange模式(fanout,direct,topic)的性能比较

    RabbitMQ中,所有生产者提交的消息都由Exchange来接受,然后Exchange按照特定的策略转发到Queue进行存储 RabbitMQ提供了四种Exchange:fanout,direct, ...

随机推荐

  1. VIM使用系列: 复制并移动文本

    1 5. 复制并移动文本 *copy-move* 2 3 *quote* 4 "{a-zA-Z0-9.%#:-"} 指定下次的删除.抽出和放置命令使用的寄存器 5 {a-zA-Z0 ...

  2. 谈谈dpdk应用层包处理程序的多进程和多线程模型选择时的若干考虑

    看到知乎上有个关于linux多进程.多线程的讨论:http://www.zhihu.com/question/19903801/answer/14842584 自己项目里也对这个问题有过很多探讨和测试 ...

  3. 打印PE目录数据信息

    printf("数据目录信息:\n"); PIMAGE_DATA_DIRECTORY MyDataDir; MyDataDir = PIMAGE_DATA_DIRECTORY((& ...

  4. 转化一个数字数组为function数组(每个function都弹出相应的数字)

    从汤姆大叔的博客里看到了6个基础题目:本篇是第2题 - 转化一个数字数组为function数组(每个function都弹出相应的数字) 此题关键点: 1.如何将一个匿名函数存入数组? 2.如何锁住需要 ...

  5. HDU 6301.Distinct Values-贪心、构造字典序最小的数列 (2018 Multi-University Training Contest 1 1004)

    HDU6301.Distinct Values 这个题就是给你区间要求区间内的数都不相同,然后要求是字典序最小,直接贪心走一遍,但是自己写的时候,思路没有错,初始化写挫了... 将区间按左端点小的排序 ...

  6. (2)C语言 基础2

    一.函数 二.指针 1.指针是一个用来存储内存地址的变量. int * p ; 定义了一个指针变量p,p中存储的是一个地址,改地址里必定会存储一个int类型的数据. *号表示变量p是一个指针.*和指针 ...

  7. “玲珑杯”ACM比赛 Round #1

    Start Time:2016-08-20 13:00:00 End Time:2016-08-20 18:00:00 Refresh Time:2017-11-12 19:51:52 Public ...

  8. 分层图【p4568】 [JLOI2011]飞行路线

    Description Alice和Bob现在要乘飞机旅行,他们选择了一家相对便宜的航空公司.该航空公司一共在nn个城市设有业务,设这些城市分别标记为\(0\)到\(n−1\),一共有\(m\)种航线 ...

  9. [COCI2015]TRAKTOR

    题目大意: 一个$X\times Y(X,Y\leq10^5)$的格子中,每秒钟依次$n(n\leq10^6)$个蘑菇, 告诉你每个蘑菇出现的时间和位置,问何时第一次出现$k(2\leq k\leq ...

  10. kong后台接口

    在nginx-kong.lua中,require('lapis').serve('kong.api'),先require文件/usr/local/share/lua/5.1/lapis/init.lu ...