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:在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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