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

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

猜你喜欢

转载自blog.csdn.net/u014007037/article/details/50453928