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-api对245上的虚拟机进行关机操作,245的compute.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类作用:获取了kombu的connection对象,用于进行消费者、队列定义和重连接等逻辑相关操作,使用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函数进行获取消息,该函数会调用到kombu的Connection类的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.py的Connection类的drain_events方法,等待来自服务器的单个事件,所以这是事件触发型的

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

# 最后是调用到kombu/transport/pyamqp.py的drain_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实例append到incoming队列中。

消息到队列中之后,当我们取到一个消息后做的事情,也就是回到_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.py的AMQPListener类poll函数

# 处理消息

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那边定义的很多内嵌函数来协助处理重连逻辑。

OpenStack RPC框架解析的更多相关文章

  1. Hadoop系列番外篇之一文搞懂Hadoop RPC框架及细节实现

    @ 目录 Hadoop RPC 框架解析 1.Hadoop RPC框架概述 1.1 RPC框架特点 1.2 Hadoop RPC框架 2.Java基础知识回顾 2.1 Java反射机制与动态代理 2. ...

  2. [源码解析] PyTorch 分布式(15) --- 使用分布式 RPC 框架实现参数服务器

    [源码解析] PyTorch 分布式(15) --- 使用分布式 RPC 框架实现参数服务器 目录 [源码解析] PyTorch 分布式(15) --- 使用分布式 RPC 框架实现参数服务器 0x0 ...

  3. [源码解析] PyTorch 分布式(17) --- 结合DDP和分布式 RPC 框架

    [源码解析] PyTorch 分布式(17) --- 结合DDP和分布式 RPC 框架 目录 [源码解析] PyTorch 分布式(17) --- 结合DDP和分布式 RPC 框架 0x00 摘要 0 ...

  4. RPC框架实现 - 通信协议篇

    RPC(Remote Procedure Call,远程过程调用)框架是分布式服务的基石,实现RPC框架需要考虑方方面面.其对业务隐藏了底层通信过程(TCP/UDP.打包/解包.序列化/反序列化),使 ...

  5. [转]新兵训练营系列课程——平台RPC框架介绍

    原文:http://weibo.com/p/1001643875439147097368 课程大纲 1.RPC简介 1.1 什么是RPC 1.2 RPC与其他远程调用方式比较 2.Motan RPC框 ...

  6. 智能 RPC框架 (C++)

    RPC中文叫远程函数调用,它是一种通信方式,只是看起来像普通的函数调用. 它包括三个基本要素: 1:服务端注册相应的(服务)函数(用于调用方调用) 2:调用方通过函数调用的方式将一些信息和参数打包到消 ...

  7. RPC框架之Thrift

    目前流行的服务调用方式有很多种,例如基于SOAP消息格式的 Web Service,基于 JSON 消息格式的 RESTful 服务等.其中所用到的数据传输方式包括 XML,JSON 等,然而 XML ...

  8. Netty自娱自乐之类Dubbo RPC 框架设计构想 【上篇】

    之前在前一篇的<Netty自娱自乐之协议栈设计>,菜鸟我已经自娱自乐了设计协议栈,gitHub地址为https://github.com/vOoT/ncustomer-protocal.先 ...

  9. 谁能用通俗的语言解释一下什么是 RPC 框架?

    转载自知乎:https://www.zhihu.com/question/25536695 知乎上很多问题的答案还是很好的,R大就经常在上面回答问题 关于RPC你的题目是RPC框架,首先了解什么叫RP ...

随机推荐

  1. go语言学习笔记----模拟实现文件拷贝函数

    实例1 //main package main import ( "bufio" "flag" "fmt" "io" & ...

  2. 【.Net Core】编译时禁止自动生成netcoreapp文件夹

    原文:[.Net Core]编译时禁止自动生成netcoreapp文件夹 每次在编译生成文件时,VS都会自动在<OutputPath>属性指定的路劲后再追加一个用NetCore命名的文件夹 ...

  3. 如何在 WPF 中获取所有已经显式赋过值的依赖项属性

    原文:如何在 WPF 中获取所有已经显式赋过值的依赖项属性 获取 WPF 的依赖项属性的值时,会依照优先级去各个级别获取.这样,无论你什么时候去获取依赖项属性,都至少是有一个有效值的.有什么方法可以获 ...

  4. VBA编程图表(二十一)

    使用VBA,可以根据特定标准生成图表.下面通过一个例子来看看它如何实现. 第1步 - 输入要生成图形的数据. 第2步 - 创建3个按钮 - 一个生成条形图,另一个生成饼图,另一个生成柱形图. 第3步 ...

  5. js原型,原型链

    先铺垫下原型规则: 1.所有的引用类型(数组,对象,函数)都具有对象特性,可自由扩展属性(出了null外) 2.所有的引用类型(数组,对象,函数)都有一个__proto__属性(隐式原型),属性值是一 ...

  6. Ldr和bl指令

    Ldr和bl在启动程序中,都是可以负责pc跳转的指令. 1)bl是地址无关指令,和什么地址无关呢?和当前的运行地址无关,链接器脚本中标明了一个运行地址,但是arm中的代码实际是从地址0开始运行的.这个 ...

  7. org.apache.shiro.session.UnknownSessionException: There is no session with id [xxxx]的解决方案

    org.apache.shiro.session.UnknownSessionException: There is no session with id [xxxx]的解决方案 背景描述 Sprin ...

  8. Python学习日记(十九) 模块导入

    模块导入 当文件夹中有这样一个自定义的command模块 在它的内部写下下列代码: print('这个py文件被调用!') def fuc(): print('这个函数被调用!') 然后我们在comm ...

  9. python(函数调用)

    1.在原文件中调用 def abc(x,y): print x + y abc(2,3) #直接通过函数名加括号进行调用传参 2.同一个包(package)下面调用不同文件中的函数 "&qu ...

  10. DELL R730 做raid10

    1.服务器开机,在出现下图提示时,同时按着<ctrl >+ < R >键,即可进入配置界面 2.会进入下图 3.按上下键到第一项PERC H730P MINI ,按F2,选择c ...