RabbitMQ學習小結(二)—— Work Queues[Python]

1. 簡介

在Hello World中,已經學會如何發送和接收消息,但是在實際的應用過程中,並不是簡單的接收和發送。例如:當我們有複雜需求,我們需要提升效率,畢竟只有一個消費者難免處理不過來,就如官網中所提到的一樣——在這篇教程中,將創建一個工作隊列(Work Queue),它會發送一些耗時的任務給多個工作者(Worker)。
工作隊列(又稱:任務隊列——Task Queues)是爲了避免等待一些佔用大量資源、時間的操作。當我們把任務(Task)當作消息發送到隊列中,一個運行在後臺的工作者(worker)進程就會取出任務然後處理。當你運行多個工作者(workers),任務就會在它們之間共享。

2. 應用場景

在Hello World中只是單獨的發送一個字符串,但是實際應用中可能是圖片,可能是pdf處理等等,可能需要消耗的時間長短不一,根據官網示例不做複雜操作,而是使用符號“.”來代表任務的難易程度,從而用time.sleep()來控制任務的時間長短。一個點(.)將會耗時1秒鐘。比如"Hello..."就會耗時3秒鐘。

3.  Work Queues

我們需要在Hello World的基礎上進行修改,結合上一章節未做解釋的參數,一一處理,我們先介紹以下幾個關鍵詞,最後再進行總結和整合。

循環調度

使用工作隊列的一個好處就是它能夠並行的處理隊列。如果堆積了很多任務,我們只需要添加更多的工作者(workers)就可以,然後擴展也很簡單。
默認來說,RabbitMQ會按順序得把消息發送給每個消費者(consumer)。平均每個消費者都會收到同等數量得消息。這種發送消息得方式叫做——輪詢(round-robin)。

消息確認

簡單來說,當RabbitMQ將任務丟給worker(也就是消費者)處理時,只有worker給RabbitMQ返回確認消息時,RabbitMQ才認爲這條消息被正確處理完成,繼而將消息移除。否則RabbitMQ認爲消息沒有處理還會繼續保留,當worker掛掉時,RabbitMQ會將這個任務交給另一個worker來處理,直到處理完成。
基於此,RabbitMQ提供了消息響應(acknowledgments)。消費者會通過一個ack(響應),告訴RabbitMQ已經收到並處理了某條消息,然後RabbitMQ就會釋放並刪除這條消息。
消息是沒有超時這個概念的;當工作者與它斷開連的時候,RabbitMQ會重新發送消息。這樣在處理一個耗時非常長的消息任務的時候就不會出問題了。
消息響應默認是開啓的。之前的例子中我們可以使用no_ack=True標識把它關閉。我們下面需要移除這個標識,並在工作者(worker)完成任務後,就發送一個響應。
代碼如下:
def callback(ch, method, properties, body):
    """
        @ch: channel 通道,是由外部調用時上送
        out body
        讀取隊列內容做不同的操作
    """
    print " [x] Recived %r" % (body, )
    print " [x] ch {0}".format(ch)
    print " [x] method {0}".format(method)
    print " [x] properties {0}".format(properties)
    time.sleep(body.count('.'))
    print " [x] Done %r" % (body, )
    ch.basic_ack(delivery_tag=method.delivery_tag)
使用basic_ack來發送確認消息。
注意:
如果忘記確認也是一件非常危險的事情。從技術上來講,這條消息即使被處理也不會從內存移除,這就造成可用內存會越來越小,最終造成內存溢出。從業務上來來講,同一條消息被處理兩次,涉及到業務模型可能會造成重複處理,造成嚴重後果。
對於這種問題也可以通過rabbitmqctl輸出messages_unacknowledged字段:
[tRabbitMQ@iZ250x18mnzZ src]$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
task_queue      0       0
hello   0       0
[tRabbitMQ@iZ250x18mnzZ src]$ 

持久化

如果沒有特意告訴RabbitMQ,那麼在它退出或者崩潰的時候,將會丟失所有隊列和消息。爲了確保信息不會丟失,有兩個事情是需要注意的:我們必須把“隊列”和“消息”設爲持久化。

隊列持久化,需要在聲明隊列時,通過參數durable=True來聲明隊列是持久化
channel.queue_declare(queue='hello', durable=True)
儘管這行代碼本身是正確的,但是仍然不會正確運行。因爲我們已經定義過一個叫hello的非持久化隊列。RabbitMq不允許你使用不同的參數重新定義一個隊列,它會返回一個錯誤。但我們現在使用一個快捷的解決方法——用不同的名字,例如task_queue。
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
	                      ))
注意:
將消息設爲持久化並不能完全保證不會丟失。以上代碼只是告訴了RabbitMq要把消息存到硬盤,但從RabbitMq收到消息到保存之間還是有一個很小的間隔時間。因爲RabbitMq並不是所有的消息都使用fsync(2)——它有可能只是保存到緩存中,並不一定會寫到硬盤中。並不能保證真正的持久化,但已經足夠應付我們的簡單工作隊列。如果你一定要保證持久化,你需要改寫你的代碼來支持事務(transaction)。

