OpenStack之RPC调用(一)

众所周知,OpenStack的通信方式有两种,一种是基于HTTP协议的RESTFul API方式,另一种则是RPC调用。两种通信方式的应用场景有所不同,在OpenStack中,前者主要用于各组件之间的通信(如nova与glance的通信),而后者则用于同一组件中各个不同模块之间的通信(如nova组件中nova-compute与nova-scheduler的通信)。

关于OpenStack中基于RESTFul API的通信方式主要是应用了WSGI,我会在以后的博文中详细讨论。这里主要还是分享一下关于RPC调用的一些理解。

首先,什么是RPC呢?百度百科给出的解释是这样的:“RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议”。这个概念听起来还是比较抽象,下面我会结合OpenStack中RPC调用的具体应用来具体分析一下这个协议。

其次,为什么要采用RPC呢?单纯的依靠RESTFul API不可以吗?其实原因有下面这几个:

1. 由于RESTFul API是基于HTTP协议的,因此客户端与服务端之间所能传输的消息仅限于文本

2. RESTFul API客户端与服务端之间采用的是同步机制,当发送HTTP请求时,客户端需要等待服务端的响应。当然对于这一点是可以通过一些技术来实现异步的机制的

3. 采用RESTFul API,客户端与服务端之间虽然可以独立开发,但还是存在耦合。比如,客户端在发送请求的时,必须知道服务器的地址,且必须保证服务器正常工作

基于上面这几个原因,所以OpenStack才采用了另一种远程通信机制,这就是我们今天要讨论的鼎鼎大名的RPC。

要了解OpenStack中的RPC,有一个组件是必不可少的,那就是RabbitMQ(消息队列)。OpenStack中,RPC采用AMQP协议实现进程间通信,而RabbitMQ正是AMQP的实现方式,所以可以说OpenStack中的RPC调用都是基于RabbitMq完成的(注:有关AMQP、RabbitMQ的一些知识,可以参看我上篇分享的文章——《兔子与兔子洞》)。

在Nova中,定义了两种远程调用方式——rpc.call和rpc.cast。其中rpc.call方式是指request/response(请求/响应)模式,即客户端在发送请求后,继续等待服务器端的响应结果,待响应结果到达后,才结束整个过程。rpc.cast方式是指客户端发送RPC调用请求后,不等待服务器端的响应结果。不难看出,较rpc.cast模式,rpc.call更为复杂。为了处理rpc.call,Nova采用了Topic Exchange(主题交换器)和Direct Exchange(直接交换器)两种消息交换器。其中Topic Exchange用于客户端向服务器端rpc.call的发起,Direct Exchange用于服务器端向客户端返回rpc.call。对应于这两种交换机,Nova中定义了Topic/Direct消息消费者、Topic/Direct消息发布者、Topic/Direct交换器。各部件如下图所示:

OpenStack之RPC调用(一)_第1张图片

需要说明的是一个主题交换器可以关联多个队列,而一个直接交换器只能关联一个队列。

结合rpc.call代码实例,开始我们的分析吧,代码结构如下:

client.py——RPC调用请求客户端启动脚本

dispatcher.py——将客户端发布的消息分发给相应的方法处理

impl_kombu.py——核心代码,实现了消费者、生产者、建立连接等操作

manager.py——定义处理RPC请求的方法

rpc_amqp.py——发送RPC请求和发生RPC响应

rpc.py——为外部提供访问API

server.py——服务器端启动脚本

service.py——创建和管理RPC服务

接下来看一下rpc.call执行的流程:

1). RPC服务器端定义并启动RPC服务

2). RPC服务器端建立和RabbitMQ服务器的连接

3). RPC服务器端创建和激活主题消费者

4). RPC客户端向主题交换器发送RPC请求

5). RPC服务器端接收和处理RPC请求

6). RPC客户端创建和激活直接消费者

7). RPC服务器端向直接交换机发送RPC响应

1. 服务器端:定义和启动RPC服务

server.py

import service

srv = service.Service()  #创建RPC服务
srv.start()              #启动RPC服务

while True:
    srv.drain_events()   #监听RPC请求
以上代码的作用是创建Service对象,然后分别调用start和drain_events方法。Service类的定义在service.py中。

service.py

import rpc
import manager
import dispatcher
TOPIC = 'sendout_request'

class Service(object):
    def __init__(self):
        self.topic = TOPIC
        self.manager = manager.Manager()
    def start(self):
        self.conn = rpc.create_connection()
        rpc_dispatcher = dispatcher.RpcDispatcher(self.manager)
        self.conn.create_consumer(self.topic, rpc_dispatcher)
        self.conn.consume()
    def drain_events(self):
        self.conn.drain_events()

Service类中包含init、start、drain_events三个方法。

2. 服务器端:建立与RabbitMQ的连接

Service类调用rpc.crerate_connection方法来建立连接。该方法会返回一个Connection对象。Connection对象的定义在impl_kombu.py文件中。

impl_kombu.py

