RPC通訊原理(未完,先睡覺)

linhaifeng發表於2017-03-06

一 背景

    OpenStack 各元件之間是通過 REST 介面進行相互通訊,比如Nova、Cinder、Neutron、Glance直間的通訊都是通過keystone獲取目標的endpoint,即api(至於到底什麼是restful風格的api請點選紅色連結)

    而各元件內部則採用了基於 AMQP 模型的 RPC 通訊。

    為了讓大家有更為直觀的認識,我們單拿出cinder的架構來舉例,至於cinder內部包含的具體元件,大家不必過於糾結,我們在後續的章節會詳細介紹,這裡只需要有一個直觀的體會:rest和rpc分別在openstack的元件間和元件內通訊所處位置。

 

 

1.Cinder-api 是 cinder 服務的 endpoint,提供 rest 介面,負責處理 client 請求,並將 RPC 請求傳送至 cinder-scheduler 元件。


2.  Cinder-scheduler 負責 cinder 請求排程,其核心部分就是 scheduler_driver, 作為 scheduler manager 的 driver,負責 cinder-volume 具體的排程處理,傳送 cinder RPC 請求到選擇的 cinder-volume。


3. Cinder-volume 負責具體的 volume 請求處理,由不同後端儲存提供 volume 儲存空間。


目前各大儲存廠商已經積極地將儲存產品的 driver 貢獻到 cinder 社群。目前支援的後端儲存系統,可參見:https://wiki.Openstack.org/wiki/CinderSupportMatrix
cinder內部各元件功能:選看

(注意:glance的內部元件的通訊是直接呼叫的自己的api,除此之外,cinder,nova,neutron的內部元件通訊都是基於rpc機制)

二 為何

 二 OpenStack RPC通訊

Openstack 元件內部的 RPC(Remote Producer Call)機制的實現是基於 AMQP(Advanced Message Queuing Protocol)作為通訊模型,從而滿足元件內部的鬆耦合性。AMQP 是用於非同步訊息通訊的訊息中介軟體協議,AMQP 模型有四個重要的角色:

  • Exchange:根據 Routing key 轉發訊息到對應的 Message Queue 中
  • Routing key:用於 Exchange 判斷哪些訊息需要傳送對應的 Message Queue
  • Publisher:訊息傳送者,將訊息傳送的 Exchange 並指明 Routing Key,以便 Message Queue 可以正確的收到訊息 
  • Consumer:訊息接受者,從 Message Queue 獲取訊息 (收郵件的人)

訊息釋出者 Publisher 將 Message 傳送給 Exchange 並且說明 Routing Key。Exchange 負責根據 Message 的 Routing Key 進行路由,將 Message 正確地轉發給相應的 Message Queue。監聽在 Message Queue 上的 Consumer 將會從 Queue 中讀取訊息。

Routing Key 是 Exchange 轉發資訊的依據,因此每個訊息都有一個 Routing Key 表明可以接受訊息的目的地址,而每個 Message Queue 都可以通過將自己想要接收的 Routing Key 告訴 Exchange 進行 binding,這樣 Exchange 就可以將訊息正確地轉發給相應的 Message Queue。

圖  AMQP 訊息模型

圖 2. AMQP 訊息模型

 

