nova中调用rpc向server发送命令请求的模块为各个组件的rpcapi,我们以nova.compute.rpcapi为例,先看ComputeAPI
这个类的初始化:
def __init__(self):
super(ComputeAPI, self).__init__()
target = messaging.Target(topic=CONF.compute_topic, version='4.0')
upgrade_level = CONF.upgrade_levels.compute
if upgrade_level == 'auto':
version_cap = self._determine_version_cap(target)
else:
version_cap = self.VERSION_ALIASES.get(upgrade_level,
upgrade_level)
serializer = objects_base.NovaObjectSerializer()
default_client = self.get_client(target, version_cap, serializer)
self.router = rpc.ClientRouter(default_client)
以上初始化时获取的属性在后期调用时候进行分析,现在我们看发起rpc请求时的逻辑。例如,在api需要compute节点执行attach_interface
操作时发起的请求:
def attach_interface(self, ctxt, instance, network_id, port_id,
requested_ip):
version = '4.0'
cctxt = self.router.by_instance(ctxt, instance).prepare(
server=_compute_host(None, instance), version=version)
return cctxt.call(ctxt, 'attach_interface',
instance=instance, network_id=network_id,
port_id=port_id, requested_ip=requested_ip)
在发起请求时,会给出instance,是请求操作虚拟机的数据模型。将它传入self.router.by_instance
中:
def by_instance(self, context, instance):
try:
cell_mapping = objects.InstanceMapping.get_by_instance_uuid(
context, instance.uuid).cell_mapping
except nova.exception.InstanceMappingNotFound:
# Not a cells v2 deployment
cell_mapping = None
return self._client(context, cell_mapping=cell_mapping)
在此取出instance相关的cell_mapping。我们暂不考虑使用cell的情况,所以cell_mapping=None:
def _client(self, context, cell_mapping=None):
if cell_mapping:
client_id = cell_mapping.uuid
else:
client_id = 'default'
try:
client = self.clients[client_id].client
except KeyError:
transport = create_transport(cell_mapping.transport_url)
client = messaging.RPCClient(transport, self.target,
version_cap=self.version_cap,
serializer=self.serializer)
self.clients[client_id] = ClientWrapper(client)
return client
当前情况下,返回的client即为self.router初始化时定义的self.clients[‘default’]:self.clients['default'] = ClientWrapper(default_client)
:
class ClientWrapper(object):
def __init__(self, client):
self._client = client
self.last_access_time = timeutils.utcnow()
@property
def client(self):
self.last_access_time = timeutils.utcnow()
return self._client
到此可知by_instance方法拿到的返回值即为default_client,再继续看代码:
version = '4.0'
cctxt = self.router.by_instance(ctxt, instance).prepare(
server=_compute_host(None, instance), version=version)
得知调用了其prepare方法,传入了两个参数server和version,其中server的获取代码简单不贴出,值为从instance数据模型中取出的虚拟机所在物理机地址。我们看client的prepare方法:
def prepare(self, exchange=_marker, topic=_marker, namespace=_marker,
version=_marker, server=_marker, fanout=_marker,
timeout=_marker, version_cap=_marker, retry=_marker)
return _CallContext._prepare(self,
exchange, topic, namespace,
version, server, fanout,
timeout, version_cap, retry)
↓
class _CallContext(_BaseCallContext):
_marker = _BaseCallContext._marker
@classmethod
def _prepare(cls, call_context,
exchange=_marker, topic=_marker, namespace=_marker,
version=_marker, server=_marker, fanout=_marker,
timeout=_marker, version_cap=_marker, retry=_marker):
cls._check_version(version)
kwargs = dict(
exchange=exchange,
topic=topic,
namespace=namespace,
version=version,
server=server,
fanout=fanout)
kwargs = dict([(k, v) for k, v in kwargs.items()
if v is not cls._marker])
target = call_context.target(**kwargs)
if timeout is cls._marker:
timeout = call_context.timeout
if version_cap is cls._marker:
version_cap = call_context.version_cap
if retry is cls._marker:
retry = call_context.retry
return _CallContext(call_context.transport, target,
call_context.serializer,
timeout, version_cap, retry)
↓
class _BaseCallContext(object):
_marker = object()
def __init__(self, transport, target, serializer,
timeout=None, version_cap=None, retry=None):
self.conf = transport.conf
self.transport = transport
self.target = target
self.serializer = serializer
self.timeout = timeout
self.retry = retry
self.version_cap = version_cap
super(_BaseCallContext, self).__init__()
包装比较迂回,简单解释代码逻辑:
exchange/topic/namespace/version/server/fanout
这几项参数进行过滤,选出不是_marker的参数,并将其传入绑定的target实例。而在target实例支持的方法中,覆写了__call__
: def __call__(self, **kwargs):
for a in ('exchange', 'topic', 'namespace',
'version', 'server', 'fanout'):
kwargs.setdefault(a, getattr(self, a))
return Target(**kwargs)
可知在此生成了新的target,并拥有绑定target的topic和传入的server、version属性
call_context.transport = self.client.transport = TRANSPORT
target = 新生成的target
call_context.serializer = self.client.serializer = rpcapi初始化时的serializer
timeout = None
version_cap = rpcapi初始化时的version_cap = mitaka的version = '4.11'
retry = None
好的,再次回到顶层,继续看代码:
return cctxt.call(ctxt, 'attach_interface',
instance=instance, network_id=network_id,
port_id=port_id, requested_ip=requested_ip)
可知其调取的是_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 = self._make_message(ctxt, method, kwargs)
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:
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)
分析逻辑:
method = 'attach_interface'
kwargs = dict(instance=instance, network_id=network_id,
port_id=port_id, requested_ip=requested_ip)
def _make_message(self, ctxt, method, args):
msg = dict(method=method)
msg['args'] = dict()
for argname, arg in args.items():
msg['args'][argname] = self.serializer.serialize_entity(ctxt, arg)
if self.target.namespace is not None:
msg['namespace'] = self.target.namespace
if self.target.version is not None:
msg['version'] = self.target.version
return msg
将参数组合、序列化后,形成字典,格式为dict=(method=method, args={argname:序列化后的arg}, namespace=self.target.namespace(如果有), version=self.target.version)
做完上述步骤后,将参数传入self.transport._send方法,即TRANSPORT.send。由于是同步call,需要等待回复,wait_for_reply设置为True:
try:
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)
↓
def _send(self, target, ctxt, message, wait_for_reply=None, timeout=None,
retry=None):
if not target.topic:
raise exceptions.InvalidTarget('A topic is required to send',
target)
return self._driver.send(target, ctxt, message,
wait_for_reply=wait_for_reply,
timeout=timeout, retry=retry)
↓
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})
msg.update({'_reply_q': self._get_reply_q()})
rpc_amqp._add_unique_id(msg)
unique_id = msg[rpc_amqp.UNIQUE_ID]
rpc_amqp.pack_context(msg, ctxt)
if envelope:
msg = rpc_common.serialize_msg(msg)
if wait_for_reply:
self._waiter.listen(msg_id)
log_msg = "CALL msg_id: %s " % msg_id
else:
log_msg = "CAST unique_id: %s " % unique_id
try:
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)
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)
通过TRANSPORT逻辑可知最后调用了messaging._drivers_amqpdriver.AMQPDriverBase的_send方法,解析逻辑:
_get_reply_q
: def _get_reply_q(self):
with self._reply_q_lock:
if self._reply_q is not None:
return self._reply_q
reply_q = 'reply_' + uuid.uuid4().hex
conn = self._get_connection(rpc_common.PURPOSE_LISTEN)
self._waiter = ReplyWaiter(reply_q, conn,
self._allowed_remote_exmods)
self._reply_q = reply_q
self._reply_q_conn = conn
return self._reply_q
to_dict()
,并使用字典items()方法取出所有键值对(k, v)。在message中插入规则为 '_context_%s' % k
的key,其value为vmsg = {'oslo.version': '2.0', 'oslo.message': jsonutils.dumps(message)}
def listen(self, msg_id):
self.waiters.add(msg_id)
↓
self.waiters = ReplyWaiters()
↓
def add(self, msg_id):
self._queues[msg_id] = moves.queue.Queue()
queues_length = len(self._queues)
if queues_length > self._wrn_threshold:
LOG.warning(_LW('Number of call queues is %(queues_length)s, '
'greater than warning threshold: %(old_threshold)s'
'. There could be a leak. Increasing threshold to:'
' %(threshold)s'),
{'queues_length': queues_length,
'old_threshold': self._wrn_threshold,
'threshold': self._wrn_threshold * 2})
self._wrn_threshold *= 2
可知生成了一个FIFO的queue,并赋值在ReplyWaiter类的字典属性self._queue[msg_id]上
with self._get_connection(rpc_common.PURPOSE_SEND) as conn
,从之前建立好的connection pool中取出一个连接,赋值为conn.connectiontopic = 'compute.%s' % target.server, exchange = 'openstack'
def topic_send(self, exchange_name, topic, msg, timeout=None, retry=None):
"""Send a 'topic' message."""
exchange = kombu.entity.Exchange(
name=exchange_name,
type='topic',
durable=self.amqp_durable_queues,
auto_delete=self.amqp_auto_delete)
self._ensure_publishing(self._publish, exchange, msg,
routing_key=topic, timeout=timeout,
retry=retry)
↓
def _publish(self, exchange, msg, routing_key=None, timeout=None):
"""Publish a message."""
if not (exchange.passive or exchange.name in self._declared_exchanges):
exchange(self.channel).declare()
self._declared_exchanges.add(exchange.name)
log_info = {'msg': msg,
'who': exchange or 'default',
'key': routing_key}
LOG.trace('Connection._publish: sending message %(msg)s to'
' %(who)s with routing key %(key)s', log_info)
# NOTE(sileht): no need to wait more, caller expects
# a answer before timeout is reached
with self._transport_socket_timeout(timeout):
self._producer.publish(msg,
exchange=exchange,
routing_key=routing_key,
expiration=timeout,
compression=self.kombu_compression)
分析逻辑:
name = 'openstack', type='topic', durable=False, auto_delete=False
;_publish
方法,首先判断exchange.passive(默认False)及exchange.name是否在self._declared_exchanges中,如都为False,则绑定目前的channel至exchange,调用declare在librabbitmq的channel上定义exchange,并将exchange.name加入self._declared_exchanges中 def publish(self, body, routing_key=None, delivery_mode=None,
mandatory=False, immediate=False, priority=0,
content_type=None, content_encoding=None, serializer=None,
headers=None, compression=None, exchange=None, retry=False,
retry_policy=None, declare=[], expiration=None, **properties):
if isinstance(exchange, Exchange):
delivery_mode = delivery_mode or exchange.delivery_mode
exchange = exchange.name
else:
delivery_mode = delivery_mode or self.exchange.delivery_mode
if not isinstance(delivery_mode, numbers.Integral):
delivery_mode = DELIVERY_MODES[delivery_mode]
properties['delivery_mode'] = delivery_mode
if expiration is not None:
properties['expiration'] = str(int(expiration*1000))
body, content_type, content_encoding = self._prepare(
body, serializer, content_type, content_encoding,
compression, headers)
publish = self._publish
if retry:
publish = self.connection.ensure(self, publish, **retry_policy)
return publish(body, priority, content_type,
content_encoding, headers, properties,
routing_key, mandatory, immediate, exchange, declare)
↓
def _publish(self, body, priority, content_type, content_encoding,
headers, properties, routing_key, mandatory,
immediate, exchange, declare):
channel = self.channel
message = channel.prepare_message(
body, priority, content_type,
content_encoding, headers, properties,
)
if declare:
maybe_declare = self.maybe_declare
[maybe_declare(entity) for entity in declare]
return channel.basic_publish(
message,
exchange=exchange, routing_key=routing_key,
mandatory=mandatory, immediate=immediate,
)
简单逻辑为,将body(message)进行编码等转换后,配合生成的参数传入self._publish
。在此方法中,按照librabbitmq所需的格式准备message,最终通过librabbitmq中的channel.basic_publih发出消息。