Openstack的消息队列机制及其部分代码解析(非oslo.message)

来源:互联网 发布:v70摩托罗拉淘宝 编辑:程序博客网 时间:2024/06/06 02:27

1 什么是AMPQ,什么事RabbitMQ,二者是什么关系?

         AMPQ是应用层协议的一个标准,它是为了面向消息中间件而设计的。它的主要作用是规定了异步消息传递所使用的协议标准。而AMPQ只是一个协议标准,需要有中间件来实现,实现的中间件有很多,例如,RabbitMQ、Qpid等,openstack官方推荐使用RabbitMQ支持的RPC方式来实现异步消息通信,模式采用典型的发布(Publish)/订阅(subcribe)模式。

 2        openstack为什么要用AMPQ,rabbitmq和AMPQ又是什么关系?

Openstack的架构决定了需要使用消息队列机制来实现不同模块间的通信,通过消息验证、消息转换、消息路由架构模式,带来的好处就是可以是模块之间最大程度解耦,客户端不需要关注服务端的位置和是否存在,只需通过消息队列进行信息的发送。RabbitMQ适合部署在一个拓扑灵活易扩展的规模化系统环境中,有效保证不同模块、不同节点、不同进程之间消息通信的时效性,可有效支持OpenStack云平台系统的规模化部署、弹性扩展、灵活架构以及信息安全的需求。

         注:RPC(remoteprocedure Call Protocol)远程调用协议。它是一种通过网络从远程计算机程序上请求服务协议。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。

 3 下面我们根据conductor 发送RPC信息到Schedule流程理解实现机制。我们都知道,任何一个RPC调用,都有Client/Server两部分,那么在openstack中分别在rpcapi.py和manager.py中实现了client和server。

其实发送消息的机制简单来讲就是下面的流程:

    (1)创建连接;

    (2)获得channel;

    (3)创建exchange;

    (4)创建Producer,指定exchange与routing_key;

    (5)发送消息;

当然openstack中实现不会这么简单,而是经过了很多层封装,不会整个流程就是上面5部分。既然是根据代码分析,那么多说无益,先上rpcapi.py的代码。

 Nova.conduct.rpcapi.py