class Connection(object):
    def __init__(self):
        self.consumers = []
        self.connection = None
        self.reconnect()
    def reconnect(self):
        #初次重连的等待时间
        sleep_time = conf.get('interval_start', 1)
        #每次连接失败后增加的等待时间
        stepping = conf.get('interval_stepping', 2)
        #重连的最大等待时间
        interval_max = conf.get('interval_max', 30)
        sleep_time -= stepping

        while True:
            try:
                self._connect()
                return
            except Exception, e:
                if 'timeout' not in str(e):
                   raise
            
            sleep_time += stepping
            sleep_time = min(sleep_time, interval_max)
            print("AMQP Server is unreachable,"
                  "trying to connect %d seconds later\n" % sleep_time)
            time.sleep(sleep_time)
    def _connect(self):
        hostname = rabbit_params.get('hostname')
        port = rabbit_params.get('port')
        if self.connection:  #如果已有连接,释放原有连接
            print("Reconnecting to AMQP Server on "
                "%(hostname)s:%(port)d\n" % locals())
        self.connection.release()
        self.connection = None
        self.connection = kombu.connection.BrokerConnection(**rabbit_params)
        self.consumer_num = itertools.count(1)  #消费者迭代器,产生消费者唯一的标识
        self.connection.connect()  #建立与RabbitMQ的连接
        self.channel = self.connection.channel()  #获取连接的通道
        for consumer in self.consumers:
            consumer.reconnect(self.channel)
 
  

Connection类的初始化方法比较简单,主要是调用reconnect方法。reconnect方法会不断尝试与RabbitMQ建立连接。_connect方法实现真正的连接的建立。

3. 服务器端:创建和激活主题消费者

(1)Service类通过Connection类的create_consumer方法创建消费者

impl_kombu.py

class Connection(object):
    def create_consumer(self, topic, proxy):
        proxy_cb = rpc_amqp.ProxyCallback(proxy)
        self.declare_topic_consumer(topic, proxy_cb)

    def declare_topic_consumer(self, topic, callback):
        print('declaring topic consumer for topic %s...\n' % topic)
        self.declare_consumer(TopicConsumer, topic, callback)
    
    def declare_consumer(self, consumer_cls, topic, callback):
        def _declare_consumer():
            consumer = consumer_cls(self.channel,topic, callback,self.consumer_num.next())
            self.consumers.append(consumer)
            print('Succed declaring consumer for topic %s\n' % topic)
            return consumer
        return self.ensure(_declare_consumer, topic)
    
    def ensure(self, method, topic):
        while True:
            try:
                return method()
            except Exception, e:
                if 'timeout' not in str(e):
                    raise
                print('Failed to declare consumer for topic %s: '
                      '%s\n' % (topic, str(e)))
            self.reconnect()
ProxyCallback类是一个处理回调的代理类,后续会说明。主要来看declare_topic_consumer方法,这个方法创建了消费者。declare_topic_consumer方法调用declare_consumer,向declare_consumer方法中传入TopicConsumer类。declare_consumer方法对TopicConsumer类进行实例化,并通过ensure方法保证消费者创建成功。来看一下TopicConsumer这个类。

impl_kombu.py

class TopicConsumer(ConsumerBase):
    def __init__(self, channel, topic, callback, tag, **kwargs):
        self.topic = topic
        options = {'durable': False,
                   'auto_delete': False,
                   'exclusive': False}
        options.update(kwargs)
        exchange = kombu.entity.Exchange(name=topic,
                                         type='topic',
                                         durable=options['durable'],
                                         auto_delete=options['auto_delete'])
        super(TopicConsumer, self).__init__(channel,
                                            callback,
                                            tag,
                                            name=topic,
                                            exchange=exchange,
                                            routing_key=topic,
                                            **options)
options变量用来设置交换器的属性。这里交换器的名称为topic的值,tag为消费者的唯一标识。可以看到TopicConsumer类继承自ConsumerBase类,来看一下这个基类。

impl_kombu.py

class ConsumerBase(object):
    def __init__(self, channel, callback, tag, **kwargs):
        self.callback = callback
        self.tag = str(tag)
        self.kwargs = kwargs
        self.queue = None
        self.reconnect(channel)
    def reconnect(self, channel):
        self.channel = channel
        self.kwargs['channel'] = channel
        self.queue = kombu.entity.Queue(**self.kwargs)
        self.queue.declare()
ConsumerBase类的初始化方法调用了reconnect方法来创建队列。

(2)激活消费者

Service类通过Connection类的consume方法激活消费者。Connection类的consume方法最终会调用ConsumerBase类的consume方法。

impl_kombu.py

class ConsumerBase(object):
    def consume(self, *args, **kwargs):
        options = {'consumer_tag': self.tag}
        options['nowait'] = False
        def _callback(raw_message):
            message = self.channel.message_to_python(raw_message)
            try:
                msg = message.payload  #获取消息体
                self.callback(msg)  #处理消息
                message.ack()  #通知交换器消息处理完毕
            except Exception:
                print("Failed to process message... skipping it.\n")
        self.queue.consume(*args, callback=_callback, **options)  #激活消费者

主要定义了_callback方法,分发和处理RPC请求。

至此服务器端的工作暂时告一段落,下一篇博文来我们再来分析下客户端的工作和服务器端的后续接收和处理RPC请求的工作。

你可能感兴趣的:(openstack)