【OpenStack源碼分析之二】RabbitMQ分析

前言

正在捋Nova的代碼,從服務啓動的入口這塊就用到了第三方的Oslo_messaging庫,可能也是因爲消息中間件確實是整個軟件的瓶頸,Oslo_messaging試圖隔離出消息中間件和應用之間的接口,使得不僅僅可以使用RabbitMQ,也可以使用Kafka等其他中間件。

RabbitMQ介紹

這裏十分感謝anzhsoft的技術專欄http://blog.csdn.net/column/details/rabbitmq.html;把RobbitMQ這款中間件工具從使用者的視角寫得很全面,我也不想深究裏面的細節,在anzhsoft的基礎之上我再提取一些用戶關心的信息。

歷史

RabbitMQ是一個由erlang開發的AMQP(Advanced Message Queue )的開源實現。AMQP 的出現其實也是應了廣大人民羣衆的需求,雖然在同步消息通訊的世界裏有很多公開標準(如 COBAR的 IIOP ,或者是 SOAP 等),但是在異步消息處理中卻不是這樣,只有大企業有一些商業實現(如微軟的 MSMQ ,IBM 的 Websphere MQ 等),因此,在 2006 年的 6 月,Cisco 、Redhat、iMatix 等聯合制定了 AMQP 的公開標準。
這裏寫圖片描述
RabbitMQ是由RabbitMQ Technologies Ltd開發並且提供商業支持的。該公司在2010年4月被SpringSource(VMWare的一個部門)收購。在2013年5月被併入Pivotal。其實VMWare,Pivotal和EMC本質上是一家的。不同的是VMWare是獨立上市子公司,而Pivotal是整合了EMC的某些資源,現在並沒有上市。

RabbitMQ的官網是http://www.rabbitmq.com

架構術語

這裏寫圖片描述
1.Server(broker): 接受客戶端連接,實現AMQP消息隊列和路由功能的進程。

2.Virtual Host:其實是一個虛擬概念,類似於權限控制組,一個Virtual Host裏面可以有若干個Exchange和Queue,但是權限控制的最小粒度是Virtual Host

3.Exchange:接受生產者發送的消息,並根據Binding規則將消息路由給服務器中的隊列。ExchangeType決定了Exchange路由消息的行爲,例如,在RabbitMQ中,ExchangeType有direct、Fanout和Topic三種,不同類型的Exchange路由的行爲是不一樣的。

4.Message Queue:消息隊列,用於存儲還未被消費者消費的消息。

5.Message: 由Header和Body組成,Header是由生產者添加的各種屬性的集合,包括Message是否被持久化、由哪個Message Queue接受、優先級是多少等。而Body是真正需要傳輸的APP數據。

6.Binding:Binding聯繫了Exchange與Message Queue。Exchange在與多個Message Queue發生Binding後會生成一張路由表,路由表中存儲着Message Queue所需消息的限制條件即Binding Key。當Exchange收到Message時會解析其Header得到Routing Key,Exchange根據Routing Key與Exchange Type將Message路由到Message Queue。Binding Key由Consumer在Binding Exchange與Message Queue時指定,而Routing Key由Producer發送Message時指定,兩者的匹配方式由Exchange Type決定。

7.Connection:連接,對於RabbitMQ而言,其實就是一個位於客戶端和Broker之間的TCP連接。

8.Channel:信道,僅僅創建了客戶端到Broker之間的連接後,客戶端還是不能發送消息的。需要爲每一個Connection創建Channel,AMQP協議規定只有通過Channel才能執行AMQP的命令。一個Connection可以包含多個Channel。之所以需要Channel,是因爲TCP連接的建立和釋放都是十分昂貴的,如果一個客戶端每一個線程都需要與Broker交互,如果每一個線程都建立一個TCP連接,暫且不考慮TCP連接是否浪費,就算操作系統也無法承受每秒建立如此多的TCP連接。RabbitMQ建議客戶端線程之間不要共用Channel,至少要保證共用Channel的線程發送消息必須是串行的,但是建議儘量共用Connection。

9.Command:AMQP的命令,客戶端通過Command完成與AMQP服務器的交互來實現自身的邏輯。例如在RabbitMQ中,客戶端可以通過publish命令發送消息,txSelect開啓一個事務,txCommit提交一個事務。

