OpenStack RPC框架解析

1  消息队列Rabbitmq介绍

Rabbitmq的整体架构图

1)Rabbitmq Server:中间那部分就是Rabbitmq Server,也叫broken server,主要是负责消息的传递,保证client A、B发送的消息Cleint 1、2、3能够正确的接收到。

2)Client A、B:在消息队列里我们称之为生产者-Producer,发送消息的客户端。

3)Client 1、2、3:在消息队列里我们称之为消费者-Consume,接收消息的客户端。

4)Exchange:我们可以称之为消息队列的路由,根据发送的消息的routing key来转发到对应的队列上。有四种类型的Exchange,对应四种不同的转发策略:

direct Exchange:完全匹配,比如routing key是abc,就对应binding key为abc对应的queue。

topic Exchange:正则匹配,比如routing key是ab*,可以用来匹配binding key为abc或abd等的queue。

fanout Exchange:广播策略,忽略掉routing key,转发给所有绑定在这个Exchange的queue。

headers Exchange:不依赖于routing key,会根据发送的消息的内容的headers属性来进行匹配。

5)Queue:队列,消息存放的地方。

6)Connection (连接)和 Channel (通道):生产者和消费者需要和 RabbitMQ 建立 TCP 连接。一些应用需要多个connection,为了节省TCP 连接,可以使用 Channel,它可以被认为是一种轻型的共享 TCP 连接的连接。连接需要用户认证,并且支持 TLS (SSL)。连接需要显式关闭。

7)vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离

8)Message(消息):在通道上传输的二进制对象,结构为Headers(头)、Properties (属性)和 Data (数据)。

以下是消息的几个重要属性:

routing key:Exchange根据该key来转发消息到对应的队列中

delivery_mode:消息模式,有持久模式和非持久模式,持久模式则是将消息保存到磁盘中,非持久模式则是消息保存在内存中

reply_to:RPC调用时处理结果返回需传送到的队列名,称为回调队列

correlation_id:RPC调用返回时需要用到的参数,一个请求id

content_type这个编码类型是给生产者和消费者使用的,rabbitmq只是按原样传输的

 

对应到OpenStack的平台则是:

Client端的生产者可以是nova-api,nova-conductor等,以虚拟机开机为例,则nova-api是生产者,nova-api收到一个http请求,产生一个开机消息,exchange是’nova’,发送的队列名compute.hostname,routing key为队列名,然后发送到Rabbitmq Server上去,消息队列服务保存到对应的队列上,然后将消息派发给消费者。因为消费者跟rabbitmq服务是建立了一条channel连接的,所以派发消息就相当于是通过这条channel传送数据。

消费者则对应是nova-compute,nova-compute接收到消息后进行解析,然后调用对应的函数进行处理,然后将处理结果返回。

 

2  Rabbitmq集群模式

Rabbitmq集群工作原理图:

Rabbitmq是用Erlang语言写的,该语言天生有分布式特性,本身支持原生的HA模式。

普通的消息队列集群会始终同步四种类型的内部元数据:

(1)队列元数据:队列名称和它的属性

(2)交换器元数据:交换器名称、类型和属性

(3)绑定(binding)元数据:一张简单的表格展示如何将消息路由到对应的队列

(4)Vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性

 

但普通模式并没有对消息队列的消息进行同步,需要设置成镜像模式,才会对消息进行同步。

 

2.1  同步原理

通过镜像模式,Rabbitmq会将镜像队列放置于多个消息队列服务节点上,消息的生产和消费都会在节点间进行同步,镜像队列包含一个master和多个slave,当master退出时,时间最长的则升为master。

 

每个消息队列进程会创建一个gm(guaranteed multicast)进程,镜像队列中的所有gm进程会组成一个gm进程组用于广播和接收消息。Gm组将集群中的节点组成一个环,主节点收到或处理完一个消息都会发起消息同步,消息沿着环形链走,当主节点接收到自己发的消息后则表示消息已经同步到所有的节点。

 

消息的发布和消费都是通过 master 队列完成,master 队列对消息进行处理同时将消息的处理动作通过 gm 广播给所有 slave 队列。

 

2.2  消息走向路径

以开机一个虚拟机为例,环境状况:244的nova-api245上的虚拟机进行关机操作,245compute.hostname的主队列在242

nova-api会将消息发到给主队列242上的消息队列服务器保存,242进行gm广播,242通过channel将消息传送到245上,245接收到消息进行处理

 

2.3  消息确认机制

程序中是在拿到消息后和开始处理前期间进行了message.acknowledge()的调用,调用后即是告诉消息队列服务,该消息已经被处理完了,可以进行删除了。

从实践来看确实是acknowledge调用了后才删除的,但程序是实际调用后才开始执行消息处理函数,期间如果有异常报错没有处理成功则也不会重新处理了。

Openstack平台没有对no_ack进行设置,查看kombu的代码默认no_ack是false的,也就是需要进行确认才会删除消息。

也可以发送nack的方式表示消息处理有问题,这时如果队列的requeue设置为true,则会重新进入队列交由其它消费者进行处理,默认是为false。

 

dead lettering机制:当调用了reject或nack且requeue是false时或者消息过期时,该机制会将失败的消息放入到dead-lettered队列中。

 

3  OpenStack RPC框架

3.1  接收消息(以nova-compute服务为例)

云平台消息队列RPC处理框架图:

