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_unacknowledgedListing queues ...task_queue 0 0hello 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 (mx472756841@gmail.com)# @Link : http://www.shujutiyu.com.cn/# @Version : $Id$import osimport pikaimport sysconn = Nonemessage = ' '.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 retexcept Exception, e: raise efinally: if conn: conn.close()
接收程式
#!/usr/bin/env python# -*- coding: utf-8 -*-# @Date : 2016-02-29 16:30:21# @Author : mx (mx472756841@gmail.com)# @Link : http://www.shujutiyu.com/# @Version : $Id$import osimport pikaimport timeconn = Nonedef 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 efinally: if conn: conn.close()
本章就介紹到這裡,其實還有好多可用的東西,可以到下面提供的參考資料查看,也可以查看pika的源碼,自取所需。
4. 參考資料 中文資料:http://rabbitmq-into-chinese.readthedocs.org/zh_CN/latest/tutorials_with_python/[2]Work_Queues/ 官網資料:http://www.rabbitmq.com/tutorials/tutorial-two-python.html