rabbitmq學習 頂 原

主要參考網址: rabbitmq 官網quick start

如果理解有不對的地方,歡迎留言指正!

簡介

rabbitmq基本架構

簡單的rabbitmq工作模型 上圖有3個角色:

  1. RabbitMQ Server:Producer,數據的發送方。create messages and publish (send) them to a broker server (RabbitMQ).一個Message有兩個部分:payload(有效載荷)和label(標籤)。payload顧名思義就是傳輸的數據。label是exchange的名字或者說是一個tag,它描述了payload,而且RabbitMQ也是通過這個label來決定把這個Message發給哪個Consumer。AMQP僅僅描述了label,而RabbitMQ決定了如何使用這個label的規則。

  2. Client A & B:RabbitMQ Server: 也叫broker server,它不是運送食物的卡車,而是一種傳輸服務。原話是RabbitMQisn’t a food truck, it’s a delivery service. 他的角色就是維護一條從Producer到Consumer的路線,保證數據能夠按照指定的方式進行傳輸。但是這個保證也不是100%的保證,但是對於普通的應用來說這已經足夠了。當然對於商業系統來說,可以再做一層數據一致性的guard,就可以徹底保證系統的一致性了。

  3. Client 1,2,3:Consumer,數據的接收方。Consumers attach to a broker server (RabbitMQ) and subscribe to a queue。把queue比作是一個有名字的郵箱。當有Message到達某個郵箱後,RabbitMQ把它發送給它的某個訂閱者即Consumer。當然可能會把同一個Message發送給很多的Consumer。在這個Message中,只有payload,label已經被刪掉了。對於Consumer來說,它是不知道誰發送的這個信息的。就是協議本身不支持。但是當然瞭如果Producer發送的payload包含了Producer的信息就另當別論了。

  4. 數據從Producer到Consumer的正確傳遞,還有三個概念需要明確:exchanges, queues and bindings。

    • Exchanges are where producers publish their messages.
    • Queuesare where the messages end up and are received by consumers
    • Bindings are how the messages get routed from the exchange to particular queues.
    • Connection: 就是一個TCP的連接。Producer和Consumer都是通過TCP連接到RabbitMQ Server的。以後我們可以看到,程序的起始處就是建立這個TCP連接。
    • Channels: 虛擬連接。它建立在上述的TCP連接中。數據流動都是在Channel中進行的。也就是說,一般情況是程序起始建立TCP連接,第二步就是建立這個Channel。

==說明==:爲什麼使用Channel,而不是直接使用TCP連接? 對於OS來說,建立和關閉TCP連接是有代價的,頻繁的建立關閉TCP連接對於系統的性能有很大的影響,而且TCP的連接數也有限制,這也限制了系統處理高併發的能力。但是,在TCP連接中建立Channel是沒有上述代價的。對於Producer或者Consumer來說,可以併發的使用多個Channel進行Publish或者Receive。有實驗表明,1s的數據可以Publish10K的數據包。當然對於不同的硬件環境,不同的數據包大小這個數據肯定不一樣,但是我只想說明,對於普通的Consumer或者Producer來說,這已經足夠了。如果不夠用,你考慮的應該是如何細化split你的設計。

rabbitmq思想闡述

使用ack確認Message的正確傳遞

默認情況下,如果Message 已經被某個Consumer正確的接收到了,那麼該Message就會被從queue中移除。當然也可以讓同一個Message發送到很多的Consumer。

如果一個queue沒被任何的Consumer Subscribe(訂閱),那麼,如果這個queue有數據到達,那麼這個數據會被cache,不會被丟棄。當有Consumer時,這個數據會被立即發送到這個Consumer,這個數據被Consumer正確收到時,這個數據就被從queue中刪除。

那麼什麼是正確收到呢?通過ack。每個Message都要被acknowledged(確認,ack)。我們可以顯示的在程序中去ack,也可以自動的ack。如果有數據沒有被ack,那麼:RabbitMQ Server會把這個信息發送到下一個Consumer。 如果這個app有bug,忘記了ack,那麼RabbitMQ Server不會再發送數據給它,因爲Server認爲這個Consumer處理能力有限。 而且ack的機制可以起到限流的作用(Benefitto throttling):在Consumer處理完成數據後發送ack,甚至在額外的延時後發送ack,將有效的balance Consumer的load。