1.这里以nova-compute服务启动为例进行讲解,nova-compute服务启动,会通过配置文件解析获取一个Transport类对象,Transport对象里引用了RabbitDriver类对象

(1)Transport类作用:通过配置文件获取对应的_driver,使用_driver来发送消息

(2)RabbitDriver类作用:用于发送消息和创建监听类

 

2.nova-compute通过调用get_server函数获取RPCServer类对象,类对象聚合了Transport类对象和RPCDispatcher类对象

(1)RPCServer类作用:初始化rpc监听服务,创建队列

(2)RPCDispatcher类作用:收到消息后进行解析找到相对应的函数进行调用

 

3.调用RPCServer类对象的start方法,里面调用 _create_listener方法创建监听者AMQPListener类对象,用于作为绑定为消费者的回调对象,该类对象引用了一个Connection类对象conn。使用conn定义队列,最后返回一个PollStyleListenerAdapter类对象

(1)AMQPListener类作用:作为消费者绑定的回调对象,同时poll方法用于获取消息

(2)PollStyleListenerAdapter类作用:创建线程不断获取消息

(3)Connection类作用:获取了kombuconnection对象,用于进行消费者、队列定义和重连接等逻辑相关操作,使用Consumer类来管理消费者

(4)Consumer类作用:一个Consumer类对象代表一个消费者,里面保存了消费者信息和定义消费者的方法

(5)AMQPIncomingMessage类作用:消息进行解析后初始化为该类对象,代表一个消息的结构,里面有reply方法用于返回消息处理结果给发布消息者

 

4.PollStyleListenerAdapter类调用start方法开启一个线程while循环专门调用AMQPListener类对象的poll方法进行消息获取

 

5.poll函数会读取incomings队列里有没有消息,如果有则表示拿到一个未处理消息发给Dispatcher类去处理这个消息,如果该队列空了,则调用drain_events方法去获取各channel上传过来的消息并将它们存到inconings队列中。

 

6.drain_events方法机理:从strace工具看到该nova-compute服务有大量的epoll_wait方法调用,可知采用了事件触发的方式。

 

3.2  发送消息

由于是发送消息,所以只要看右边的RPCClient端那部分就可以了:

1.跟接收消息一样,会根据target生成一个Transport类对象,该对象根据配置文件会获取一个driver对象,我们的是RabbitDriver对象,继承于AMQPDriverBase类;

2.获取一个_CallContext类对象,引用了Transport类对象

(1)_CallContext类作用:用来发送消息,对消息进行序列化并调用Transport类对象的driver来进行消息发送

3.获取Connection类对象进行消息发送

4.Connection类中通过kombu中的Producer类的发送方法进行消息发出

 

3.3  重连机制

每个消费者都是建立在一个channel上的,channel是建立在Tcp连接上的,如果连接的rabbitmq服务节点关闭了,则连接会断开,因此需要重新在其它未关闭节点上建立连接,重新建立channel和消费者。

重连机制的代码存在于impl_driver.py中的Connection类的consume函数中

这里有两个地方是可以检测到连接断开了,需要重连的,一个是在读取socket时发现,一个是在心跳检测机制里发现。

 

读socket时抛异常触发的重连:

1. 由上面分析我们知道程序会不断调用Connection的consume函数进行获取消息,该函数会调用到kombuConnection类的autoretry函数,同时传入了_consume函数作为参数

2. autoretry又调用到ensure函数,该函数主要作用是调用传进来的_consume函数,如果有异常抛出,则进入异常重连处理,调用on_error函数,再调用ensure_connection函数确保重新建立好一条新的连接,然后在连接上建立新的channel,最后将channel进行更新。

 

心跳检测机制触发的重连:

1. 在服务启动后就有一个专门的线程定时发包检测连接是否正常,超时60秒则触发异常

2. 触发异常后调用ensure_connection函数将当前channel置为None,从而触发重建channel

 

消费者的重新建立:

1.在_consume函数中每次都会去判断self._new_tags集合是不是不为空,如果不为空则会重新建立这些tag的消费者,执行建立函数后就会把它remove掉,关键代码逻辑:

2.而_new_tags的获取则是根据异常抛出,检测异常类型来重新赋予之前消费者的tags,以此重新建立消费者,关键代码逻辑:

 

4  代码流程解析

4.1  nova-compute启动流程

Openstack的服务启动都是先从cmd目录下的main函数开始执行的,比如nova-compute服务的启动则是nova/cmd/compute.py文件中的main函数开始执行:

File:nova/cmd/compute.py

def main():
    # 调用nova/service.py文件的Service类的create类方法实例化一个service类
    server = service.Service.create(binary='nova-compute',
                                    topic=CONF.compute_topic)
    service.serve(server)
service.wait()

 

得到server后调用server函数进行服务运行:

File:nova/service.py

def serve(server, workers=None):
    global _launcher
    if _launcher:
        raise RuntimeError(_('serve() can only be called once'))

    # 这里的service是指oslo_service包导入的service了
    # 调用到oslo_service/service.py的launch方法
    _launcher = service.launch(CONF, server, workers=workers)

 

launch函数调用oslo_service包的service.py的launch方法初始化一个ServiceLauncher实例,并调用launch_service函数:

File:oslo_service/service.py

