openstack開發實踐(五):RPC遠程調用解析

RPC服務實現分析

openstack的項目(如nova,cinder,glance以及neutron等)中,各個組件之間主要是通過REST API接口進行通信,而同一個組件內部(比如nova中的nova-scheduler與nova-compute通信)都採用基於AMQP通信模型的RPC通信。AMQP是一種提供統一消息服務的應用層標準協議,是應用層協議的一個開放標準,爲面向消息的中間件設計。基於此協議的客戶端與消息中間件可傳遞消息,並不受客戶端/中間件同產品,不同的開發語言等條件的限制。在基於AMQP實現的模型中主要有5個比較重要的概念(角色):

  1. Exchange:相當於路由器,可以根據一定的規則(即Routing Key)最終把消息轉發到相應的消息隊列中去。
  2. Routing Key:用來告訴Exchange如何進行消息路由,即哪些消息應該發送到哪些消息隊列中去。實際上它是一個虛擬地址,通過它可以確定如何路由一個特定消息。
  3. Binding Key:用來創建消息隊列Queue和交換器Exchange綁定關係的實現。
  4. Publisher:消息的生產者,有人也稱它爲消息的發送者。它是消息的來源,消息發送時會指定目標Exchange以及Routing Key,這樣可以保證正確的消息隊列收到此消息。
  5. Consumer:消息的消費者,它主要是從消息隊列中獲取消息並處理。
    不同角色之間的關係如下圖所示,一個Exchange可以對接多個消息隊列,根據不同的Routing Key,最終可以把消息分別發送到相應的消息隊列中,不同的消費者最終從各自的消息隊列中獲取消息進行消費。
    在這裏插入圖片描述
    在openstack中,AMQP的實現主要使用的是RabbitMQ,RabbitMQ是一個開源的消息隊列實現方式,是一種消息中間件,其官方的地址爲https://www.rabbitmq.com,這裏通過官方文檔中的“Hello Word”案例演示一下RabbitMQ的消息收發過程,參考文檔爲https://www.rabbitmq.com/tutorials/tutorial-one-python.html,本示例設計的架構如下圖所示,其中p爲生產者Publisher,c爲消費者consumer,hello爲name爲hello的消息隊列queue:
    在這裏插入圖片描述
    首先建立生產者和RabbitMQ Server的連接。代碼如下:
import pika

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

執行完上述代碼之後,就建立一個和本地(localhost,如果要和遠程機器則更換爲相應的ip地址)的RabbitMQ Server的連接connection以及信道channel,信道channel是爲了能夠複用TCP連接connection而引入的概念,這樣在需要建立多個連接的時候,通過複用TCP連接,能夠很好地節約資源消耗。
接下來要創建接受端的消息隊列,代碼如下:

channel.queue_declare(queue='hello world')

到這裏,我們便完成了消息發送的準備工作,下面就是把生產者的消息發送到這個消息隊列中。生產者不能直接把消息發送到消息隊列中,生產者發送的消息,首先應該到達exchange,然後再由Exchange將消息路由到不同隊列中。發送“Hello World”的代碼如下:

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

這裏的routing_key參數用來指明消息應該被哪個消息隊列來接收,body是消息體,exchange參數即指明消息需要的Exchange,這裏的參數爲‘’表明使用的是Default Exchange,如果需要某一種Exchange則需要使用如下語句聲明之後,將exchange參數替換爲‘logs’

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

發送完消息並推出之前,我們需要關閉連接,使用如下的代碼:

connection.close()

整個消息的發送端代碼最終如下所示:

#!/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()

對於接收端代碼來說,同樣需要建立與RabbitMQ Server的連接和信道,聲明消息隊列。之後需要定義消費信息的內容,即Server端接受消息並進行處理的過程,這一過程依靠callback函數來實現,代碼如下:

def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)
channel.basic_consume(queue='hello',
                      auto_ack=True,
                      on_message_callback=callback)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

basic_consume()是用來消費信息的,通過對call_back的調用來實現,callback函數則定義具體的操作,比如打印消息等。最終消息接受端的代碼如下:

#!/usr/bin/env python
import pika

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

channel.queue_declare(queue='hello')


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


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

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

在openstack RPC中創建Server並實現Client向Server發送請求的分析(以nova爲例)

