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-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框架解析的更多相关文章
- Hadoop系列番外篇之一文搞懂Hadoop RPC框架及细节实现
@ 目录 Hadoop RPC 框架解析 1.Hadoop RPC框架概述 1.1 RPC框架特点 1.2 Hadoop RPC框架 2.Java基础知识回顾 2.1 Java反射机制与动态代理 2. ...
- [源码解析] PyTorch 分布式(15) --- 使用分布式 RPC 框架实现参数服务器
[源码解析] PyTorch 分布式(15) --- 使用分布式 RPC 框架实现参数服务器 目录 [源码解析] PyTorch 分布式(15) --- 使用分布式 RPC 框架实现参数服务器 0x0 ...
- [源码解析] PyTorch 分布式(17) --- 结合DDP和分布式 RPC 框架
[源码解析] PyTorch 分布式(17) --- 结合DDP和分布式 RPC 框架 目录 [源码解析] PyTorch 分布式(17) --- 结合DDP和分布式 RPC 框架 0x00 摘要 0 ...
- RPC框架实现 - 通信协议篇
RPC(Remote Procedure Call,远程过程调用)框架是分布式服务的基石,实现RPC框架需要考虑方方面面.其对业务隐藏了底层通信过程(TCP/UDP.打包/解包.序列化/反序列化),使 ...
- [转]新兵训练营系列课程——平台RPC框架介绍
原文:http://weibo.com/p/1001643875439147097368 课程大纲 1.RPC简介 1.1 什么是RPC 1.2 RPC与其他远程调用方式比较 2.Motan RPC框 ...
- 智能 RPC框架 (C++)
RPC中文叫远程函数调用,它是一种通信方式,只是看起来像普通的函数调用. 它包括三个基本要素: 1:服务端注册相应的(服务)函数(用于调用方调用) 2:调用方通过函数调用的方式将一些信息和参数打包到消 ...
- RPC框架之Thrift
目前流行的服务调用方式有很多种,例如基于SOAP消息格式的 Web Service,基于 JSON 消息格式的 RESTful 服务等.其中所用到的数据传输方式包括 XML,JSON 等,然而 XML ...
- Netty自娱自乐之类Dubbo RPC 框架设计构想 【上篇】
之前在前一篇的<Netty自娱自乐之协议栈设计>,菜鸟我已经自娱自乐了设计协议栈,gitHub地址为https://github.com/vOoT/ncustomer-protocal.先 ...
- 谁能用通俗的语言解释一下什么是 RPC 框架?
转载自知乎:https://www.zhihu.com/question/25536695 知乎上很多问题的答案还是很好的,R大就经常在上面回答问题 关于RPC你的题目是RPC框架,首先了解什么叫RP ...
随机推荐
- ASP.NET面试题130道
130道ASP.NET面试题 1. 简述 private. protected. public. internal 修饰符的访问权限. 答 . private : 私有成员, 在类的内部才可以访问. ...
- 创建Core项目使用IdentityServer4
本文主要参照https://www.bilibili.com/video/av42364337/?p=4 英文帮助文档:https://identityserver4.readthedocs.io/e ...
- 【openshift】在Openshift上通过yaml部署应用
在Openshift上通过yaml部署应用 1.通过直接执行yaml 通过如下命令直接执行 oc create -f nginx.yml nginx.yml apiVersion: v1 items: ...
- 压测工具wrk的编译安装与基础使用
Linux上编译安装: [root@centos ~]# cd /usr/local/src [root@centos ~]# yum install git -y [root@centos ~]# ...
- viewer 图片点击放大 用法汇总
A 不用viewer插件 1弹出框 https://www.cnblogs.com/web1/p/8989967.html 2表格中 https://www.jianshu.com/p/c17f4f6 ...
- 微信小程序 wxml 中使用 js函数
原文链接 1.在 utils 目录下 新建`filter.wxs` var filters = { toFix: function (value) { return value.toFixed(2) ...
- php 执行大量sql语句 MySQL server has gone away
php 设置超时时间单位秒 set_time_limit(3600); php 设置内存限制ini_set('memory_limit', '1024M'); mysql服务端接收到的包的大小 ...
- SpringCloud2.0 Zuul 网关路由 基础教程(十)
1.启动基础工程 1.1.启动[服务注册中心],工程名称:springcloud-eureka-server 参考 SpringCloud2.0 Eureka Server 服务中心 基础教程(二) ...
- 农业银行网上支付平台-商户接口编程-demo调试
调试的时候会报一个这样的错误. ReturnCode = [1999]ErrorMessage = [系统发生无法预期的错误 - 第1个证书无法读取证书文档] 网上其他资料说是权限问题,有的人可能是权 ...
- python安装脚本
[root@dn3 hadoop]# cat install.py #!/usr/bin/python #coding=utf- import os import sys : pass else: p ...