def launch(conf, service, workers=1, restart_method='reload'):

    if workers is not None and workers <= 0:
        raise ValueError(_("Number of workers should be positive!"))

    # 默认传入的是None
    if workers is None or workers == 1:
        # 这里是初始化一个继承了Launcher类的ServiceLauncher类实例
        launcher = ServiceLauncher(conf, restart_method=restart_method)
    else:
        launcher = ProcessLauncher(conf, restart_method=restart_method)
    # 调用Launcher类里的launch_service方法,launch_service方法运行给定的service
    launcher.launch_service(service, workers=workers)

    return launcher

 

Launch_service函数调用了父类的实现:

File:oslo_service/service.py  Launch:launch_service

def launch_service(self, service, workers=1):
    if workers is not None and workers != 1:
        raise ValueError(_("Launcher asked to start multiple workers"))
    _check_service_base(service)
    service.backdoor_port = self.backdoor_port
    # 调用Services类的add方法来运行给定service
    # 其实最后也就是开辟了个绿色线程池和获取一个绿色线程运行service的start方法
    self.services.add(service)

 

我们可以直接看service的start方法:

File:nova/service.py  Service:start

def start(self):
    ......

    # 初始化oslo_messaging/target.py的Target类
    target = messaging.Target(topic=self.topic, server=self.host)

    endpoints = [
        self.manager,
        baserpc.BaseRPCAPI(self.manager.service_name,         
                self.backdoor_port)
    ]
    endpoints.extend(self.manager.additional_endpoints)

    # 获取nova/objects/base.py中的NovaObjectSerializer类实例
    # 用来序列化nova服务中的对象
    serializer = objects_base.NovaObjectSerializer()

    # 获取一个oslo_messaging/rpc/RPCServer类实例
    self.rpcserver = rpc.get_server(target, endpoints, serializer)
    # 调用到oslo_messaging/server.py的MessageHandlingServer类的start方法
    self.rpcserver.start()

    ......

 

看下get_server实现:

File:nova/rpc.py

def get_server(target, endpoints, serializer=None):

    # TRANSPORT一个transport类对象,里面包含发消息的driver实现对象,如果是rabbit则对应到实现rabbit的driver类
    # 获取TRANSPORT对象:<class 'oslo_messaging.transport.Transport'>
    # 更重要的是transport对象里的driver对象:   oslo_messaging._drivers.impl_rabbit.RabbitDriver
    assert TRANSPORT is not None

    if profiler:
        serializer = ProfilerRequestContextSerializer(serializer)
    else:
        serializer = RequestContextSerializer(serializer)

    # get_rpc_server在oslo_messaging/rpc/server.py文件中
    # 获取一个RPCServer实例
    return messaging.get_rpc_server(TRANSPORT,
                                    target,
                                    endpoints,
                                    executor='eventlet',
                                    serializer=serializer)

 

查看get_rpc_server实现:

File:oslo_messaging/rpc/server.py

def get_rpc_server(transport, target, endpoints,
                   executor='blocking', serializer=None, access_policy=None):
    # 获取一个消息调度员,它能识别收到的消息的结构
    # 用于接收到消息后进行消息分发处理 A message dispatcher which understands RPC messages
    # oslo_messaging/rpc/dispatcher.py类的RPCDispatcher类
    # 解析消息然后调用相对应的方法进行处理
    dispatcher = rpc_dispatcher.RPCDispatcher(endpoints, serializer,
                                              access_policy)
    # 该类有个关键函数_process_incoming是在接收到消息时进行回调的
    return RPCServer(transport, target, dispatcher, executor)

 

获取到RPCServer实例后,调用start方法,因为RPCServer继承于MessageHandlingServer但没有实现,所以是调用父类的start方法:

File:oslo_messaging/server.py  MessageHandlingServer:start

def start(self, override_pool_size=None):
    ......

    try:
        # 这里程序调用到的是oslo_messaging/rpc/server.py的RPCServer类的_create_listener方法
        self.listener = self._create_listener()
    except driver_base.TransportDriverError as ex:
        raise ServerListenError(self.target, ex)
    ......

    self.listener.start(self._on_incoming)

 

File:oslo_messaging/rpc/server.py  RPCServer:_create_listener

def _create_listener(self):
    # oslo_messaging/transport/Transport类_listen方法
    return self.transport._listen(self._target, 1, None)

 

File:oslo_messaging/transport.py  Transport:_listen

def _listen(self, target, batch_size, batch_timeout):
    if not (target.topic and target.server):
        raise exceptions.InvalidTarget('A server\'s target must have '
                                       'topic and server names specified',
                                       target)
    # 这个_driver对应的是oslo_messaging._drivers.impl_rabbit.RabbitDriver
    return self._driver.listen(target, batch_size,
                               batch_timeout)

 

File:oslo_messaging/_drivers/amqpdriver.py  AMQPDriverBase:listen

def listen(self, target, batch_size, batch_timeout):
    # 这里是从连接池里获取一个连接对象
    # conn是oslo_messaging/_drivers/impl_rabbit.py的类Connection实例
    conn = self._get_connection(rpc_common.PURPOSE_LISTEN)

    # 这个listen很关键,它被绑定为消费者的回调对象,也就是收到消息时是调用该对象,该对象实现了__call__方法
    # 所以可直接调用
    listener = AMQPListener(self, conn)

    conn.declare_topic_consumer(exchange_name=self._get_exchange(target),
                                topic=target.topic,
                                callback=listener)
    conn.declare_topic_consumer(exchange_name=self._get_exchange(target),
                                topic='%s.%s' % (target.topic,
                                                 target.server),
                                callback=listener)
    conn.declare_fanout_consumer(target.topic, listener)

    # 返回一个实现poll模式监听消息到来的类
    return base.PollStyleListenerAdapter(listener, batch_size,
                                         batch_timeout)

 