結合源碼分析openstack中對於RPC通信的具體使用。在openstack中對於消息隊列RabbitMQ的具體使用主要是通過oslo_messaging庫來實現的,oslo.messaging庫就是把rabbitmq的Python庫做了封裝,在openstack中調用RPC通信就要調用oslo.messaging庫,爲RPC和事件通知提供一套統一的接口。下面以nova-compute啓動RPC Server和在創建虛擬機build_instance過程中的RPC遠程調用爲例來看一下oslo_messging在RPC通信中的具體應用。

nova-compute中啓動RPC Server

在上篇教程《openstack開發實踐(四):Nova源碼解析》https://blog.csdn.net/weixin_41977332/article/details/104569906中介紹nova-compute服務的啓動,有介紹到RPC Service的創建,RPC Service需要啓動RPC Server來實現相關的功能,創建並啓動相應RPC Server的代碼如下(nova/service.py:Service中的start方法):
在這裏插入圖片描述
在這裏插入圖片描述
這裏的messaging即爲oslo_messaging(import oslo_messaging as messaging),179行爲RPC Server創建,調用的是如下的代碼(nova/rpc.py):
在這裏插入圖片描述
要創建一個RPC Server需要transport,target,endpoints和executor參數,具體參數解析如下:

  1. transport(傳輸層):主要實現RPC底層的通信(比如socket)以及事件循環、多線程等其他功能,可以使用rabbitMQ作爲transport,也可以使用其他消息隊列來實現。一般通過URL來獲得指向不同Transport實現的句柄,URL格式爲:Transport://user:pass@host1:port[,hostN:portN]/virtual_host;
  2. Target:封裝指定某一個消息的最終目的地的所有信息,定義在第169行的messaging.Target(),Target所需要的主要參數包括exchange(爲none時默認使用配置文件中的control_exchange選項),topic(表示服務器暴露的一組接口(可被遠程調用的方法)。 允許多個服務器暴露同一組接口,消息會輪詢發給多個服務器中的某一個),server(客戶端可以指定 此參數要求消息的目的地是某個特定的服務器, 而不是 topic 一組服務器的一臺)。RPC server和RPC client的創建都需要Target參數,對於Server創建來說,需要topic和server參數,exchange參數可選;對於client創建來說,需要topic參數。
  3. endpoints:endpoints可以理解成一個方法的集合類,這些方法會用來被RPC Client遠程調用,圖中第171-174行即爲endpoints的定義,指被調用的方法主中的第一個self.manager,即nova每個組件代碼下的manager.py中的方法(詳情見上一篇教程https://blog.csdn.net/weixin_41977332/article/details/104569906)
  4. executor:指明消息的接收和處理的方式,主要有兩種處理方式:blocking(用戶調用start函數後,在start函數中開始循環處理請求:用戶線程阻塞,處理下一個請求。直到用戶調用stop函數之後處理循環纔會退出。 消息的分發和處理循環都是在start函數的線程中完成)和eventlet(會有協程GreenThread來處理消息的接收,然後會有不同的協程來處理不同消息的分發處理。調用start函數的用戶線程不會阻塞)

創建虛擬機build_instance過程中的RPC遠程調用

在上篇教程《openstack開發實踐(四):Nova源碼解析》中有介紹到nova-compute服務創建虛擬機,是從nova/conductor/manager.py:ComputeTaskManager的build_instance()方法在確定好按權重排序的主機列表之後,調用self.compute_rpcapi_build_and_run_instance()方法開始,在 nova project 中大多數的服務都提供了 API 或 RPC API 的實現文件,這些 API 是服務進程自身爲了能被其他的服務進程訪問所提供出來的一種接口,compute_rpcapi就是這樣一個RPC API,當別的服務進程希望影響另一個服務進程時,就可以通過 import 另一個服務進程的 rpcapi 就可以實現了。self.compute_rpcapi_build_and_run_instance()方法的具體代碼如下所示,其中第1072行的cctxt創建了一個RPC Client,cctxt.cast()則是一種異步調用的方式,在cctxt 發出request後,不會等待Server端的response,而是會去繼續執行其他操作:
在這裏插入圖片描述
self.router.by_host(ctxt, host).prepare(server=host,version=version)中的self.router對應的是nova/rpc.py中的ClientRouter類,該類中的by_host()方法調用了該類中的_client()方法,而_client()方法則具體定義了一個RPC Client,代碼如下圖所示,messaging.RPCClient則通過transport和target等參數返回一個RPCClient類。prepare()則是RPCClient中的一個方法,可以根據一些參數比如版本號重載client:在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

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