應用場景

場景1:單發送單接收
這個場景比較簡單,只是個Helllo word,並沒有太大的實際用途。不過要注意的是Queue和Binding的CURD權限,生產者和消費者是有的,但是vHost和Exchange的權限他們並沒有,因爲前者和特定用戶相關,後者則是多個用戶共享使用的。
這裏寫圖片描述
send.py:

#!/usr/bin/env python  
import pika  

connection = pika.BlockingConnection(pika.ConnectionParameters(  
        host='localhost'))  
channel = connection.channel()  

channel.queue_declare(queue='hello')  

channel.basic_publish(exchange='',  
                      routing_key='hello',  
                      body='Hello World!')  
print " [x] Sent 'Hello World!'"  
connection.close() 

receive.py:

#!/usr/bin/env python  
import pika  

connection = pika.BlockingConnection(pika.ConnectionParameters(  
        host='localhost'))  
channel = connection.channel()  

channel.queue_declare(queue='hello')  

print ' [*] Waiting for messages. To exit press CTRL+C'  

def callback(ch, method, properties, body):  
    print " [x] Received %r" % (body,)  

channel.basic_consume(callback,  
                      queue='hello',  
                      no_ack=True)  

channel.start_consuming()  

場景2:任務分發
這裏寫圖片描述
這種場景是有實際用途的,比如Job的調度,所以Rabbit在這個場景上做了HA的保障工作以及調度的優化:

爲了防止消息丟失做了持久化;防止消息不被處理又增加了消息確認機制。這裏面要注意,Consumer端在完成任務處理之後要回復ACK,否則後果很嚴重。當Consumer退出時,Message會重新分發。然後RabbitMQ會佔用越來越多的內存,由於RabbitMQ會長時間運行,可能導致“內存泄漏”。

在Job的調度這塊支持多種算法,除了round robin機制還有Fair dispatch 公平分發機制,通過 basic.qos 方法設置prefetch_count=1 。這樣RabbitMQ就會使得每個Consumer在同一個時間點最多處理一個Message。換句話說,在接收到該Consumer的ack前,他它不會將新的Message分發給它。

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

場景3:Pub-Sub
使用場景:發佈、訂閱模式,發送端發送廣播消息,多個接收端接收。這個場景應用空間很廣闊,尤其是在大型軟件內部的子系統之間的消息傳遞。不過和前兩者在使用上不同的是這裏需要用到Exchange,類似於一個Router把消息轉發到消費者Binding的消息隊列上。
這裏寫圖片描述
emit_log.py script:

#!/usr/bin/env python  
import pika  
import sys  

connection = pika.BlockingConnection(pika.ConnectionParameters(  
        host='localhost'))  
channel = connection.channel()  

channel.exchange_declare(exchange='logs',  
                         type='fanout')  

message = ' '.join(sys.argv[1:]) or "info: Hello World!"  
channel.basic_publish(exchange='logs',  
                      routing_key='',  
                      body=message)  
print " [x] Sent %r" % (message,)  
connection.close()  

還有一點要注意的是我們聲明瞭exchange。publish到一個不存在的exchange是被禁止的。如果沒有queue bindings exchange的話,log是被丟棄的。
Consumer:receive_logs.py:

#!/usr/bin/env python  
import pika  

connection = pika.BlockingConnection(pika.ConnectionParameters(  
        host='localhost'))  
channel = connection.channel()  

channel.exchange_declare(exchange='logs',  
                         type='fanout')  

result = channel.queue_declare(exclusive=True)  
queue_name = result.method.queue  

channel.queue_bind(exchange='logs',  
                   queue=queue_name)  

print ' [*] Waiting for logs. To exit press CTRL+C'  

def callback(ch, method, properties, body):  
    print " [x] %r" % (body,)  

channel.basic_consume(callback,  
                      queue=queue_name,  
                      no_ack=True)  

channel.start_consuming()  