创建好监听类后,调用start方法:

File:oslo_messaging/_drivers/base.py  PollStyleListenerAdapter:start

def start(self, on_incoming_callback):
    super(PollStyleListenerAdapter, self).start(on_incoming_callback)
    self._started = True
    # _listen_thread在__init__方法中定义了,如下行所示
    # self._listen_thread = threading.Thread(target=self._runner)
    # 所以是开启一个线程运行_runner函数
    self._listen_thread.start()

 

再来看下_runner函数:

File:oslo_messaging/_drivers/base.py  PollStyleListenerAdapter:_runner

def _runner(self):
    while self._started:
        # 这里poll调用oslo_messaging/_drivers/amqpdriver.py的AMQPListener类poll函数
        incoming = self._poll_style_listener.poll(
            batch_size=self.batch_size, batch_timeout=self.batch_timeout)

        # 读到有消息,调用回调函数进行处理
        if incoming:
            # 该on_incoming_callback是oslo_messaging/server.py中self.listener.start(self._on_incoming)中传进来的
            # 是该文件中MessageHandlingServer类的_on_incoming函数
            # _on_incoming函数又调用到了RPCServer中的_process_incoming函数
            self.on_incoming_callback(incoming)

 

这里先来看下poll函数实现:

File:oslo_messaging/_drivers/amqpdriver.py  AMQPListener:poll

def poll(self, timeout=None):
    stopwatch = timeutils.StopWatch(duration=timeout).start()

    while not self._shutdown.is_set():
        self._message_operations_handler.process()

        if self.incoming:
            # 从incoming列表中获取第一个消息返回
            return self.incoming.pop(0)

        left = stopwatch.leftover(return_none=True)
        if left is None:
            left = self._current_timeout
        if left <= 0:
            return None

        try:
            # 获取所有队列的消息
            # oslo_messaging/dr_drivers/impl_rabbit.py的Connection类的consume函数
            # 将获取到的消息经过解析存到incoming列表中
            self.conn.consume(timeout=min(self._current_timeout, left))
        except rpc_common.Timeout:
            self._current_timeout = max(self._current_timeout * 2,
                                        ACK_REQUEUE_EVERY_SECONDS_MAX)
        else:
            self._current_timeout = ACK_REQUEUE_EVERY_SECONDS_MIN

    # NOTE(sileht): listener is stopped, just processes remaining messages
    # and operations
    self._message_operations_handler.process()
    if self.incoming:
        return self.incoming.pop(0)

    self._shutoff.set()

 

这里关键函数是调用consume进行消息获取,调用到了Connection类的consume函数,以下是该函数的关键语句:

# 调用kombu/connection.pyConnection类的drain_events方法,等待来自服务器的单个事件,所以这是事件触发型的

# 其中里面的supports_librabbitmq()=False(因为环境支持’eventlet’,所以未采用’default’,所以返回False

# 最后是调用到kombu/transport/pyamqp.pydrain_events方法

# 再调用到amqp包的Connection类的drain_events(amqp/connection.py)

File:oslo_messaging/_drivers/impl_rabbit.py  Connection:consume

self.connection.drain_events(timeout=poll_timeout)

 

amqp包的drain_events实现

File:amqp/connection.py  Connection: drain_events

def drain_events(self, timeout=None):
    """Wait for an event on a channel."""
    # 等待事件通知
    chanmap = self.channels
    chanid, method_sig, args, content = self._wait_multiple(
        chanmap, None, timeout=timeout,
    )

    channel = chanmap[chanid]

    if (content and
            channel.auto_decode and
            hasattr(content, 'content_encoding')):
        try:
            content.body = content.body.decode(content.content_encoding)
        except Exception:
            pass

    amqp_method = (self._method_override.get(method_sig) or
                   channel._METHOD_MAP.get(method_sig, None))

    if amqp_method is None:
        raise AMQPNotImplementedError(
            'Unknown AMQP method {0!r}'.format(method_sig))

    if content is None:
        return amqp_method(channel, args)
    else:
        return amqp_method(channel, args, content)

到amqp包我们就不深究下去了

 

4.1  收到消息时行为

接着看下有消息到来时执行的回调函数,从前面我们可知在创建消费者时我们绑定了个listen对象作为callback,如下:

File:oslo_messaging/_drivers/amqpdriver.py  AMQPDriverBase:listen

listener = AMQPListener(self, conn)
conn.declare_topic_consumer(exchange_name=self._get_exchange(target),
                            topic=target.topic,
                            callback=listener)

 

所以当收到消息时会调用AMQPListener类的__call__函数:

def __call__(self, message):
    # 收到消息后解析消息结构体并构建成AMQPIncomingMessage结构
    # type(message)对应类:<class 'oslo_messaging._drivers.impl_rabbit.RabbitMessage'>
    ctxt = rpc_amqp.unpack_context(message)
    unique_id = self.msg_id_cache.check_duplicate_message(message)
    if ctxt.msg_id:
        LOG.debug("received message msg_id: %(msg_id)s reply to "
                  "%(queue)s", {'queue': ctxt.reply_q,
                                'msg_id': ctxt.msg_id})
    else:
        LOG.debug("received message with unique_id: %s", unique_id)

    self.incoming.append(AMQPIncomingMessage(
        self,
        ctxt.to_dict(),
        message,
        unique_id,
        ctxt.msg_id,
        ctxt.reply_q,
        self._obsolete_reply_queues,
        self._message_operations_handler))

 