公平調度

默認情況下,如果沒有特殊說明,RabbitMQ只管將消息分配給woker,而rabbitMQ並不知道woker是否能夠應付得了相應的消息,也不會關心有多少worker沒有作出響應。它盲目的把第n-th條消息發給第n-th個消費者。
可以使用basic.qos方法,並設置prefetch_count=1。這樣是告訴RabbitMQ,再同一時刻,不要發送超過1條消息給一個工作者(worker),直到它已經處理了上一條消息並且作出了響應。這樣,RabbitMQ就會把消息分發給下一個空閒的工作者(worker)。
channel.basic_qos(prefetch_count=1)
注意:
關於工作隊列的大小
如果所有的worker都處理繁忙狀態,你的隊列就會被填滿。需要注意這個問題,要麼添加更多的工作者(workers),要麼使用其他策略。

總結

在第一章的基礎上,我們在這裏重點增加了以下幾個概念
工作隊列:實際應用過程中,隊列是可以並行處理的,我們可以通過增加worker的方式提升工作效率
持久化:爲了安全性,我們需要保證隊列不能丟失,那麼隊列以及消息都必須要保持持久化
隊列持久化:聲明時,指定durable=True
消息持久化:指定消息屬性,delivery_mode=2
消息確認:保持完整性,在worker處理完成後需要通知RabbitMQ已經處理,這樣整個流程才能保持完整,避免風險。
worker處理時,通過basic_ack來進行確認消息
公平調度:RabbitMQ只負責分發,而不關注worker是否處理完成,所以worker中需要指定,我最多同時可以處理多少任務,處理多大的任務,否則rabbitMQ會無限制的發送,導致崩潰。
worker處理時,通過basic_qos(prefetch_count=1)來控制
整合代碼
發送程序
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date    : 2016-02-28 21:28:17
# @Author  : mx ([email protected])
# @Link    : http://www.shujutiyu.com.cn/
# @Version : $Id$

import os
import pika
import sys

conn = None

message = ' '.join(sys.argv[1:]) or "Hello World!"

try:
    # 獲取連接
    conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
    # 獲取通道
    channel = conn.channel()

    # 在發送隊列前,需要確定隊列是否存在,如果不存在RabbitMQ將會丟棄,先創建隊列
    # @param: durable 隊列持久化, 默認False
    channel.queue_declare('task_queue', durable=True)

    # 在RabbitMQ中發送消息,不是直接發送隊列,而是發送交換機(exchange),此處不多做研究
    ret = channel.basic_publish(exchange='',
                                routing_key='task_queue',
                                body=message,
                                properties=pika.BasicProperties(
                                    delivery_mode=2, # 消息持久化
                                ) )
    print " [x] Sent '{0}'".format(message)
    print ret
except Exception, e:
    raise e
finally:
    if conn:
        conn.close()
接收程序
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date    : 2016-02-29 16:30:21
# @Author  : mx ([email protected])
# @Link    : http://www.shujutiyu.com/
# @Version : $Id$

import os
import pika
import time

conn = None


def callback(ch, method, properties, body):
    """
        @ch: channel 通道,是由外部調用時上送
        out body
        讀取隊列內容做不同的操作
    """
    print " [x] Recived %r" % (body, )
    print " [x] ch {0}".format(ch)
    print " [x] method {0}".format(method)
    print " [x] properties {0}".format(properties)
    time.sleep(body.count('.'))
    print " [x] Done %r" % (body, )
    ch.basic_ack(delivery_tag=method.delivery_tag)

try:
    # get connection
    conn = pika.BlockingConnection(pika.ConnectionParameters(
        'localhost')
    )

    # get channel
    channel = conn.channel()

    # declare queue, 重複聲明不會報錯,但是沒有隊列的話直接取用會報錯
    # @param: durable  隊列持久化
    channel.queue_declare('task_queue', durable=True)

    # set channel 在同一時刻處理一條消息
    channel.basic_qos(prefetch_count=1)
    # get message
    # @no_ack,不需要返回確認消息, 默認是False,設置的話,有可能會丟包,不設置的話,
    # 當不返回確認消息時,有可能重複處理或者是內存溢出。
    channel.basic_consume(callback, queue='task_queue')

    print ' [*] Waiting for messages. To exit press CTRL+C'
    channel.start_consuming()
except Exception, e:
    raise e
finally:
    if conn:
        conn.close()

本章就介紹到這裏,其實還有好多可用的東西,可以到下面提供的參考資料查看,也可以查看pika的源碼,自取所需。

4.  參考資料

發佈了37 篇原創文章 · 獲贊 127 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章