場景4:Routing 消息路由
上篇文章中,我們構建了一個簡單的日誌系統。接下來,我們將豐富它:能夠使用不同的severity來監聽不同等級的log。比如我們希望只有error的log才保存到磁盤上。

  1. Direct exchange
    Direct exchange的路由算法非常簡單:通過binding key的完全匹配,可以通過下圖來說明。
    這裏寫圖片描述
    exchange X和兩個queue綁定在一起。Q1的binding key是orange。Q2的binding key是black和green。
    當P publish key是orange時,exchange會把它放到Q1。如果是black或者green那麼就會到Q2。其餘的Message都會被丟棄。

  2. Multiple bindings
    多個queue綁定同一個key是可以的。對於下圖的例子,Q1和Q2都綁定了black。也就是說,對於routing key是black的Message,會被deliver到Q1和Q2。其餘的Message都會被丟棄。
    這裏寫圖片描述

最終代碼:
這裏寫圖片描述
The code for emit_log_direct.py:

#!/usr/bin/env python  
import pika  
import sys  

connection = pika.BlockingConnection(pika.ConnectionParameters(  
        host='localhost'))  
channel = connection.channel()  

channel.exchange_declare(exchange='direct_logs',  
                         type='direct')  

severity = sys.argv[1] if len(sys.argv) > 1 else 'info'  
message = ' '.join(sys.argv[2:]) or 'Hello World!'  
channel.basic_publish(exchange='direct_logs',  
                      routing_key=severity,  
                      body=message)  
print " [x] Sent %r:%r" % (severity, message)  
connection.close() 

The code for receive_logs_direct.py:

#!/usr/bin/env python  
import pika  
import sys  

connection = pika.BlockingConnection(pika.ConnectionParameters(  
        host='localhost'))  
channel = connection.channel()  

channel.exchange_declare(exchange='direct_logs',  
                         type='direct')  

result = channel.queue_declare(exclusive=True)  
queue_name = result.method.queue  

severities = sys.argv[1:]  
if not severities:  
    print >> sys.stderr, "Usage: %s [info] [warning] [error]" % \  
                         (sys.argv[0],)  
    sys.exit(1)  

for severity in severities:  
    channel.queue_bind(exchange='direct_logs',  
                       queue=queue_name,  
                       routing_key=severity)  

print ' [*] Waiting for logs. To exit press CTRL+C'  

def callback(ch, method, properties, body):  
    print " [x] %r:%r" % (method.routing_key, body,)  

channel.basic_consume(callback,  
                      queue=queue_name,  
                      no_ack=True)  

channel.start_consuming()  

場景5:使用主題Topic進行消息分發
在上文中,我們實現了一個簡單的日誌系統。Consumer可以監聽不同severity的log。但是,這也是它之所以叫做簡單日誌系統的原因,因爲是僅僅能夠通過severity設定。不支持更多的標準。

比如syslog unix的日誌工具,它可以通過severity (info/warn/crit…) 和模塊(auth/cron/kern…)。這可能更是我們想要的:我們可以僅僅需要cron模塊的log。

爲了實現類似的功能,我們需要用到topic exchange。
這裏寫圖片描述
現在我們要refine我們上篇的日誌系統。routing keys 有兩個部分: “.”。

The code for emit_log_topic.py:

#!/usr/bin/env python  
import pika  
import sys  

connection = pika.BlockingConnection(pika.ConnectionParameters(  
        host='localhost'))  
channel = connection.channel()  

channel.exchange_declare(exchange='topic_logs',  
                         type='topic')  

routing_key = sys.argv[1] if len(sys.argv) > 1 else 'anonymous.info'  
message = ' '.join(sys.argv[2:]) or 'Hello World!'  
channel.basic_publish(exchange='topic_logs',  
                      routing_key=routing_key,  
                      body=message)  
print " [x] Sent %r:%r" % (routing_key, message)  
connection.close()  

The code for receive_logs_topic.py:

#!/usr/bin/env python  
import pika  
import sys  

connection = pika.BlockingConnection(pika.ConnectionParameters(  
        host='localhost'))  
channel = connection.channel()  

channel.exchange_declare(exchange='topic_logs',  
                         type='topic')  

result = channel.queue_declare(exclusive=True)  
queue_name = result.method.queue  

binding_keys = sys.argv[1:]  
if not binding_keys:  
    print >> sys.stderr, "Usage: %s [binding_key]..." % (sys.argv[0],)  
    sys.exit(1)  