當然對於實際的例子,比如我們可能會對某些數據進行merge,比如merge 4s內的數據,然後sleep 4s後再獲取數據。特別是在監聽系統的state,我們不希望所有的state實時的傳遞上去,而是希望有一定的延時。這樣可以減少某些IO,而且終端用戶也不會感覺到。

Reject a message

有兩種方式:

  • Reject可以讓RabbitMQ Server將該Message 發送到下一個Consumer。
  • 從queue中立即刪除該Message。

Creating a queue

Consumer和Procuder都可以通過 queue.declare 創建queue。對於某個Channel來說,Consumer不能declare一個queue,卻訂閱其他的queue。當然也可以創建私有的queue。這樣只有app本身纔可以使用這個queue。queue也可以自動刪除,被標爲auto-delete的queue在最後一個Consumer unsubscribe後就會被自動刪除。那麼如果是創建一個已經存在的queue呢?那麼不會有任何的影響。需要注意的是沒有任何的影響,也就是說第二次創建如果參數和第一次不一樣,那麼該操作雖然成功,但是queue的屬性並不會被修改

++那麼誰應該負責創建這個queue呢?是Consumer,還是Producer?++

如果queue不存在,當然Consumer不會得到任何的Message。但是如果queue不存在,那麼Producer Publish的Message會被丟棄。所以,還是爲了數據不丟失,Consumer和Producer都try to create the queue!反正不管怎麼樣,這個接口都不會出問題。 queue對load balance的處理是完美的。對於多個Consumer來說,RabbitMQ 使用循環的方式(round-robin)的方式均衡的發送給不同的Consumer。

Exchanges

Procuder Publish的Message進入了Exchange。接着通過routing keys, RabbitMQ會找到應該把這個Message放到哪個queue裏。queue也是通過這個routing keys來做的綁定。 有三種類型的Exchanges:direct, fanout, topic。每個實現了不同的路由算法(routing algorithm)。

  • Direct exchange: 如果 routing key 匹配, 那麼Message就會被傳遞到相應的queue中。其實在queue創建時,它會自動的以queue的名字作爲routing key來綁定那個exchange。
  • Fanout exchange: 會向響應的queue廣播。
  • Topic exchange: 對key進行模式匹配,比如ab可以傳遞到所有ab的queue。

Virtual hosts

每個virtual host本質上都是一個RabbitMQ Server,擁有它自己的queue,exchagne,和bings rule等等。 這保證了你可以在多個不同的application中使用RabbitMQ

rabbitmq “hello world”

hello world

主要步驟:

  1. 啓動rabbitmq server:rabbitmq-server start
  2. 與rabbitmq建立連接
  3. 聲明queue
  4. sender使用默認的exchage,發送消息
  5. handler接受消息,並處理。

producer(sender):

#! /usr/bin/python
# coding:utf-8
import pika

__author__ = 'hgf'

# 與rabbitmq建立連接,傳入的參數就是RabbitMQ Server的ip或者name
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))

#在這個鏈接上建立一個channel
channel = connection.channel()

# 聲明queue,創建名字爲hello的queue
channel.queue_declare("hello")

# 使用默認的exchage,發送消息
#Producer只能發送到exchange,它是不能直接發送到queue的。現在我們使用默認的exchange(名字是空字符)。這個默認的exchange允許我們發送給指定的queue
channel.basic_publish(exchange='',
                      routing_key="hello",
                      body="hello world"
                      )

# 關閉channel
channel.close()

consumer(handler):

#! /usr/bin/python
# coding:utf-8
import pika

__author__ = 'hgf'

# consumer處理消息的回調函數
def callback(ch, method, properties, body):
    print "[X] Recieved %r" % body


# 與rabbitmq建立連接
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()

# 聲明queue,創建名字爲hello的queue
channel.queue_declare("hello")

channel.basic_consume(callback,queue = "hello", no_ack=True)
print '[*] Waiting for messages. To exit press Ctrl+C'

channel.start_consuming()