def ComputeTaskAPI(self, context, instances, image, filter_properties,    admin_password, injected_files, requested_networks,    security_groups, block_device_mapping, legacy_bdm=True):    image_p = jsonutils.to_primitive(image)    cctxt = self.client.prepare(version='1.5')    cctxt.cast(context, 'build_instances',               instances=instances, image=image_p,               filter_properties=filter_properties,

         其中,我们可以看出首先创建一个叫cctxt的实例,这个cctxt是rabbitmq提供的client对象,可以通过cctxt.cast()来进行消息的发送。那么问题来了,这个cctxt是哪里来的?

class ComputeTaskAPI(rpcclient.RpcProxy)
     def __init__(self):
           self.client = self.get_client(namespace=self.RPC_API_NAMESPACE)
                ... ... 

父类:nova.rpcclient.py

class RpcProxy(proxy.RpcProxy):    def get_client(self, namespace=None, server_params=None):        return RPCClient(self,                        ... ... 

返回RPCClient,再看RPCClient类:

 

class RPCClient(object):
        def cast(self, ctxt, method, **kwargs):         ... ...                return self.proxy.fanout_cast_to_server(                    ctxt, self.server_params, msg, **kwargs)                    caster = cast_to_server         ... ...
 

可以看出这个cctxt是RPClient实例,这个RPClient又是怎么实现发送消息呢?

from nova.openstack.common.rpc import proxy

在RPCClient中的初始化构造函数中,我们可以看到:

self.proxy = proxy

可以看出最终是调用nova/openstack/common/rpc/proxy.py中的PRCProxy类(RPCClinet辅助类)来实现发送任务,那么现在看看RpcProxy类,由于发送时通过.cast()方法发送消息的。下面只列出我们用到的cast()方法。

class RpcProxy(object):    """A helper class for rpc clients.
... ...
def cast(self, context, msg, topic=None, version=None):    self._set_version(msg, version)    msg['args'] = self._serialize_msg_args(context, msg['args'])    rpc.cast(context, self._get_topic(topic), msg)
... ... 

从这里

from nova.openstack.common import rpc

nova.openstack.common:__init__.py中的cast()方法

def cast(context, topic, msg):    return _get_impl().cast(CONF, context, topic, msg)

我们可以看出,最终是通过调用rpc的__init__.py中cast()方法来实现,这个方法返回_get_impl().

_get_impl()

def _get_impl():
... ... impl = CONF.rpc_backend.replace('nova.rpc',                           'nova.openstack.common.rpc') _RPCIMPL = importutils.import_module(impl)    return _RPCIMPL

这里可以看出,这里load的是CONF.rpc_backend,也就是系统配置的RPC消息的后端。因为我们是通过rabbitmq来实现RPC消息通信,所以系统配置nova.conf文件可以找到rpc_backend = impl_kombu,那么我们就可以找到的Impl_kombu.py的cast()方法。

Nova.openstack.common.rpc. impl_kombu.py

1

def cast(conf, context, topic, msg):    """Sends a message on a topic without waiting for a response."""    return rpc_amqp.cast(        conf, context, topic, msg,        rpc_amqp.get_connection_pool(conf, Connection))

然后通过下面,可以知道又引用了nova/openstack/common/rpc/ampq.py的get_connection_pool():

from nova.openstack.common.rpc import amqp as rpc_amqp

ampq.py中的get_connection_pool()

get_connection_pool(conf, connection_cls):    with _pool_create_sem:        # Make sure only one thread tries to create the connection pool.        if not connection_cls.pool:            connection_cls.pool = Pool(conf, connection_cls)    return connection_cls.pool

get_connection_pool()就是实现了到RabbitMQ的连接池中获取一个连接。而其中connection_cls传进来的就是类Connection的实例化对象,Connection类就是实现了到RabbitMQ的连接。这里就完成了获取connection链接工作。来看connection:

class Connection(object):
。。。。。。
def cast(conf, context, topic, msg):    """Sends a message on a topic without waiting for a response."""        return rpc_amqp.cast(                conf, context, topic, msg,                rpc_amqp.get_connection_pool(conf, Connection))

这里我们回到1步的.cast(),也就是connectioncast()方法。

nova.openstack.common.rpc.ampq.py

def cast(conf, context, topic, msg, connection_pool):    """Sends a message on a topic without waiting for a response."""    LOG.debug(_('Making asynchronous cast on %s...'), topic)    _add_unique_id(msg)    pack_context(msg, context)    with ConnectionContext(conf, connection_pool) as conn:        conn.topic_send(topic, rpc_common.serialize_msg(msg))

这里我们会发现最红是调用connection的topic_send()来进行主题模式的发送:

def topic_send(self, topic, msg, timeout=None):    """Send a 'topic' message."""    self.publisher_send(TopicPublisher, topic, msg, timeout)

topic_send 中通过.publisher_send()中通过实现发布者类Publish来对消息进行发送。

def publisher_send(self, cls, topic, msg, timeout=None, **kwargs):    """Send to a publisher based on the publisher class."""    def _error_callback(exc):        log_info = {'topic': topic, 'err_str': str(exc)}        LOG.exception(_("Failed to publish message to topic "                      "'%(topic)s': %(err_str)s") % log_info)    def _publish():        publisher = cls(self.conf, self.channel, topic, **kwargs)        publisher.send(msg, timeout)    self.ensure(_error_callback, _publish)

 

这个publisher发布者类,是通过传进来的TopicPublisher主题式发布者类进行修饰的,在这个主题式发布者类TopicPublisher实例化时,完成了很多功能:channel的获取,交换器exchange的声明与routing_key的确定、并创建相应的producer等。

         接下来通过publisher.send(msg,timeout)将消息进行一定的消息结构封装,并通过routing_key与将消息发送到相应的队列中去。至此我们已经实现了消息的发送操作。流程如下(本图摘自网络):

 

 

接下来看下接收端也就是server端是如何从特定的消息队列发送给自己的消息,并进行具体操作:

我们知道,nova中的各个服务在启动时机会初始化所有自己会用到的队列,并会启动一个绿色线程,不断的检测新的消息的到来,一旦有新的消息,将会由合适的consumer进行读取,并进一步进行消息的解析和执行操作。

下面是usr/bin/nova-scheduler中的启动操作:

import sys from nova.cmd.scheduler importmain 

if __name__ == "__main__":

   sys.exit(main())

我们可以看出,首先会找到nova/cmd/scheduler.py中的main()函数:

def main():    config.parse_args(sys.argv)    logging.setup("nova")    utils.monkey_patch()    server = service.Service.create(binary='nova-scheduler',                                    topic=CONF.scheduler_topic)    service.serve(server)    service.wait()

可以看出,这里主要的任务就是创建和启动服务。先看看Service类的静态方法create:

def create(cls, host=None, binary=None, topic=None, manager=None,           report_interval=None, periodic_enable=None,           periodic_fuzzy_delay=None, periodic_interval_max=None,           db_allowed=True):  ... ...     service_obj = cls(host, binary, topic, manager,                      report_interval=report_interval,                      periodic_enable=periodic_enable,                      periodic_fuzzy_delay=periodic_fuzzy_delay,                      periodic_interval_max=periodic_interval_max,                      db_allowed=db_allowed)    return service_obj

主要是返回各Service的对象。随后会调用这个service的start()方法。 这个方法主要实现了获取所有服务、创建到RPC的连接,创建不同类型的消息消费者,启动消费者线程用来执行获取的消息,并在启动服务后添加服务到服务成员组等等操作。它其中共创建了两个不同范围的topic消费者及一个广播消费者。然后每个消费都都放置在自己的协程中进行处理。

具体见start()代码:

def start(self):    verstr = version.version_string_with_package()    LOG.audit(_('Starting %(topic)s node (version %(version)s)'),              {'topic': self.topic, 'version': verstr})     # 在进程开始之前执行基本配置的检测;    self.basic_config_check()    self.manager.init_host()    self.model_disconnected = False    # 获取需要的上下文信息;    ctxt = context.get_admin_context()    try:        # 查询数据库获取topic、host、binary类型指定的所有的服务;        self.service_ref = self.conductor_api.service_get_by_args(ctxt,                self.host, self.binary)         # 获取这些服务的ID值;        self.service_id = self.service_ref['id']    except exception.NotFound:        self.service_ref = self._create_service_ref(ctxt)    self.manager.pre_start_hook()    if self.backdoor_port is not None:        self.manager.backdoor_port = self.backdoor_port     # 建立一个到用于RPC的消息总线的连接;    # 建立获取到RabbitMQ的连接;    # 创建连接,默认是kombu实现;    self.conn = rpc.create_connection(new=True)    LOG.debug(_("Creating Consumer connection for Service %s") %              self.topic)     # 初始化RPC调度器;    rpc_dispatcher = self.manager.create_rpc_dispatcher(self.backdoor_port)    # Share this same connection for these Consumers    # 建立不同的消息消费者;    # 创建以服务的topic为路由键的消费者;    self.conn.create_consumer(self.topic, rpc_dispatcher, fanout=False)    # 创建以服务的topic和本机名为路由键的消费者    node_topic = '%s.%s' % (self.topic, self.host)    # 创建以服务的topic和本机名为路由键的消费者(基于topic&host,可用来接收定向消息);    self.conn.create_consumer(node_topic, rpc_dispatcher, fanout=False)    # fanout直接投递消息,不进行匹配,速度最快(fanout类型,可用于接收广播消息);    self.conn.create_consumer(self.topic, rpc_dispatcher, fanout=True)    # 创建协程并启动,等待消息    self.conn.consume_in_thread()     self.manager.post_start_hook()    LOG.debug(_("Join ServiceGroup membership for this service %s")              % self.topic)    # Add service to the ServiceGroup membership group.    self.servicegroup_api.join(self.host, self.topic, self)    if self.periodic_enable:        if self.periodic_fuzzy_delay:            initial_delay = random.randint(0, self.periodic_fuzzy_delay)        else:            initial_delay = None        self.tg.add_dynamic_timer(self.periodic_tasks,                                 initial_delay=initial_delay,                                 periodic_interval_max=self.periodic_interval_max)

协程这里不花太多的时间来研究。这里我们分析下:self.conn = rpc.create_connection()

,这个的作用就是建立一个connection的链接,完成接收的第一步。

接下来会创建消费者cusumer,也是在connection类中的create_consumer()方法中完成的。

def create_consumer(self, topic, proxy, fanout=False):    """Create a consumer that calls a method in a proxy object."""    proxy_cb = rpc_amqp.ProxyCallback(        self.conf, proxy,        rpc_amqp.get_connection_pool(self.conf, Connection))    self.proxy_callbacks.append(proxy_cb)    if fanout:        self.declare_fanout_consumer(topic, proxy_cb)    else:        self.declare_topic_consumer(topic, proxy_cb)

完成了建立一个主题式的消息消费者的工作。

def declare_topic_consumer(self, topic, callback=None, queue_name=None,                           exchange_name=None, ack_on_error=True):    """Create a 'topic' consumer."""    self.declare_consumer(functools.partial(TopicConsumer,                                            name=queue_name,                                            exchange_name=exchange_name,                                            ack_on_error=ack_on_error,                                            ),                          topic, callback)

看下declare_topic_consumer():

def declare_consumer(self, consumer_cls, topic, callback):    """Create a Consumer using the class that was passed in and    add it to our list of consumers    """    def _connect_error(exc):        log_info = {'topic': topic, 'err_str': str(exc)}        LOG.error(_("Failed to declare consumer for topic '%(topic)s': "                  "%(err_str)s") % log_info)    def _declare_consumer():        consumer = consumer_cls(self.conf, self.channel, topic, callback,                                self.consumer_num.next())        self.consumers.append(consumer)        return consumer

这里其实是实例化了一个TopicConsumer进行初始化,并获取其实例化对象consumer,再把建立好的消费者对象加入到consumers列表中然后返回,等待dispatch接收到后来调用。

这个消费者Cusumer在建立过程中,经过TopicConsumer的实例以及其父类ConsumerBase一系列的操作实现exchange以及queue的声明、创建与绑定,最终完成得到这个主题式消费者。

下面看下协程的处理:

self.conn.consume_in_thread()

def consume_in_thread(self):    """Consumer from all queues/consumers in a greenthread."""    @excutils.forever_retry_uncaught_exceptions    def _consumer_thread():        try:            self.consume()        except greenlet.GreenletExit:            return    if self.consumer_thread is None:        self.consumer_thread = eventlet.spawn(_consumer_thread)    return self.consumer_thread

协程相关的代码与我们理解rabbitmq关系不大,就先不去深追,感兴趣的话,推荐博客:

http://blog.csdn.net/gaoxingnengjisuan/article/details/12234079

我们只需知道,最终会通过ConsumerBase的message = self.receiver.fetch()接受消息,并调用start()方法中的rpc_dispatcher实例,来对消息进行分发,调用相关的方法去解决。

参考文档:

http://www.cnblogs.com/popsuper1982/p/3800396.html

http://blog.csdn.net/tantexian/article/details/44804329http://blog.csdn.net/gaoxingnengjisuan/article/details/9623529

http://www.aboutyun.com/thread-11230-1-1.html

http://lynnkong.iteye.com/blog/1699299

http://blog.sina.com.cn/s/blog_66ca40550101l3vm.html

http://m.blog.csdn.net/blog/z_lstone/14165777

http://bingotree.cn/?p=207

0 0
原创粉丝点击