for binding_key in binding_keys:  
    channel.queue_bind(exchange='topic_logs',  
                       queue=queue_name,  
                       routing_key=binding_key)  

print ' [*] Waiting for logs. To exit press CTRL+C'  

def callback(ch, method, properties, body):  
    print " [x] %r:%r" % (method.routing_key, body,)  

channel.basic_consume(callback,  
                      queue=queue_name,  
                      no_ack=True)  

channel.start_consuming() 

場景6:適用於雲計算集羣的遠程調用(RPC)
在雲計算環境中,很多時候需要用它其他機器的計算資源,我們有可能會在接收到Message進行處理時,會把一部分計算任務分配到其他節點來完成。那麼,RabbitMQ如何使用RPC呢?在本篇文章中,我們將會通過其它節點求來斐波納契完成示例,同前幾種使用場景不同,在這個場景下需要返回調用結果。
這裏寫圖片描述
client發送請求的Message然後server返回響應結果。爲了收到響應client在publish message時需要提供一個”callback“(回調)的queue地址。這又有其他問題了:收到響應後它無法確定是否是它的,因爲所有的響應都寫到同一個queue了。上一小節的correlation_id在這種情況下就派上用場了:對於每個request,都設置唯一的一個值,在收到響應後,通過這個值就可以判斷是否是自己的響應。如果不是自己的響應,就不去處理。

AMQP 預定義了14個屬性。它們中的絕大多很少會用到。以下幾個是平時用的比較多的:

delivery_mode: 持久化一個Message(通過設定值爲2)。其他任意值都是非持久化。請移步RabbitMQ消息隊列(三):任務分發機制
content_type: 描述mime-type 的encoding。比如設置爲JSON編碼:設置該property爲application/json。
reply_to: 一般用來指明用於回調的queue(Commonly used to name a callback queue)。
correlation_id: 在請求中關聯處理RPC響應(correlate RPC responses with requests)。
工作流程:

當客戶端啓動時,它創建了匿名的exclusive callback queue.
- 客戶端的RPC請求時將同時設置兩個properties: reply_to設置爲callback queue;correlation_id設置爲每個request一個獨一無二的值.
- 請求將被髮送到an rpc_queue queue.
- RPC端或者說server一直在等待那個queue的請求。當請求到達時,它將通過在reply_to指定的queue回覆一個message給client。
- client一直等待callback queue的數據。當message到達時,它將檢查correlation_id的值,如果值和它request發送時的一致那麼就將返回響應。

The code for rpc_client.py:

#!/usr/bin/env python  
import pika  
import uuid  

class FibonacciRpcClient(object):  
    def __init__(self):  
        self.connection = pika.BlockingConnection(pika.ConnectionParameters(  
                host='localhost'))  

        self.channel = self.connection.channel()  

        result = self.channel.queue_declare(exclusive=True)  
        self.callback_queue = result.method.queue  

        self.channel.basic_consume(self.on_response, no_ack=True,  
                                   queue=self.callback_queue)  

    def on_response(self, ch, method, props, body):  
        if self.corr_id == props.correlation_id:  
            self.response = body  

    def call(self, n):  
        self.response = None  
        self.corr_id = str(uuid.uuid4())  
        self.channel.basic_publish(exchange='',  
                                   routing_key='rpc_queue',  
                                   properties=pika.BasicProperties(  
                                         reply_to = self.callback_queue,  
                                         correlation_id = self.corr_id,  
                                         ),  
                                   body=str(n))  
        while self.response is None:  
            self.connection.process_data_events()  
        return int(self.response)  

fibonacci_rpc = FibonacciRpcClient()  

print " [x] Requesting fib(30)"  
response = fibonacci_rpc.call(30)  
print " [.] Got %r" % (response,) 

The code for rpc_server.py:

#!/usr/bin/env python  
import pika  

connection = pika.BlockingConnection(pika.ConnectionParameters(  
        host='localhost'))  

channel = connection.channel()  

channel.queue_declare(queue='rpc_queue')  

def fib(n):  
    if n == 0:  
        return 0  
    elif n == 1:  
        return 1  
    else:  
        return fib(n-1) + fib(n-2)  