==說明==: 回調函數的四個參數: ch:[channel] <pika.adapters.blocking_connection.BlockingChannel object at 0x287d0d0> method:[Deliver] <Basic.Deliver(['consumer_tag=ctag1.307f92b29cc44a6c882e39a40b5a1558', 'delivery_tag=1', 'exchange=', 'redelivered=False', 'routing_key=hello'])> properties:[屬性] <BasicProperties> body:[發送的內容] 'hello world'

任務分發機制

場景和實驗程序

當有Consumer需要大量的運算時,RabbitMQ Server需要一定的分發機制來balance每個Consumer的load。試想一下,對於web application來說,在一個很多的HTTP request裏是沒有時間來處理複雜的運算的,只能通過後臺的一些工作線程來完成。接下來我們分佈講解。

應用場景就是 RabbitMQ Server會將queue的Message分發給不同的Consumer以處理計算密集型的任務:

!任務分發機制](https://static.oschina.net/uploads/img/201510/08081632_m4JK.png "任務分發機制")

即:一個很複雜的工作使用多個consumer共同完成計算。

爲了是Consumer做的是計算密集型的工作,那就不能簡單的字符串了。在現實應用中,Consumer有可能做的是一個圖片的resize,或者是pdf文件的渲染或者內容提取。但是作爲Demo,還是用字符串模擬吧:通過字符串中的.的數量來決定計算的複雜度,每個.都會消耗1s,即sleep(1)

producer:

#! /usr/bin/python
# coding:utf-8
import pika
import sys

__author__ = 'hgf'

connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
channel.queue_declare("hello")

message = " ".join(sys.argv[1:]) or "hello world"
channel.basic_publish(exchange='',
                      routing_key="hello",
                      body=message
                      )
channel.close()

consumer:

#! /usr/bin/python
# coding:utf-8
import pika
import time

__author__ = 'hgf'

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


connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
channel.queue_declare("hello")

channel.basic_consume(callback,queue = "hello", no_ack=True)
print '[*] Waiting for messages. To exit press Ctrl+C'

channel.start_consuming()

==說明==:回掉函數爲什麼加上一句time.sleep(body.count('.')) 字符串中的.號的數量來決定計算的複雜度,每個.號都會消耗1s,即sleep(1) 當producer 一次發送含有一個點.,兩個點..,三個點...,的消息時,接收到他們的consumer會分別sleep 1s, 2s, 3s。sleep的時間長度代表consumer做計算所花費的時間長度。

Round-robin dispatching 循環分發

RabbitMQ的分發機制非常適合擴展,而且它是專門爲併發程序設計的。如果現在load加重,那麼只需要創建更多的Consumer來進行任務處理即可。對於負載還要加大怎麼辦?可以創建多個virtual Host,細化不同的通信類別了。

利用前小結準備的代碼實驗:

  1. 先啓動兩個 consumer
  2. 不停地運行 producer,並且在producer的參數入口帶上不同數量的點號.。例如:
python producer.py First message.
python producer.py Second message..
python producer.py Third message...
python producer.py Fourth message....
python producer.py Fifth message.....
  1. 分別查看兩個consumer的輸出:

consumer1的輸出:

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

consumer2的輸出:

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

默認情況下,RabbitMQ 會順序的分發每個Message。當每個收到ack後,會將該Message刪除,然後將下一個Message分發到下一個Consumer。這種分發方式叫做round-robin。

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)。在實驗程序consumer中,我們通過no_ack = True(channel.basic_consume(callback,queue = "hello", 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關閉了consumer程序,那麼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.  

==驗證實驗==驗證內容:驗證rabbitmq server沒有收到ack之前,message是不會丟失的。 驗證方案:將callbak函數的ch.basic_ack(delivery_tag = method.delivery_tag)刪除,不給rabbitmq server回覆ack響應,rabbitmq server 就不知道consumer是否處理了message,當有下一個consumer來,但是上一個consumer沒有 在規定的時間,或者沒有ack前就斷開了與rabbitmq server的連接(rabbitmq server 會認爲上一個consumer處理不了這個message),rabbitmq server會把同樣的message傳給這個consumer。

Message durability消息持久化

上一節中我們知道了即使Consumer異常退出,Message也不會丟失。但是如果RabbitMQ Server退出呢?軟件都有bug,即使RabbitMQ Server是完美毫無bug的(當然這是不可能的,是軟件就有bug,沒有bug的那不叫軟件),它還是有可能退出的:被其它軟件影響,或者系統重啓了,系統panic了。。。 爲了保證在RabbitMQ退出或者crash了數據仍沒有丟失,需要將queue和Message都要持久化。 queue的持久化需要在聲明時指定durable=Truechannel.queue_declare(queue='hello', durable=True)

上述語句執行不會有什麼錯誤,但是確得不到我們想要的結果,原因就是RabbitMQ Server已經維護了一個叫hello的queue,那麼上述執行不會有任何的作用,也就是hello的任何屬性都不會被影響。 聲明一個另外的名字的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。

==驗證實驗==驗證內容:驗證queue的持久化。 驗證方案:在聲明queue時,設置durable = True,即:channel.declare_queue("hello", durable = True)。並且在producer在publish消息的時候,指定一個properties,方式如下:

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

producer往rabbitmq server上發送數據,然後kill rabbitmq server的進程;然後啓動 rabbitmq server ,啓動consumer,consumer可以正常獲取rabbitmq被kill前producer傳入的數據。

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來細化你的設計。

分發到多Consumer(Publish/Subscribe)

上節中,我們把每個Message都是deliver到某個Consumer。在本節中,我們將會將同一個Message deliver到多個Consumer中。這個模式也被成爲 "publish / subscribe"

本節中,創建一個日誌系統,它包含兩個部分:第一個部分是發出log(Producer),第二個部分接收到並打印(Consumer)。 我們將構建兩個Consumer,第一個將log寫到物理磁盤上;第二個將log輸出的屏幕。

Exchanges

RabbitMQ 的Messaging Model就是Producer並不會直接發送Message到queue。實際上,Producer並不知道它發送的Message是否已經到達queue

Producer發送的Message實際上是發到了Exchange中。它的功能也很簡單:從Producer接收Message,然後投遞到queue中。++Exchange需要知道如何處理Message,是把它放到那個queue中,還是放到多個queue中++?這個rule是通過Exchange 的類型定義的。

含有Exchange的rabbitmq模型

我們知道有三種類型的Exchange:direct, topic 和fanout。 fanout就是廣播模式,會將所有的Message都放到它所知道的queue中。創建一個名字爲logs,類型爲fanout的Exchange: channel.exchange_declare(exchange='logs', type='fanout')

現在我們可以通過exchange,而不是routing_key來publish Message了:

channel.basic_publish(exchange='logs',
                      routing_key='',
                      body=message)

==rabbitmq list exchange 命令==: sudo rabbitmqctl list_exchanges 在列出的exchange中,amq.* exchanges(amq.* 表示exchange name 的通用形式,例如amq.direct,amq.topic) 和the default (unnamed)exchange是RabbitMQ默認創建的。

Temporary queues

截至現在,我們用的queue都是有名字的:第一個是hello,第二個是task_queue。使用有名字的queue,使得在Producer和Consumer之前共享queue成爲可能。 但是對於我們將要構建的日誌系統,並不需要有名字的queue。我們希望得到所有的log,而不是它們中間的一部分。而且我們只對當前的log感興趣。爲了實現這個目標,我們需要兩件事情:

  1. 每當Consumer連接時,我們需要一個新的,空的queue。因爲我們不對老的log感興趣。幸運的是,如果在聲明queue時不指定名字,那麼RabbitMQ會隨機爲我們選擇這個名字。方法:result = channel.queue_declare(),獲取result所代表的queue的名字的方法:result.method.queue
  2. 當Consumer關閉連接時,這個queue要被deleted。可以加個exclusive的參數。result = channel.queue_declare(exclusive=True)

==注意==:

  1. publish到一個不存在的exchange是被禁止的。

==channel.queue_declare()參數意思==:

  • queue: str或unicode;queue的名字,如果爲空,會自動創建
  • passive: bool;檢查queue是否存在。
  • durable: bool;是否持久化,在中斷後然可繼續
  • exclusive: bool;只允許被當前連接中的這個connection連接到這個queue
  • auto_delete:bool;在consumer斷開連接後刪除queue
  • arguments: dict;用戶自定義的鍵值對參數

Bindings綁定

現在我們已經創建了fanout類型的exchange和沒有名字的queue(實際上是RabbitMQ幫我們取了名字)。那exchange怎麼樣知道它的Message發送到哪個queue呢?通過bindings:綁定。

exchange和queue綁定

channel.queue_bind(exchange='logs',queue=result.method.queue)

==注意==:

  1. publish到一個不存在的exchange是被禁止的
  2. 如果沒有queue bindings exchange的話,log是被丟棄的。

==rabbitmq 查看已經有的綁定==: sudo rabbitmqctl list_bindings

最終版本

我們最終實現的數據流圖如下: 最終數據流圖

Producer,在這裏就是產生log的program,基本上和前幾個都差不多。最主要的區別就是publish通過了exchange而不是routing_key。

程序源碼:

producer:

#! /usr/bin/python
# coding:utf-8
import pika
import sys

__author__ = 'hgf'

con = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = con.channel()
channel.queue_declare()
exchange = channel.exchange_declare(exchange="log",exchange_type="fanout")

message = ' '.join(sys.argv[1:]) or "hello world"

channel.publish(exchange="log",routing_key='', body = message)
print(" [x] sent: %r") % message

consumer.py:print to screen

#! /usr/bin/python
# coding:utf-8
import pika

__author__ = 'hgf'

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

con = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = con.channel()
result = channel.queue_declare(exclusive=True)

exchange = channel.exchange_declare(exchange="log",exchange_type="fanout")

channel.queue_bind(result.method.queue,'log')
channel.basic_consume(callbak,result.method.queue,no_ack=True)
channel.start_consuming()

Routing 消息路由

Bindings綁定

綁定其實就是關聯了exchange和queue。或者這麼說:queue對exchagne的內容感興趣,exchange要把它的Message deliver到queue中。 實際上,綁定可以帶routing_key這個參數。其實這個參數的名稱和basic_publish 的參數名是相同了。爲了避免混淆,我們把它成爲binding key。 例子:使用一個key來創建binding

channel.queue_bind(exchange=exchange_name,
                   queue=queue_name,
                   routing_key='black')

對於++fanout的exchange來說,這個參數是被忽略的++。

Direct exchange

Direct exchange的路由算法非常簡單:通過binding key的完全匹配,可以通過下圖來說明。

direct exchange

exchange X和兩個queue綁定在一起。Q1的binding key是orange。Q2的binding key是black和green。 當publish key是orange時,exchange會把它放到Q1。如果是black或者green那麼就會到Q2。其餘的Message都會被丟棄。

Multiple bindings

多個queue綁定同一個key是可以的。對於下圖的例子,Q1和Q2都綁定了black。也就是說,對於routing key是black的Message,會被deliver到Q1和Q2。其餘的Message都會被丟棄。

Multi bingdings

日誌系統最終版本

producer_log.py:

#! /usr/bin/python
#coding:utf-8

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()

message = "".join(sys.argv[1:]) or "hello world"
ex = channel.exchange_declare(exchange="log_direct",exchange_type="direct")
channel.basic_publish(exchange="log_direct",routing_key=sys.argv[1],body=message)
connection.close()

consumer_log.py

#!/usr/bin/python
#coding:utf-8

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
channel.exchange_declare(exchange="log_direct", exchange_type="direct")

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

serverities = sys.argv[1:]
if not serverities:
    print "Usage: %s [info] [warnning] [error]" %sys.argv[0]
    sys.exit(1)
for serverity in serverities:
    channel.queue_bind(exchange="log_direct",queue=queue_name,routing_key=serverity)

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

channel.basic_consume(callbak,queue=queue_name, no_ack=True)
channel.start_consuming()

運行時,使用python produce_log.py info創建一個info queue,然後將exchange=log_direct 與它綁定;使用python produce_log.py error創建一個 error queue,並與exchange=log_direct綁定。

使用consumer消費產生的消息。使用python consumer_log.py info > info.log將info的消息保存到文件,使用python consumer_log.py error,將消息打印到屏幕。

使用主題進行消息分發

上節我們實現了一個簡單的日誌系統。Consumer可以監聽不同severity的log。但是,這也是它之所以叫做簡單日誌系統的原因,因爲是僅僅能夠通過severity設定。不支持更多的標準。 比如syslog unix的日誌工具,它可以通過severity (info/warn/crit...) 和模塊(auth/cron/kern...)。這可能更是我們想要的:我們可以僅僅需要cron模塊的log。 爲了實現類似的功能,我們需要用到topic exchange。

Topic exchange

對於Message的routing_key是有限制的,不能使任意的。格式是以點號“."分割的字符表。比如:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit"。你可以放任意的key在routing_key中,當然最長不能超過255 bytes

==routing_key特殊字符==(在正則表達式裏叫元字符):

    • (星號) 代表任意 一個單詞,不是字符。例如:routing_key="cron.*",那麼"cron.ass"匹配,而"cron.ass.xxx"就不匹配了,因爲ass可視爲一個單詞,而ass.xxx中間帶了.,就表示兩個單詞了。
  • # (hash) 0個或者多個單詞,不是字符。例如:routing_key="cron.#",那麼"cron.ass","cron","cron.aaa.bbb.ccc"匹配,因爲#可以匹配0個單詞或多個單詞,每個單詞前面多一個.作爲分割。

==說明==: 在rabbitmq中.爲單詞的分界符號。

主題消息例子

Producer發送消息時需要設置routing_keyrouting_key包含三個單詞和兩個點號。第一個key是描述了celerity(靈巧,敏捷),第二個是colour(色彩),第三個是species(物種):"<celerity>.<colour>.<species>"。

在這裏我們創建了兩個綁定: Q1 的binding key 是"*.orange.*"; Q2 是 "*.*.rabbit" 和 "lazy.#":

  • Q1 感興趣所有orange顏色的動物
  • Q2 感興趣所有的rabbits和所有的lazy

比如routing_key是 "quick.orange.rabbit"將會發送到Q1和Q2中。消息"lazy.orange.elephant" 也會發送到Q1和Q2。但是"quick.orange.fox" 會發送到Q1;"lazy.brown.fox"會發送到Q2。"lazy.pink.rabbit" 也會發送到Q2,但是儘管兩個routing_key都匹配,它也只是發送一次。"quick.brown.fox" 會被丟棄。

如果發送的單詞不是3個呢? 要看情況,因爲#是可以匹配0個或任意個單詞。比如"orange" or "quick.orange.male.rabbit",它們會被丟棄。如果是lazy那麼就會進入Q2。類似的還有 "lazy.orange.male.rabbit",儘管它包含四個單詞。

由於有"*" (star) and "#" (hash), Topic exchange 非常強大並且可以轉化爲其他的exchange:

如果binding_key 是 "#" 它會接收所有的Message,不管routing_key是什麼,就像是fanout exchange

如果 "*" (star) and "#" (hash) 沒有被使用,那麼topic exchange就變成了direct exchange

重新定義日誌系統

現在我們要refine我們上篇的日誌系統。routing keys 有兩個部分: "<facility>.<severity>"。

producer.py

#! /usr/bin/python
# coding:utf-8
import pika
import sys

__author__ = 'hgf'

connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
channel.exchange_declare(exchange="topic_log", exchange_type="topic")

message = "".join(sys.argv[2:]) or "hello world"

channel.basic_publish(exchange="topic_log",routing_key=sys.argv[1],body=message)
channel.close()
connection.close()

運行時類似之前定義過的"<facility>.<severity>",例如:python producer.py "cron.*"

consumer.py

#! /usr/bin/python
# coding:utf-8
from netaddr.ip.iana import query
import pika
import sys
from scss.extension.core import change_color

__author__ = 'hgf'


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

connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
channel.exchange_declare(exchange="topic_log", exchange_type="topic")
result = channel.queue_declare()
queue_name = result.method.queue

serverities =sys.argv[1:]
if not serverities:
    print("Usage: %s [info] [warnning] [error]" % sys.argv[0])

for serverity in serverities:
    channel.queue_bind(queue_name,"topic_log",routing_key=serverity)

channel.basic_consume(callbak,queue_name,no_ack=True)
channel.start_consuming()

運行時類似之前定義過的"<facility>.<severity>",可聯繫定義多個routing_key類型,例如:python producer.py "cron.*" "kern.#"

適用於雲計算集羣的遠程調用(RPC)

在雲計算環境中,很多時候需要用它其他機器的計算資源,我們有可能會在接收到Message進行處理時,會把一部分計算任務分配到其他節點來完成。那麼,RabbitMQ如何使用RPC呢?在本節中,我們將會通過其它節點求來斐波納契完成示例。

客戶端接口 Client interface

爲了展示一個RPC服務是如何使用的,我們將創建一段很簡單的客戶端class。 它將會向外提供名字爲call的函數,這個call會發送RPC請求並且阻塞知道收到RPC運算的結果。代碼如下:

fibonacci_rpc = FibonacciRpcClient()  
result = fibonacci_rpc.call(4)  
print "fib(4) is %r" % (result,) 

回調函數隊列 Callback queue

總體來說,在RabbitMQ進行RPC遠程調用是比較容易的。client發送請求的Message然後server返回響應結果。爲了收到響應client在publish message時需要提供一個callback(回調)的queue地址。code如下:

result = channel.queue_declare(exclusive=True)  
callback_queue = result.method.queue  
  
channel.basic_publish(exchange='',  
                      routing_key='rpc_queue',  
                      properties=pika.BasicProperties(  
                            reply_to = callback_queue,  
                            ),  
                      body=request)  
  
# ... and some code to read a response message from the callback_queue ... 

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

  • delivery_mode: 持久化一個Message(通過設定值爲2)。其他任意值都是非持久化。
  • 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)。

相關id Correlation id

在上個小節裏,實現方法是對每個RPC請求都會創建一個callback queue。這是不高效的。幸運的是,在這裏有一個解決方法:爲每個client創建唯一的callback queue。【對每個RPC請求創建queue不高效,對每個client創建一個queue比較好

這又有其他問題了:收到響應後它無法確定是否是它的,因爲所有的響應都寫到同一個queue了。上一小節的correlation_id在這種情況下就派上用場了:對於每個request,都設置唯一的一個值,在收到響應後,通過這個值就可以判斷是否是自己的響應。如果不是自己的響應,就不去處理。

遠程調用過程小結

遠程調用過程小結

工作流程:

  • 當客戶端啓動時,它創建了匿名的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發送時的一致那麼就將返回響應。

遠程調用實現

client.py

#! /usr/bin/python
# coding:utf-8
import pika
import uuid

__author__ = 'hgf'

class Fibona(object):
    def __init__(self):
        self.response=None
        self.cor_id = str(uuid.uuid4())

        self.connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
        self.channel = self.connection.channel()
        result = self.channel.queue_declare()
        self.callbak_queue = result.method.queue
        self.channel.basic_consume(self.on_response, queue=self.callbak_queue, no_ack=True)

    def on_response(self, ch, method, proper, body):
        if self.cor_id == proper.correlation_id:
            self.response = body


    def call(self,n):
        self.channel.basic_publish(
            exchange='',
            routing_key='rpc',
            properties=pika.BasicProperties(
                reply_to = self.callbak_queue,
                correlation_id=self.cor_id
            ),
            body=str(n)
        )

        while self.response is None:
            self.connection.process_data_events()
        return int(self.response)

fibo_rpc = Fibona()

print "[X] Requesting fib(30)"
response = fibo_rpc.call(30)
print "[.] answer %r" % response

客戶端實現步驟:

  1. 連接rabbitmq server
  2. 監聽結果queue,此時相當於server發送消息的消費者。先驗證消息是不是本次任務的返回結果,然後獲取結果。
  3. 實現請求函數call,向server端發送消息,相當於消息的生產者,將生產的消息發送給rpc queue。

server.py

#! /usr/bin/python
# coding:utf-8
import pika
import sys

__author__ = 'hgf'


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

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

    print "[.] fi(%d)" % n

    response = fi(n)
    ch.basic_publish(exchange="",
                     routing_key=proper.
                     reply_to,
                     properties=pika.BasicProperties(correlation_id=proper.correlation_id),
                     body=str(response)
                     )
    ch.basic_ack(delivery_tag=method.delivery_tag)


connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()

channel.queue_declare(queue = "rpc")

channel.basic_qos(prefetch_count=1)
channel.basic_consume(on_request,"rpc")

print "[X] Waiting RPC requests"

channel.start_consuming()

服務端實現步驟:

  1. 連接rabbitmq
  2. 實現斐波那契函數
  3. 消費client發送的message,即獲取n並且計算它的斐波那契值
  4. 根據client對返回消息的要求,將計算返回到特定的queue,並設置對應的correlation_id,此時相當於消息的生產者。

ProtoBuf(Google Protocol Buffer)

什麼是ProtoBuf

一種輕便高效的結構化數據存儲格式,可以用於結構化數據串行化,或者說序列化。它很適合做數據存儲RPC 數據交換格式。可用於通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。目前提供了 C++JavaPython 三種語言的 API。

它可以作爲RabbitMQ的Message的數據格式進行傳輸,由於是結構化的數據,這樣就極大的方便了Consumer的數據高效處理。當然了你可能說使用XML不也可以嗎?與XML相比,ProtoBuf有以下優勢:

  • 簡單
  • size小了3-10倍
  • 速度快樂20-100倍
  • 易於編程
  • 減小了語義的歧義

使用例子

待學習後添加上! 詳情見google protocal buff學習(python版本)

Publisher的消息確認機制

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

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

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

事務機制 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 。

消息在什麼時候確認

broker 將在下面的情況中對消息進行 confirm:

  • broker 發現當前消息無法被路由到指定的 queues 中(如果設置了 mandatory 屬性,則 broker 會先發送 basic.return)
  • 非持久屬性的消息到達了其所應該到達的所有 queue 中(和鏡像 queue 中)
  • 持久消息到達了其所應該到達的所有 queue 中(和鏡像 queue 中),並被持久化到了磁盤(被 fsync)
  • 持久消息從其所在的所有 queue 中被 consume 了(如果必要則會被 acknowledge)

broker 會丟失持久化消息,如果 broker 在將上述消息寫入磁盤前異常。在一定條件下,這種情況會導致 broker 以一種奇怪的方式運行。例如,考慮下述情景:

  1. 一個 client 將持久消息 publish 到持久 queue 中
  2. 另一個 client 從 queue 中 consume 消息(注意:該消息具有持久屬性,並且 queue 是持久化的),當尚未對其進行 ack
  3. broker 異常重啓
  4. client 重連並開始 consume 消息

在上述情景下,client 有理由認爲消息需要被(broker)重新 deliver 。但這並非事實:重啓(有可能)會令 broker 丟失消息。爲了確保持久性,client 應該使用 confirm 機制。如果 publisher 使用的 channel 被設置爲 confirm 模式,publisher 將不會收到已丟失消息的 ack(這是因爲 consumer 沒有對消息進行 ack ,同時該消息也未被寫入磁盤)。

mandatory和immediate標誌位

首先要區別AMQP協議mandatory和immediate標誌位的作用。

mandatory和immediate是AMQP協議中basic.pulish方法中的兩個標誌位,它們都有當消息傳遞過程中不可達目的地時將消息返回給生產者的功能。具體區別在於:

  1. mandatory標誌位 當mandatory標誌位設置爲true時,如果exchange根據自身類型和消息routeKey無法找到一個符合條件的queue,那麼會調用basic.return方法將消息返還給生產者;當mandatory設爲false時,出現上述情形broker會直接將消息扔掉。
  2. immediate標誌位 當immediate標誌位設置爲true時,如果exchange在將消息route到queue(s)時發現對應的queue上沒有消費者,那麼這條消息不會放入隊列中。當與消息routeKey關聯的所有queue(一個或多個)都沒有消費者時,該消息會通過basic.return方法返還給生產者。

展望

在此我們已經學完了基本的rabbitmq的思想,下一步可以學習學習rabbitmq服務端的擴展,優化和管理方面的知識。

可以學習書目

  • 《rabbitmq in action》

{賀廣福}(heguangfu)(tm) @2015-9-27 :laughing:

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