可以看到收到message后,解析message并构建为AMQPIncomingMessage实例appendincoming队列中。

消息到队列中之后,当我们取到一个消息后做的事情,也就是回到_runner函数中的逻辑:

File:oslo_messaging/_drivers/base.py  PollStyleListenerAdapter:_runner

def _runner(self):
    while self._started:
        # 这里poll调用oslo_messaging/_drivers/amqpdriver.py的AMQPListener类poll函数
        incoming = self._poll_style_listener.poll(
            batch_size=self.batch_size, batch_timeout=self.batch_timeout)

        # 读到有消息,调用回调函数进行处理
        if incoming:
            # 该on_incoming_callback是oslo_messaging/server.py中self.listener.start(self._on_incoming)中传进来的
            # 是该文件中MessageHandlingServer类的_on_incoming函数
            # _on_incoming函数又调用到了RPCServer中的_process_incoming函数
            self.on_incoming_callback(incoming)

 

可知是调用了_process_incoming函数来处理消息:

# 这里poll调用oslo_messaging/_drivers/amqpdriver.pyAMQPListenerpoll函数

# 处理消息

File:oslo_messaging/rpc/server.py  RPCServer:__process_incoming

def _process_incoming(self, incoming):
    message = incoming[0]
    try:
        # 这里进行了消息确认发送
        # 会调用到kombu/message.py的Message类的ack函数
        # 表示该消息已经进行消费了,队列中可以删除该消息了
        message.acknowledge()
    except Exception:
        LOG.exception(_LE("Can not acknowledge message. Skip processing"))
        return

    failure = None
    try:
        # 调用oslo_messaging/rpc/dispatcher.py的RPCDispatcher类来处理消息
        # 该类的职责是找到消息对应的方法并执行
        res = self.dispatcher.dispatch(message)
    except rpc_dispatcher.ExpectedException as e:
        failure = e.exc_info
        LOG.debug(u'Expected exception during message handling (%s)', e)
    except Exception:
        # current sys.exc_info() content can be overridden
        # by another exception raised by a log handler during
        # LOG.exception(). So keep a copy and delete it later.
        failure = sys.exc_info()
        LOG.exception(_LE('Exception during message handling'))

    try:
        # 将执行结果返回
        if failure is None:
            message.reply(res)
        else:
            message.reply(failure=failure)
    except Exception:
        LOG.exception(_LE("Can not send reply for message"))
    finally:
            # NOTE(dhellmann): Remove circular object reference
            # between the current stack frame and the traceback in
            # exc_info.
            del failure

 

核心函数是dispatch函数:

File:oslo_messaging/rpc/dispatcher.py  RPCDispatcherr:dispatch

def dispatch(self, incoming):
    """Dispatch an RPC message to the appropriate endpoint method.

    :param incoming: incoming message
    :type incoming: IncomingMessage
    :raises: NoSuchMethod, UnsupportedVersion
    """
    message = incoming.message
    ctxt = incoming.ctxt

    method = message.get('method')
    args = message.get('args', {})
    namespace = message.get('namespace')
    version = message.get('version', '1.0')

    found_compatible = False
    # endpoints值是两个类
    # [<nova.compute.manager.ComputeManager object at 0x7f6eef157dd0>, <nova.baserpc.BaseRPCAPI object at 0x7f6ee4724c90>]
    # 从类中查找出方法进行调用
    for endpoint in self.endpoints:
        target = getattr(endpoint, 'target', None)
        if not target:
            target = self._default_target

        if not (self._is_namespace(target, namespace) and
                self._is_compatible(target, version)):
            continue

        if hasattr(endpoint, method):
            if self.access_policy.is_allowed(endpoint, method):
                return self._do_dispatch(endpoint, method, ctxt, args)

        found_compatible = True

    if found_compatible:
        raise NoSuchMethod(method)
    else:
        raise UnsupportedVersion(version, method=method)

 

File:oslo_messaging/rpc/dispatcher.py  RPCDispatcherr:dispatch

def _do_dispatch(self, endpoint, method, ctxt, args):
    ctxt = self.serializer.deserialize_context(ctxt)
    new_args = dict()
    for argname, arg in args.items():
        new_args[argname] = self.serializer.deserialize_entity(ctxt, arg)
    func = getattr(endpoint, method)
    # 调用方法
    result = func(ctxt, **new_args)
    return self.serializer.serialize_entity(ctxt, result)

 

4.3  发送消息流程

这里看一个发送开机指令到宿主机执行的流程。

首先由novaclient发送http请求到nova-api服务,对应调用到_start_server函数:

File:nova/api/openstack/compute/server.py  ServersController:_start_server

def _start_server(self, req, id, body):
    .....
    try:
        # nova/compute/api.py
        self.compute_api.start(context, instance)
    except (exception.InstanceNotReady, exception.InstanceIsLocked) as e:
        .....

 

查看start方法实现:

File:nova/compute/api.py  API:start

def start(self, context, instance):
    .....
    instance.task_state = task_states.POWERING_ON
    instance.save(expected_task_state=[None])
    self._record_action_start(context, instance, instance_actions.START)
    self.compute_rpcapi.start_instance(context, instance)
    .....

 