def on_request(ch, method, props, body):  
    n = int(body)  

    print " [.] fib(%s)"  % (n,)  
    response = fib(n)  

    ch.basic_publish(exchange='',  
                     routing_key=props.reply_to,  
                     properties=pika.BasicProperties(correlation_id = \  
                                                     props.correlation_id),  
                     body=str(response))  
    ch.basic_ack(delivery_tag = method.delivery_tag)  

channel.basic_qos(prefetch_count=1)  
channel.basic_consume(on_request, queue='rpc_queue')  

print " [x] Awaiting RPC requests"  
channel.start_consuming()  

場景7:消息隊列的小夥伴: ProtoBuf(Google Protocol Buffer)
ProtoBuf是一種輕便高效的結構化數據存儲格式,可以用於結構化數據串行化,或者說序列化。它很適合做數據存儲或 RPC 數據交換格式。可用於通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。目前提供了 C++、Java、Python 三種語言的 API。

RabbitMQ支持使用不同的序列化工具來進行編碼,ProtoBuf和XML, Json相較而言是目前市面上性能最好的。
這裏寫圖片描述

Publisher的消息確認機制

在前面的文章中提到了queue和consumer之間的消息確認機制:通過設置ack。那麼Publisher能不到知道他post的Message有沒有到達queue,甚至更近一步,是否被某個Consumer處理呢?畢竟對於一些非常重要的數據,可能Publisher需要確認某個消息已經被正確處理。

在我們的系統中,我們沒有是實現這種確認,也就是說,不管Message是否被Consume了,Publisher不會去care。他只是將自己的狀態publish給上層,由上層的邏輯去處理。如果Message沒有被正確處理,可能會導致某些狀態丟失。但是由於提供了其他強制刷新全部狀態的機制,因此這種異常情況的影響也就可以忽略不計了。

對於某些異步操作,比如客戶端需要創建一個FileSystem,這個可能需要比較長的時間,甚至要數秒鐘。這時候通過RPC可以解決這個問題。因此也就不存在Publisher端的確認機制了。

那麼,有沒有一種機制能保證Publisher能夠感知它的Message有沒有被處理的?答案肯定的。

事務機制 VS Publisher Confirm
如果採用標準的 AMQP 協議,則唯一能夠保證消息不會丟失的方式是利用事務機制 – 令 channel 處於 transactional 模式、向其 publish 消息、執行 commit 動作。在這種方式下,事務機制會帶來大量的多餘開銷,並會導致吞吐量下降 250% 。爲了補救事務帶來的問題,引入了 confirmation 機制(即 Publisher Confirm)。

爲了使能 confirm 機制,client 首先要發送 confirm.select 方法幀。取決於是否設置了 no-wait 屬性,broker 會相應的判定是否以 confirm.select-ok 進行應答。一旦在 channel 上使用 confirm.select方法,channel 就將處於 confirm 模式。處於 transactional 模式的 channel 不能再被設置成 confirm 模式,反之亦然。

一旦 channel 處於 confirm 模式,broker 和 client 都將啓動消息計數(以 confirm.select 爲基礎從 1 開始計數)。broker 會在處理完消息後,在當前 channel 上通過發送 basic.ack 的方式對其進行 confirm 。delivery-tag 域的值標識了被 confirm 消息的序列號。broker 也可以通過設置 basic.ack 中的 multiple 域來表明到指定序列號爲止的所有消息都已被 broker 正確的處理了。

在異常情況中,broker 將無法成功處理相應的消息,此時 broker 將發送 basic.nack 來代替 basic.ack 。在這個情形下,basic.nack 中各域值的含義與 basic.ack 中相應各域含義是相同的,同時 requeue 域的值應該被忽略。通過 nack 一或多條消息,broker 表明自身無法對相應消息完成處理,並拒絕爲這些消息的處理負責。在這種情況下,client 可以選擇將消息 re-publish 。

在 channel 被設置成 confirm 模式之後,所有被 publish 的後續消息都將被 confirm(即 ack) 或者被 nack 一次。但是沒有對消息被 confirm 的快慢做任何保證,並且同一條消息不會既被 confirm 又被 nack 。

參考資料:
http://blog.csdn.net/column/details/rabbitmq.html
http://www.cnblogs.com/luxiaoxun/p/3918054.html

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