Publisher(發郵件的人

Exchange(相當於郵局

Message Queue(相當於自己家門口的郵筒

Routing Key(自己的家門口郵筒都需要在郵局註冊/binding

Consumer(收郵件的人

 

AMQP 定義了三種型別的 Exchange,不同型別 Exchange 實現不同的 routing 演算法:

  • Direct Exchange:Point-to-Point 訊息模式,訊息點對點的通訊模式,Direct Exchange 根據 Routing Key 進行精確匹配,只有對應的 Message Queue 會接受到訊息
  • Topic Exchange:Publish-Subscribe(Pub-sub)訊息模式,Topic Exchange 根據 Routing Key 進行模式匹配,只要符合模式匹配的 Message Queue 都會收到訊息
  • Fanout Exchange:廣播訊息模式,Fanout Exchange 將訊息轉發到所有繫結的 Message Queue

OpenStack 目前支援的基於 AMQP 模型的 RPC backend 有 RabbitMQ、QPid、ZeroMQ,對應的具體實現模組在 cinder 專案下 Openstack/common/RPC/目錄下,impl_*.py 分別為對應的不同 backend 的實現。其中RabbitMQ是最常用的一個。

三 RabbitMQ詳解

RabbitMQ是一個訊息代理。它的主要原理相當簡單:接收並且轉發訊息。

你可以把它想象成一個郵局:當你把你的郵箱放到油筒裡時,肯定有郵遞員幫你把它分發給你的接收者。RabiitMQ同時是:一個郵筒、一個郵局,一個郵遞員。

RabbitMQ和郵局的主要不同是RabbitMQ處理的不是紙質,它接收、儲存並且分發二進位制資料(即訊息)

3.1 RabbitMQ的常用術語如下

producer(生產者):即一個生產訊息的程式,只負責生產(sending),簡稱"P"

queue(佇列):即一個佇列就是一個郵筒的名字,它整合在RabbitMQ內部,雖然訊息流通過RabbitMQ和你的應用程式,但其實訊息可以只儲存在一個佇列裡。佇列不受任何限制,它可以如你所願存放很多訊息,它本質上就是一個無限的緩衝區。許多生產者producer可以發訊息到一個佇列,許多消費者consumer可以嘗試從一個佇列queue裡接收資料。

consumer(消費者):即一個接等待接收訊息的程式,簡稱“C”

注意:producer,consumer,以及broker(即訊息代理,指的就是RabbitMQ)不是必須要在同一臺機器上,實際上,大多數情況下,三者是分佈在不同的機器上

3.2 最簡單的用法:'Hello World'

RabbitMQ libraries

RabbitMQ遵循AMQP 0.9.1,AMQP是開源的,通用的訊息協議,RabbitMQ支援很多不同語言的客戶端,作為python使用者,我們使用Pika模組,可以使用pip工具安裝之,目前pika最新版本為0.10.0。

 

這裡舉一個很簡單的例子,傳送一個訊息,接收它並且列印到螢幕。我們需要做的是:寫兩個程式,一個負責發訊息,另外一個負責接收訊息並且列印,如下圖

=============傳送端=============

我們的第一個程式名send.py,用來傳送一個訊息到佇列queue中

第一步:第一件我們需要做的事情就是與RabbitMQ服務建立連結

#!/usr/bin/env python
import pika
connection=pika.BlockingConnection(pika.ConnectionParameters(
    host='localhost',
    port=5672,
    virtual_host='/', #虛擬主機,起到一個名稱空間的作用
    credentials=pika.PlainCredentials('admin','admin') #使用者名稱,密碼
))

channel=connection.channel()

現在我們可以連線RabbitMQ服務了(broker),只不過此時連線的是本機的,如果RabbitMQ位於遠端主機,那麼host="遠端主機ip"

第二步:接下來,我們需要確定接收訊息的佇列是存在的,如果我們傳送一條訊息給一個不存在的佇列,RabbitMQ將把這條訊息當做垃圾處理。因此就讓我們為將要分發的訊息建立一個佇列,我們可以將該佇列命名為hello

channel.queue_declare(queue='hello')

第三步:此時,我們就已經為傳送訊息做好準備了,我們的第一個訊息就只包含一個字串“Hello world”吧,並且我們將這條訊息傳送到hello佇列。

需要強調的一點是:在RabbitMQ中,任何訊息都不可能直接傳送到佇列queue,訊息必須先傳送給exchange,後面我們會詳細介紹exchane,此處不必細究,我們只需要知道如何使用預設的exchange就可以了,即定義引數exchange='',空代表使用預設的。exchange允許我們明確地標識訊息應該發往哪個佇列,對列名需要通過routing_key這個引數被定義。

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

第四步:在退出這個程式之前,我們需要確定網路快取被重新整理並且我們的訊息真的被傳給了RabbitMQ,因此我們需要優雅地退出這個連線(這種退出方式可以確定快取重新整理和訊息傳到了RabbitMQ)

connection.close()

注意: 如果執行send.py後沒有看到'Sent'這條列印內容,可能的原因是RabbitMQ的啟動盤沒有足夠的剩餘空間(預設情況,安裝RabbitMQ的磁碟最小需要1Gb的剩餘空間),沒有足夠的剩餘空間就會拒絕接收訊息。我們可以堅持RabbitMQ的日誌檔案來減少這種空間的限制,點選來檢視如何設定disk_free_limit

 

=============接收端=============

我們的第二個程式receive.py將從佇列中接收訊息並且列印

第一步:同樣的,首先我們需要做的也是連線RabbitMQ,負責連線RabbitMQ的程式碼和send.py中的一樣。

第二步:這一步需要做的也要確定佇列的存在,我們可以執行多次queue_declare,但是無論執行多少次,將只建立一次佇列

channel.queue_declare(queue='hello') #這個操作是冪等的

 

你肯定會問:為什麼我們要再次宣告佇列呢,我們已經在send.py中宣告過一次了啊,沒錯,如果你能確定佇列已經存在了,完全沒有必要重新再定義一次,比如send.py先執行了,那麼佇列肯定是存在的。

但問題的所在就在於,我們根本無法確定,到底是send.py先執行還是receive.py先執行,因此最保險的做法就是在兩個程式中都定義上這一條,反正是如果佇列已經有了就不會重新建立。

檢視RabbitMQ有多少條訊息,(授權使用者)可以使用rabbitmqctl tool:

$ sudo rabbitmqctl list_queues
Listing queues ...
hello    0
...done.

第三步:比起發訊息來說,從佇列中收訊息是更加複雜一點,它訂閱一個callback函式到一個佇列,一旦收到訊息,這個callback函式就會被(Pika庫)呼叫。此處我們就寫一個簡單的callback函式(只完成列印功能)

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

 

第四步:我們需要告訴RabbitMQ,這個特殊的callback函式應該從我們的佇列接收訊息。

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

 

要想讓上面這條命令正確執行,我們必須保證佇列我們訂閱的佇列是存在的,幸運的是,我們很有信心,因為我們已經在上面建立了一個佇列‒使用queue_declare。 

no_ack引數將在後面描述

第五步:最後,我們進入一個無休止的迴圈,等待資料,並且在必要時執行回撥callback

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

 

=============完整版=============

send.py

#python AMQP SDK
import pika

#獲得連線物件
connection=pika.BlockingConnection(pika.ConnectionParameters(
    host='localhost',
    port=5672,
    virtual_host='/', #虛擬主機,起到一個名稱空間的作用
    credentials=pika.PlainCredentials('admin','admin') #使用者名稱,密碼
))

#連線rabbitmq
channel=connection.channel()
channel.queue_declare(queue='hello') #建立佇列hello

channel.basic_publish(exchange='',
                      routing_key='hello',
                      body='Hello World')

print(" [x] Sent 'Hello World!'")
connection.close()

receive.py

import pika
connection=pika.BlockingConnection(pika.ConnectionParameters(
    host='localhost',
    port=5672,
    virtual_host='/', #虛擬主機,起到一個名稱空間的作用
    credentials=pika.PlainCredentials('admin','admin') #使用者名稱,密碼
))
channel=connection.channel()
channel.queue_declare(queue='hello')

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

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

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

 

=============測試=============

執行send.py發訊息

 $ python send.py
 [x] Sent 'Hello World!'

 

producer程式,即send.py每次執行完畢後都會停止,我們可以接收資訊,即執行receive.py

$ python receive.py
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'Hello World!'

 

此時reveive.py不會退出,我們可以在其他終端開啟send.py來發訊息,然後觀察receive.py收訊息

3.3 Work Queues

    使用pika 0.10.0 python客戶端

    在3.2小節我們編寫了程式:從一個被命名的佇列中send和receive訊息。本節,我們將建立一個Work佇列,它將被用來在多個wokers中分發耗費時間的任務。

Work佇列(又稱為任務佇列)背後的主要思想是為了避免立即執行一個資源密集型任務(耗時),並等待它完成。相反,我們不會等它的完成,我們會排程它之後要完成的任務。我們將任務封裝為訊息並將其傳送給佇列。後臺執行的worker程式將彈出任務並最終執行任務。當您執行許多worker矜持的時候,這些任務將在它們之間共享。

    這個概念在web應用領域非常有用,比如web應用不能在一個短HTTP請求視窗期間處理一個複雜的任務

=============傳送端=============

    在3.2小節,我們傳送一個訊息“Hello World”,現在我們將傳送代表複雜任務的字串(用來模擬耗時的任務,即將揭曉)。我們沒有一個真實的任務,如影象進行調整或PDF檔案被渲染,所以讓我們通過time.sleep()函式偽造一個耗時的任務,比如,一個偽造的任務被描述成Hello...,該任務將花費三秒鐘(幾個.就花費幾秒鐘)。

    我們稍微修改下3.2小節中send.py的程式碼,來允許任意的資料能通過命令列被髮送,這個程式就將排程任務給work佇列,程式的檔名new_task.py

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)

 =============接收端=============

    同樣我們在3.2小節定義的receive.py也需要做一些修改:訊息體中包含幾個點,它就需要偽造執行幾秒鐘。它將從佇列中彈出訊息並且執行,檔名worker.py

import time

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

 

 =============round-robin排程=============

new_task.py

#_*_coding:utf-8_*_
#!/usr/bin/env python
import pika
import sys

#獲得連線物件
connection=pika.BlockingConnection(pika.ConnectionParameters(
    host='192.168.31.106',
    port=5672,
    virtual_host='/', #虛擬主機,起到一個名稱空間的作用
    credentials=pika.PlainCredentials('admin','admin') #使用者名稱,密碼
))

#連線rabbitmq
channel=connection.channel()
channel.queue_declare(queue='task_queue') #建立佇列hello

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(建立多個這種檔案)

#_*_coding:utf-8_*_
#!/usr/bin/env python
import pika
import time
connection=pika.BlockingConnection(pika.ConnectionParameters(
    host='192.168.31.106',
    port=5672,
    virtual_host='/', #虛擬主機,起到一個名稱空間的作用
    credentials=pika.PlainCredentials('admin','admin') #使用者名稱,密碼
))
channel=connection.channel()
channel.queue_declare(queue='task_queue')

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

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

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

 

同時啟動多個worker.py

shell1$ python worker.py
 [*] Waiting for messages. To exit press CTRL+C
shell2$ python worker.py
 [*] Waiting for messages. To exit press CTRL+C

 

多次執行new_tasks.py

shell3$ python new_task.py First message.
shell3$ python new_task.py Second message..
shell3$ python new_task.py Third message...
shell3$ python new_task.py Fourth message....
shell3$ python new_task.py Fifth message.....

 

檢視workers,即worker.py的執行結果

shell1$ python worker.py
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'First message.'
 [x] Received 'Third message...'
 [x] Received 'Fifth message.....'
shell2$ python worker.py
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'Second message..'
 [x] Received 'Fourth message....'

 

結論:預設,rabbitmq將傳送每個訊息到下一個consumer(即worker.py),按順序一個個的來。平均每個consumer將得到相同數量的訊息。這種分發訊息的方式叫做round-robin。

 =============Message acknowledgment訊息確認 =============

    處理一個任務可能需要花費幾秒鐘,你肯定會好奇,如果consumers中的一個(即woker.py),在開始一個耗時很長的任務但是還沒來得及完成任務的情況下就死掉了,那應該怎麼辦。就我們當前的程式碼而言,RabbitMQ一旦發訊息分發給consumer了,它就會立即從記憶體中移除這條訊息。這種情況下,如果你殺死了一個woker(比如worker.py,我們將丟失這條正在處理的訊息),我們也將丟失傳送給該特定woker但尚未處理的所有資訊。

    很明顯,我們並不想丟失任何訊息/任務,如果一個woker死了,我們希望把這個任務分發給另外一個woker。

    為了確定訊息永不丟失,RabbitMQ支援訊息確認( message acknowledgments

    consumer返回一個ack給RabbitMQ,告知RabbitMQ一條特定的訊息已經被接收了、執行完了、並且RabbitMQ可以自由刪除該任務/訊息了

    如果一個consumer死了(它的channel是掛壁了,連線是關閉了,或者TCP連線丟失)並且沒有傳送ack,RabbitMQ將會知道一個訊息沒有被完全執行完畢,並且將該訊息重新放入佇列中。這種方式你可以確認沒有訊息會丟失 。

    這裡沒有任何訊息超時;當consumer死掉以後,RabbitMQ將重新分發這個訊息。即使執行一個訊息需要花很長很長的時間,這種方式仍然是好的處理方式。

    訊息確認(Message acknowledgments)預設是被開啟的,在前面的例子裡,我們明確地將其關閉掉了,通過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) #傳送ack

channel.basic_consume(callback,
                      queue='hello') #去掉no_ack=True,預設就是False

 

    使用這種程式碼,我們可以確定是否你殺死了一個worker(當它正在處理一個訊息的時候通過CTRL+C殺死它),沒有訊息會丟失,沒返回ack就死掉的worker,不久後訊息就會被重新分發

    注意:一個經常性的錯誤是,callback函式中缺少back_ack,它是一個簡單的工作,但是結果是五花八門的,當你的客戶端退出後,訊息將會重新分發,rabbitmq將佔用越來越多的記憶體由於它不能夠釋任何沒有ack迴應(unacked)的訊息

    為了除錯這種錯誤,你可以使用rabbitmqctl來列印messages_unacknowledged

$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello    0       0
...done.

 

 

 

 

=============Message durability訊息持久化 =============

     我們已經學習瞭如何確定該

 =============Fair dispatch合理排程 =============

3.2小節我們介紹了

3.4

3.5

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

四 參考文件

http://www.rabbitmq.com/tutorials/tutorial-one-python.html

https://www.ibm.com/developerworks/cn/cloud/library/1403_renmm_opestackrpc/

 

相關文章