我们知道openstack内部消息队列基于AMQP协议,默认使用的rabbitmq 消息队列。谈到rabbitmq,大家或许并不陌生,但或许会对oslo message有些陌生。openstack内部并不是直接使用rabbitmq,而是使用了oslo.message 。oslo.message 后端的driver支持rabbitmq,kafka,zeromq等消息队列(目前只有rabbitmq能用于openstack) 。在 oslo message中封装了OpenStack各组件内部进行消息通信的方法,并将方法中所使用的数据结构封装为通用的类,以达到使用简单快捷、扩展性强的目的。
下面是rabbitmq 支持的模式和场景,简单回顾一下,包括简单模式,工作队列模式,订阅发布模式,路由模式,topics模式,RPC模式。
官网有详细的使用说明, 具体rabbitmq 的使用可以参考 https://www.rabbitmq.com/tutorials/tutorial-one-python
前面提到了openstack内部消息通信其实是使用的oslo.message库,关于oslo message 主要提供俩种主要功能:
远程过程调用 RPC:一个服务进程可以调用其他远程服务进程的方法。调用的方式:
rpc.call ():远程方法会被同步执行,调用者会被阻塞直到返回方法的结果,在一些调用时间较长的场合中使用会对效率有很大的影响。
rpc.cast():远程服务的方法会被异步执行,调用者不会被阻塞,结果也无须立即返回,因为是异步,所以也要求调用者利用其他的方法来查询这次远程调用的结果。
事件通知:某一个服务进程将事件通知发送到消息总线上,所有在消息总线上且对该事件通知感兴趣的服务进程都可以将该事件通知获取并进行处理,执行的结果并不需要返回给事件发送者。这种方式不仅可以在项目组件内部的进程服务通信间实现,还可以在项目之间的通信中实现比如计量计费等。
Oslo.message中的几个重要概念:
-
server:rpc 服务端,包含一个或多个端点(Endpoint),每个端点包含一组远程调用的方法,这组方法可以被客户端通过transport对象远程调用。创建Server对象时,需要指定Transport、Target和一组endpoint。
-
client:rpc 客户端, 负责调用服务端提供的RPC接口。
-
exchange:rabbitmq中的概念,一种交换实现,负责把消息交换到相对应队列上。
-
namespace:服务器端可以在一个主体上暴露多组方法,每组方法属于一个命名空间。
-
method:方法,方法由一个名字和相关参数组成。
-
transport:顾名思义:运输工具,就是运输载体,一个传送RPC 请求到服务器端并将响应返回给客户端的底层消息系统。目前主要使用的transport有rabbitmq和qpid。
URL格式:Transport://user:password@hotst1:port[,host:port]/virtual_host
-
API version:每个命名空间都有一个版本号,当命名空间的接口变化时,这个版本号也会响应增加。向前兼容的修改只需要更改小版本号,向前不兼容的更改需要更改大版本号。
-
Target:目的地,指定某一个消息最终目的地的所有信息。Target中封装了所有将要用到的信息,以确定应该将消息发送到何处或服务器正在侦听什么信息。
下面讲解一下组件cinder 组件内部的 rpc 通信, 从在rpc client和 rpc server端从代码看具体实现,
RPC Client
当cinder-api 收到创建volume 云硬盘时,cinder-scheduler 调度完资源filter出合适的backend之后,在cinder.scheduler.rpcapi 代码中,rpc client 发出创建volume 的rpc 请求 , 代码是下面这样的:
ruby
def create_volume(self, ctxt, volume, snapshot_id=None, image_id=None, request_spec=None, filter_properties=None, backup_id=None): volume.create_worker() cctxt = self._get_cctxt() msg_args = {'snapshot_id': snapshot_id, 'image_id': image_id, 'request_spec': request_spec, 'filter_properties': filter_properties, 'volume': volume, 'backup_id': backup_id} if not self.client.can_send_version('3.10'): msg_args.pop('backup_id') return cctxt.cast(ctxt, 'create_volume', **msg_args)
由前面所提到的,cast和call分别对应异步和同步请求。当调用cast或者call时,通过oslo.message库序列化消息体,通过 调用transport._send 发送到哪个target,transport 会调用对应driver 比如 AMQPDriverBase.send方法。从连接池中获取到 rabbitmq connection 连接,根据消息类型,选择通过topic exchange还是fanout exchange等模式 , 调用 kombu(类似于pika,但是支持重连策略以及连接池功能等)发送到对应的消息队列中。
sql
try:
with self._get_connection(rpc_common.PURPOSE_SEND, retry) as conn:
if notify:
exchange = self._get_exchange(target)
LOG.debug(log_msg + "NOTIFY exchange '%(exchange)s'"
" topic '%(topic)s'", {'exchange': exchange,
'topic': target.topic})
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.debug(log_msg + "exchange '%(exchange)s'"
" topic '%(topic)s'", {'exchange': exchange,
'topic': topic})
conn.topic_send(exchange_name=exchange, topic=topic,
msg=msg, timeout=timeout, retry=retry,
transport_options=transport_options)
那么send 完发送到队列中后,服务端怎么就能执行到对应的方法呢,我们看下rpc server端的实现
RPC Server
以cinder-volume 为例,在cinder-volume 服务启动时,会先初始化rpc 再启动rpc server,其实每个服务都是这样。
通过在cinder.service.Service.start 函数中,调用 messaging.get_rpc_server 构造rpc_server对象,调用rpc_server对象start方法启动。
每个组件通过service start时 ,会启动相关的rpc 服务。
ruby
if not rpc.initialized():
rpc.init(CONF)
endpoints = [self.manager]
endpoints.extend(self.manager.additional_endpoints)
serializer = objects_base.CinderObjectSerializer(obj_version_cap)
target = messaging.Target(topic=self.topic, server=self.host)
self.rpcserver = rpc.get_server(target, endpoints, serializer)
self.rpcserver.start()
if self.topic == constants.VOLUME_TOPIC:
target = messaging.Target(
topic='%(topic)s.%(host)s' % {'topic': self.topic,
'host': self.host},
server=vol_utils.extract_host(self.host, 'host'))
self.backend_rpcserver = rpc.get_server(target, endpoints,
serializer)
self.backend_rpcserver.start()
由上面可以看到在构造 messaging.get_rpc_server 实例时 ,传入TRANSPORT,target,endpoints, json serializer,其中TRANSPORT 传的rabbitmq,target 传入的是包含当前服务 topic和本机名称的target对象,endpoints这里传的就是VolumeManager对象,serializer传入的是json serializer,最终通过构造了RPCServer实例,self.rpcserver.start() 启动会 启动 max_workers 大小的eventlet 协程池,创建 listener , 并不断处理incoming的message。RPCServer. processincoming 中取到message后,确认消息,并dispatch消息到对应的endpint上
python
class RPCServer(msg_server.MessageHandlingServer):
def _process_incoming(self, incoming):
try:
res = self.dispatcher.dispatch(message)
...........
swift
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)
oslo.message 在接收到dispatch的message后,解析message中的method,args,namespace,version等,并遍历endpoints,如果endpoint含有对应method,则反射执行,并最终反序列化返回结果
总体来说:相比较其他消息队列,比如kafka,redis,pulsar,rocketMQ ,rabbitmq算是功能比较丰富的消息队列了,openstack社区实现的 oslo message 完美的基于rabbitmq 很好的实现了一套内部组件的消息通信功能。可能这种消息通信方式用的比较少,但是代码实现上来说很有深度,尤其在较大型项目中,很有借鉴价值。
推荐阅读:
更多技术和产品文章,请关注👆
如果您对哪个产品感兴趣,欢迎留言给我们,我们会定向邀文~
go
360智汇云是以"汇聚数据价值,助力智能未来"为目标的企业应用开放服务平台,融合360丰富的产品、技术力量,为客户提供平台服务。
目前,智汇云提供数据库、中间件、存储、大数据、人工智能、计算、网络、视联物联与通信等多种产品服务以及一站式解决方案,助力客户降本增效,累计服务业务1000+。
智汇云致力于为各行各业的业务及应用提供强有力的产品、技术服务,帮助企业和业务实现更大的商业价值。
官网:https://zyun.360.cn 或搜索"360智汇云"
客服电话:4000052360