RabbitMQ官網教程2——工作隊列

        第一節中我們實現了簡單的發送接收消息。現在我們創建一個工作隊列,用於在多個worker中分配耗時任務。工作隊列是爲了避免立即執行資源密集型任務並等待其完成,有了工作隊列,就可以稍後再處理任務。我們把任務封裝成消息併發送到隊列,後臺的工作進程從隊列中取出任務並執行。當有多個工作進程時,任務會在它們中分發。

        web app中,在一個很短的http請求時長內不可能處理一個複雜的任務,這時工作隊列就很有用。

 

準備

        用字符串模擬複雜任務,通過time.sleep()函數模擬進程繁忙,字符串中的點表示任務複雜度。

        修改上節的發送程序,實現從命令行輸入消息併發送到隊列。

import sys

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)

        接收程序中增加延時。

import time

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

輪詢分發

        使用任務隊列的一個優點就是方便水平擴展。如果系統負載過大就可以增加新的工作進程即可。

        默認情況下,Rabbitmq會順序的將消息發給下一個消費者,這種分發方式就是round-robin。平均下來每個消費者會獲得等量的消息。

 

消息確認

        執行任務可能需要一點時間。如果消費者在執行任務時掛掉,任務只執行了一部分,這時會怎樣呢?當前的代碼中,rabbitmq投遞消息到消費者後就立即把消息刪除。這樣,如果工作進程被殺掉,正在處理的消息就會丟失,包括那些分配給該進程但還沒來得及處理的消息也會丟失。

        我們不希望丟失任何消息,如果一個worker掛掉,我們希望把消息發送給另一個worker。

        爲了保證消息不丟失,rabbitmq支持消息確認。消費者會發送ack消息確認給rabbitmq,通知它消息已收到並處理,rabbitmq可以刪除它。

        如果消費者在發送ack前掛掉(通道關閉、連接關閉、tcp連接丟失),rabbitmq認爲消息未被完全處理,並將消息重新入隊,如果存在其他消費者,就將消息重新投遞到其他消費者。這樣就保證消息不丟失。

        這裏沒有超時限制,消費者掛掉,rabbitmq就重新投遞消息。消息處理需要很長時間時也可以很好的支持。

        默認情況下消息確認是開啓的。前面的例子我們通過no_ack=True顯式關閉了。現在我們需要去掉該標誌,任務處理完髮送消息確認。

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

        以上代碼可以保證當工作進程處理消息時被殺掉,不會丟消息,一旦程序掛掉所有未確認的消息都會被重新投遞。

        忘記確認是一個非常容易犯的常見錯誤,結果很嚴重。rabbitmq會佔用越來越多的內存因爲它不能釋放未確認的消息。要調試這種錯誤,可以使用rabbitmqctl打印未確認消息:

        sudo rabbitmqctl list_queue namemessages_ready messages_unacknowledged

 

消息持久化

        我們知道了如何在消費者掛掉的情況下確保不丟消息,但是如果rabbitmq服務掛了,消息還是會丟失,RabbitMQ不會記住隊列及消息,除非我們設置它。要確保不丟消息,需要把隊列和消息都設爲可持久化durable。

        RabbitMQ中已經存在一個非durable的隊列hello,以durable參數再次創建該隊列並不生效。RabbitMQ不允許用不同的參數重定義一個已經存在的隊列,會返回錯誤。

        注意,將消息標記爲持久化並不能完全保證消息不丟失。雖然該標誌通知RabbitMQ將消息保存到磁盤,但是在RabbitMQ收到消息但還未保存前還是有一個小小的時間窗。而且RabbitMQ並不對每個消息都做fsync操作,可能消息還只是保存在緩存中,並未真正寫入磁盤。但是這種持久化對簡單隊列來講已經足夠了,如果需要強持久性,可以使用publisher confirms。

 

公平分發

        輪詢分發有個問題,假如有兩個消費者,且所有奇數消息都很繁重,所有偶數消息都很輕量,那麼一個消費者就會一直很繁忙而另一個則幾乎沒什麼工作量。RabbitMQ則對此並不知道。

        這是因爲,在消息進入隊列時RabbitMQ就分發消息,它並不看消費者的未確認消息數量,只是盲目的把第n個消息分配給第n個消費者。

        爲防止這種情況,我們可以使用basic.qos方法,參數prefetch_count=1。這是通知RabbitMQ每次只給消費者一個消息。換句話說,在消費者處理完消息發送確認之前不再給他分發新消息,而是把消息分發給那些不繁忙的消費者。

        當然,如果所有的消費者都繁忙,隊列可能會滿,這是應該考慮增加消費者或其它策略。


 最終程序

        發送端:

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

        接收端:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章