可以看到是先将task_state状态改为了POWERING_ON后再发送消息,查看start_instance方法的实现:

File:nova/compute/rpcapi.py  ComputeAPI:start_instance

def start_instance(self, ctxt, instance):
    version = '4.0'
    # self.router.by_instance(ctxt, instance)获取oslo_messaging/rpc/client.py的RPCClient类实例
    # prepare方法是用于设置一些属性并生成一个oslo_messaging/rpc/client.py的_CallContext类实例
    cctxt = self.router.by_instance(ctxt, instance).prepare(
            server=_compute_host(None, instance), version=version)
    # 该call方法是调用到oslo_messaging/rpc/client.py的_BaseCallContext(_CallContext的父类)类的call方法
    return cctxt.call(ctxt, 'start_instance', instance=instance)

 

查看call方法实现:

File:oslo_messaging/rpc/client.py  _BaseCallContext:call

def call(self, ctxt, method, **kwargs):
    """Invoke a method and wait for a reply. See RPCClient.call()."""
    if self.target.fanout:
        raise exceptions.InvalidTarget('A call cannot be used with fanout',
                                       self.target)

    # 生成一个msg
    msg = self._make_message(ctxt, method, kwargs)
    # 序列化ctxt
    msg_ctxt = self.serializer.serialize_context(ctxt)

    timeout = self.timeout
    if self.timeout is None:
        timeout = self.conf.rpc_response_timeout

    self._check_version_cap(msg.get('version'))

    try:
        # 调用transport中的_send方法
        # _send方法中又是使用_driver对象调用send方法
        # _driver对象是oslo_messaging._drivers.impl_rabbit.RabbitDriver
        # 所以是调用RabbitDriver类里的send方法,实际是调用它父类AMQPDriverBase的send方法
        result = self.transport._send(self.target, msg_ctxt, msg,
                                      wait_for_reply=True, timeout=timeout,
                                      retry=self.retry)
    except driver_base.TransportDriverError as ex:
        raise ClientSendError(self.target, ex)

    return self.serializer.deserialize_entity(ctxt, result)

 

查看send方法实现:

File:oslo_messaging/_drivers/amqpdriver.py  AMQPDriverBase:_send

def _send(self, target, ctxt, message,
          wait_for_reply=None, timeout=None,
          envelope=True, notify=False, retry=None):

    msg = message

    if wait_for_reply:
        msg_id = uuid.uuid4().hex
        msg.update({'_msg_id': msg_id})
        # _get_reply_q方法获取一个ReplyWaiter对象,开始poll模式等待获取返回消息
        # 同时使用waiters(ReplyWaiter实例对象)管理返回的消息
        msg.update({'_reply_q': self._get_reply_q()})

    # 获取一个唯一uuid添加到msg字典中
    rpc_amqp._add_unique_id(msg)
    unique_id = msg[rpc_amqp.UNIQUE_ID]

    # 把ctxt字典值也更新到msg中
    rpc_amqp.pack_context(msg, ctxt)

    if envelope:
        msg = rpc_common.serialize_msg(msg)

    if wait_for_reply:
        # 把该消息加到waiters里去监控管理
        self._waiter.listen(msg_id)
        log_msg = "CALL msg_id: %s " % msg_id
    else:
        log_msg = "CAST unique_id: %s " % unique_id

    try:
        # 根据target保存的发送模式发送消息
        with self._get_connection(rpc_common.PURPOSE_SEND) as conn:
            if notify:
                exchange = self._get_exchange(target)
                log_msg += "NOTIFY exchange '%(exchange)s'" \
                           " topic '%(topic)s'" % {
                               'exchange': exchange,
                               'topic': target.topic}
                LOG.debug(log_msg)
                conn.notify_send(exchange, target.topic, msg, retry=retry)
            elif target.fanout:
                log_msg += "FANOUT topic '%(topic)s'" % {
                    'topic': target.topic}
                LOG.debug(log_msg)
                conn.fanout_send(target.topic, msg, retry=retry)
            else:
                topic = target.topic
                exchange = self._get_exchange(target)
                if target.server:
                    topic = '%s.%s' % (target.topic, target.server)
                log_msg += "exchange '%(exchange)s'" \
                           " topic '%(topic)s'" % {
                               'exchange': exchange,
                               'topic': topic}
                LOG.debug(log_msg)
                # 发送topic模式的队列
                # 调用oslo_messaging/_drivers/impl_rabbit.py中Connection类实例的topic_send方法
                conn.topic_send(exchange_name=exchange, topic=topic,
                                msg=msg, timeout=timeout, retry=retry)

        if wait_for_reply:
            # 等待返回或消息超时返回
            # 轮询方式检测消息有没有被返回并放到对应的字典中
            result = self._waiter.wait(msg_id, timeout)
            if isinstance(result, Exception):
                raise result
            return result
    finally:
        if wait_for_reply:
            self._waiter.unlisten(msg_id) 

当消息返回或消息超时时就返回结果给call调用,这个请求就完成了 

 

4.4  重连机制

当比如当前连接着的rabbitmq服务断开了时,则连接会断开,则需要重新建立连接,建立新的channel,然后重新建立消费者。

这个逻辑在Connection的consume函数中,是由监听类的poll函数进行不断调用的,我们看下该函数实现:

File:oslo_messaging/_drivers/impl_rabbit.py  Connection:consume

def consume(self, timeout=None):
    """Consume from all queues/consumers."""

    timer = rpc_common.DecayingTimer(duration=timeout)
    timer.start()

    def _raise_timeout(exc):
        LOG.debug('Timed out waiting for RPC response: %s', exc)
        raise rpc_common.Timeout()

    def _recoverable_error_callback(exc):
        # 判断异常类型是不是非Timeout类型,因为Timeout类型是由drain_events函数获取
        # 消息等待超时导致的,属于正常的异常,除了这种异常,其它异常则都会被视作需要重建消费者
        if not isinstance(exc, rpc_common.Timeout):
            self._new_tags = set(self._consumers.values())
        timer.check_return(_raise_timeout, exc)

    def _error_callback(exc):
        # 将异常交给_recoverable_error_callback函数进行处理
        _recoverable_error_callback(exc)
        LOG.error(_LE('Failed to consume message from queue: %s'),
                  exc)

    def _consume():
        # NOTE(sileht): in case the acknowledgment or requeue of a
        # message fail, the kombu transport can be disconnected
        # In this case, we must redeclare our consumers, so raise
        # a recoverable error to trigger the reconnection code.
        # 这里是判断了连接是否还正常,如果不正常,我们需要重新获取连接并且重新定义consumer
        if not self.connection.connected:
            # 这里抛错以进入重连机制
            raise self.connection.recoverable_connection_errors[0]

        while self._new_tags:
            for consumer, tag in self._consumers.items():
                if tag in self._new_tags:
                    # 如果是新标签则消费者也需建立
                    # 在重建channel时这里就是重建消费者了
                    # 这里consumer是该文件的Consumer类实例,该consume函数会调用到kombu中的consume函数定义消费者
                    consumer.consume(self, tag=tag)
                    self._new_tags.remove(tag)

        poll_timeout = (self._poll_timeout if timeout is None
                        else min(timeout, self._poll_timeout))
        while True:
            if self._consume_loop_stopped:
                return

            if self._heartbeat_supported_and_enabled():
                # 心跳检查,如果连不通则抛错
                # 抛错则会在kombu中进行重连机制
                self._heartbeat_check()

            try:
                # 调用kombu/connection.py的Connection类的drain_events方法,等待来自服务器的单个事件,所以这是事件触发型的
                # 其中里面的supports_librabbitmq()=False(因为环境支持’eventlet’,所以未采用’default’,所以返回False
                # 最后是调用到kombu/transport/pyamqp.py的drain_events方法
                # 再调用到amqp包的Connection类的drain_events(amqp/connection.py)
                self.connection.drain_events(timeout=poll_timeout)
                return
            except socket.timeout as exc:
                # 超时会进入这个逻辑,check_return会raise一个Exception,从而导致ensure中抛异常被捕获调用了error_callback函数
                # error_callback函数又调用了recoverable_error_callback函数
                # 从而导致日志中经常可以看到_recoverable_error_callback
                poll_timeout = timer.check_return(
                    _raise_timeout, exc, maximum=self._poll_timeout)

    with self._connection_lock:
        self.ensure(_consume,
                    recoverable_error_callback=_recoverable_error_callback,
                    error_callback=_error_callback)

 

这里很多内嵌函数都会通过传参的方式传入到其它方法中处理,然后由其它方法在检测到异常时执行。我们可以看到最终是执行了ensure函数,我们看下ensure函数的实现:

File:oslo_messaging/_drivers/impl_rabbit.py  Connection:ensure

def ensure(self, method, retry=None,
           recoverable_error_callback=None, error_callback=None,
           timeout_is_error=True):
    .....

    # 在kombu中如果进入了异常重连处理机制会回调该函数
    def on_error(exc, interval):
        LOG.debug("[%s] Received recoverable error from kombu:"
                  % self.connection_id,
                  exc_info=True)

        # 执行_recoverable_error_callback函数处理异常
        recoverable_error_callback and recoverable_error_callback(exc)

        interval = (self.kombu_reconnect_delay + interval
                    if self.kombu_reconnect_delay > 0
                    else interval)

        info = {'err_str': exc, 'sleep_time': interval}
        info.update(self._get_connection_info())

        if 'Socket closed' in six.text_type(exc):
            LOG.error(_LE('[%(connection_id)s] AMQP server'
                          ' %(hostname)s:%(port)s closed'
                          ' the connection. Check login credentials:'
                          ' %(err_str)s'), info)
        else:
            LOG.error(_LE('[%(connection_id)s] AMQP server on '
                          '%(hostname)s:%(port)s is unreachable: '
                          '%(err_str)s. Trying again in '
                          '%(sleep_time)d seconds. Client port: '
                          '%(client_port)s'), info)

    ......

    # 当在kombu中执行autoretry时抛出异常了并在异常处理时重新连接了其它节点
    # 则会回调该函数
    def on_reconnection(new_channel):
        # 更新channel
        self._set_current_channel(new_channel)
        self.set_transport_socket_timeout()

    def execute_method(channel):
        # 更新channel
        self._set_current_channel(channel)
        # 这个method指的就是_consume函数
        # 注意我这里指的是consume调入的时候该method就是_consume函数
        # 因为该ensure函数是很多函数都会调用的,每个函数都会传入它的method函数进行回调
        # 我这里是为了方便理解就这样指明了,文中还有很多地方也是如此指明,就不一一解释了
        method()

    # NOTE(sileht): Some dummy driver like the in-memory one doesn't
    # have notion of recoverable connection, so we must raise the original
    # exception like kombu does in this case.
    has_modern_errors = hasattr(
        self.connection.transport, 'recoverable_connection_errors',
    )
    if has_modern_errors:
        recoverable_errors = (
            self.connection.recoverable_channel_errors +
            self.connection.recoverable_connection_errors)
    else:
        recoverable_errors = ()

    try:
        autoretry_method = self.connection.autoretry(
            execute_method, channel=self.channel,
            max_retries=retry,
            errback=on_error,
            interval_start=self.interval_start or 1,
            interval_step=self.interval_stepping,
            interval_max=self.interval_max,
            on_revive=on_reconnection)
        ret, channel = autoretry_method()
        self._set_current_channel(channel)
        return ret
    except recoverable_errors as exc:
        LOG.debug("Received recoverable error from kombu:",
                  exc_info=True)
        # 在kombu重建立连接失败时会跑入该逻辑,调用error_callback
        # 如果是consume函数调入该函数的话,则该函数是_error_callback函数
        error_callback and error_callback(exc)
        self._set_current_channel(None)
        # NOTE(sileht): number of retry exceeded and the connection
        # is still broken
        info = {'err_str': exc, 'retry': retry}
        info.update(self.connection.info())
        msg = _('Unable to connect to AMQP server on '
                '%(hostname)s:%(port)s after %(retry)s '
                'tries: %(err_str)s') % info
        LOG.error(msg)
        raise exceptions.MessageDeliveryFailure(msg)
    except rpc_amqp.AMQPDestinationNotFound:
        # NOTE(sileht): we must reraise this without
        # trigger error_callback
        raise
    except Exception as exc:
        error_callback and error_callback(exc)
        Raise

 

这个函数也是如此,定义了很多内嵌函数,然后作为参数传递到kombu中的autoretry函数中进行处理,方便有异常时就行异常处理且回调相对应的函数。查看autoretry函数实现:

File:kombu/connection.py  Connection:autoretry

def autoretry(self, fun, channel=None, **ensure_options):
    channels = [channel]
    create_channel = self.channel

    class Revival(object):
        __name__ = getattr(fun, '__name__', None)
        __module__ = getattr(fun, '__module__', None)
        __doc__ = getattr(fun, '__doc__', None)

        def revive(self, channel):
            channels[0] = channel

        def __call__(self, *args, **kwargs):
            if channels[0] is None:
                self.revive(create_channel())
            # 执行oslo_messaging中impl_rabbit的ensure中的execute_method函数
            # execute_method最终又是回调到_consume函数
            return fun(*args, channel=channels[0], **kwargs), channels[0]

    revive = Revival()
    # 返回了一个_ensure闭包函数
    # 但oslo_messaging的ensure函数中的下一行便是执行该闭包函数
    return self.ensure(revive, revive, **ensure_options)

 

这里最后又调用了ensure函数,该ensure函数是关键,它里面进行了连接重连机制。查看ensure实现:

File:kombu/connection.py  Connection:ensure

def ensure(self, obj, fun, errback=None, max_retries=None,
           interval_start=1, interval_step=1, interval_max=1,
           on_revive=None):
           
    def _ensured(*args, **kwargs):
        got_connection = 0
        conn_errors = self.recoverable_connection_errors
        chan_errors = self.recoverable_channel_errors
        has_modern_errors = hasattr(
            self.transport, 'recoverable_connection_errors',
        )
        for retries in count(0):  # for infinity
            try:
                # 调用了Revival类的__call__函数
                # 在进行的一系列调用中如果有异常抛出则进入下面的重连机制
                return fun(*args, **kwargs)
            except conn_errors as exc:
                if got_connection and not has_modern_errors:
                    raise
                if max_retries is not None and retries > max_retries:
                    raise
                self._debug('ensure connection error: %r', exc, exc_info=1)
                self._connection = None
                self._do_close_self()
                errback and errback(exc, 0)
                remaining_retries = None
                if max_retries is not None:
                    remaining_retries = max(max_retries - retries, 1)
                # 尝试重新建立连接,确保有连接建立成功
                self.ensure_connection(errback,
                                       remaining_retries,
                                       interval_start,
                                       interval_step,
                                       interval_max)
                # 在连接上获取新的channel
                new_channel = self.channel()
                self.revive(new_channel)
                obj.revive(new_channel)
                if on_revive:
                    # 调用oslo_messaging中的on_reconnection函数
                    # 将获得的新channel赋给Connection类的channel
                    on_revive(new_channel)
                got_connection += 1
            except chan_errors as exc:
                if max_retries is not None and retries > max_retries:
                    raise
                self._debug('ensure channel error: %r', exc, exc_info=1)
                errback and errback(exc, 0)
    _ensured.__name__ = "%s(ensured)" % fun.__name__
    _ensured.__doc__ = fun.__doc__
    _ensured.__module__ = fun.__module__
    return _ensured        

可以看到如果是触发了异常则进入下面的异常处理,进行重连和回调函数调用等操作,所以如果进行了重连,就会触发到oslo_messaging那边定义的很多内嵌函数来协助处理重连逻辑。

 

posted @ 2019-06-26 00:18  luohaixian  阅读(3101)  评论(1编辑  收藏  举报