消息的传递与处理是IaaS软件设计中最重要的部分,对于IaaS软件面向的场景来说,消息的传递方式决定了任务执行的效率。根据3.1章节的描述,IaaS软件涵盖的场景包含了计算、网络、存储的虚拟化及管理,因此,IaaS软件需要和不同的硬件产生交互,这将导致IaaS软件上的任务运行的非常缓慢,通常一项任务完成需要花费几秒甚至几分钟。所以当整个系统被缓慢的任务填满的时候,新任务的延迟非常大是很正常的。执行缓慢的任务通常是由一个很长的任务路径组成的,比如,创建一个虚拟机,需要经过身份验证服务→调度器→镜像服务→存储服务→网络服务→虚拟机管理程序,每一个服务可能会花费几秒甚至几分钟去引导外部硬件完成一些操作,这极大地延长了任务执行的时间。
如何解决任务的缓慢执行问题,或者说,消息如何处理能够最大化的提高IaaS软件的执行效率?
为了解决这个问题,一个直观的想法是提高线程池容量,但是这个想法在实际中是不可行的,即使现代操作系统允许一个应用程序拥有成千上万条线程,没有操作系统可以非常有效率的调度他们。随后有一个想法是把线程分发出去,让不同的操作系统上相似的软件分布式的处理线程,因为每一个软件都有它自己的线程池,这样最终增加了整个系统的线程容量。然而,分发会带来一定的开销,它增加了管理的复杂度,同时集群软件在软件设计层面依旧是一个挑战。最后,IaaS软件自身变成云的瓶颈,而其他的基础设施包括数据库,消息代理和外部的系统(比如成千台物理服务器)都足够去处理更多的并发任务。
如果我们把目光投向IaaS软件和数据中心的设备之间的关系,我们会发现IaaS软件实际上扮演着一个协调者的角色,它负责协调外部系统但并不做任何真正耗时的操作。举个例子,存储系统可以分配磁盘容量,镜像系统可以下载镜像模板,虚拟机管理程序可以创建虚拟机。IaaS软件所做的工作是做决策然后把子任务分配给不同的外部系统。比如,对于KVM,KVM主机需要执行诸如准备磁盘、准备网络、创建虚拟机等子任务。创建一台虚拟机可能需要花费5 s,IaaS软件花费时间为0.5 s,剩下的4.5 s被KVM主机占用,如果采用异步架构,使IaaS管理软件不用等待4.5 s,它只需要花费0.5 s的时间选择让哪一台主机处理这个任务,然后把任务分派给那个主机。一旦主机完成了它的任务,它将结果通知给IaaS软件。通过异步架构,一个只有100条线程容量的线程池可以处理上千数的并发任务。
因此,可以得到一个结论,在对IaaS软件的消息处理机制做设计的时候,应该最大程度的使用异步架构。
让我们来看看目前主流的IaaS平台如何处理消息。
3.3.1.1 OpenStack消息处理机制
以OpenStack的核心组件Nova为例,在arch.rst中,对Nova的架构做了说明:
从说明中可以看到,云平台控制节点内部的和object store的消息处理和外部的消息处理使用不同的方式,内部的和object store的消息处理使用HTTP的方式,而和外部组件的通讯都是使用AMQP的消息队列方式。由于Nova是虚拟机的核心管理组件,大量的时间需要与外部的网络服务、云硬盘服务、镜像服务等打交道,因此外部的消息处理机制决定了OpenStack的消息处理效率。我们看一下Nova和外部组件的通讯方式具体是如何来实现。
根据文档里的描述,Nova使用Advanced Message Queuing Protocol(AMQP)和外部组件实现通信,那么,我们需要一起来了解一下,什么是AMQP?
AMQP从本质上来说是一个网络协议,它支持符合要求的客户端应用(application)和消息代理(message broker)之间进行通信。
消息代理(message broker)连接了消息的发布者(publishers)和消息的消费者(consumers),并且,他们三者可以存在于不同的设备上。
由于引入了较多新的概念,我们来看如图3-5的一个模型从而让读者更清晰的了解这些概念的作用。
图3-5 消息队列模型
在上面这个模型中,消息(message)被发布者(publisher)发送给交换器(exchange)。然后交换器将收到的消息根据路由规则分发给绑定的队列(queue)。最后AMQP代理会将消息投递给订阅了此队列的消费者(consumers),或者消费者按照需求自行获取。
发布者(publisher)发布消息时可以给消息指定各种消息属性(messagemeta-data)。有些属性有可能会被消息代理(brokers)使用,然而其他的属性则是完全不透明的,它们只能被接收消息的应用所使用。
OpenStack默认使用Rabbitmq作为AMQP的消息代理实现。这样可以保证消息发送的松耦合性。更精确地说,Nova组件使用RPC通信方式来进行服务之间的通信,但是这样的RPC通信仍然基于上文讨论的发布者消费者模式,这样可以得到以下几个好处:
(1)解耦了客户端和服务端,客户端不需要知道服务端在哪里,只需要往消息队列里面发送消息即可
(2)可以实现异步的通讯机制,客户端在发送消息的时候,不需要远程的服务端必须同时在线
(3)可以实现随机的负载均衡,例如当客户端消息非常多的时候,可以有多个服务端提供服务,并分别从消息队列里获取要处理的消息
OpenStack每个服务都可以连接消息队列,一个服务可以作为一个消息发送者Invoker(如API、Scheduler)连接消息队列,也可以作为一个消息接收者Worker(如compute、volume、network)连接消息队列。Invoker发送消息有两种方式:同步调用rpc.call和异步调用rpc.cast,Worker接受并根据rpc调用的信息返回消息
如图3-6的Nova架构图可以比较详细的体现Nova各个服务的是通过何种方式传递消息:
图3-6 Nova的架构图
从上图可以看到,Nova控制服务(Cloud Controller)和Nova调度器(Scheduler),以及Scheduler和云盘控制服务(Volume Controller)、网络控制服务(Network Controller)、计算节点控制服务(Compute Controller)之间,都是通过AMQP实现消息的传递。
Nova通过两种方式通过AMQP实现了RPC,一种是rpc.call,另外一种是rpc.cast,这两种的区别如下文:
(1)call():同步执行远程调用,调用会被阻塞直到结果返回,在一些调用时间较长的情况下会对效率产生很大的影响。(call()所调用的远程服务方法会被马上执行,在执行过程中调用者的进程会一直阻塞,直到返回结果。)
(2)cast():异步执行远程调用,调用不会被阻塞,结果也无须立即返回,在任务执行完毕之后,再返回结果,也可以人为指定返回的时间。因此对调用者来说,需要通过另外的手段去查询调用是否完成
每一个nova服务,例如compute服务,sheduler服务等等,都会在初始化的时候创建这两个队列。
下面再详细介绍一下rpc call和rpc cast的操作过程。
(1)RPC Call
图3-7展示了一个RPC Call的处理流程:
①消息发送者(Invoker)发送一个消息到消息队列(OpenStack的消息队列默认为RabbitMQ),同时创建并初始化一个Direct Consumer去等待返回的消息
②消息到了交换器(exchange)之后,将根据routing key(例如topic.host)通过不同的队列让Topic Consumer获取消息
③Topic Consumer获取消息后,即开始处理消息
④一旦消息处理完成,消息将通过Direct Publisher发送返回消息到消息队列
⑤当消息进入交换器,交换机将根据消息的routing key(例如msg_id以及对应的direct类型),被Direct Consumer获取并传递给消息发送者(Invoker)
(www.xing528.com)
图3-7 RPC Call处理流程图
(2)RPC Cast
图3-8展示了一个RPC Cast消息在消息队列的处理流程:
①消息发布者(Inovker)发送消息到消息队列
②和RPC Call一样,消息到了交换器(exchange)之后,将根据routing key(例如topic.host)通过不同的队列让Topic Consumer获取消息
③和RPCCall不同的是,RPCCast不会创建Direct Consumer,因为不需要立刻接收返回的消息
图3-8 RPC Cast处理流程图
说了原理,再看下OpenStack里的实现,先看异步的实现:
这里可以看到,rebuild_instance函数最终通过cctxt.cast把消息异步发送到消息队列。再看下OpenStack同步的实现:
这里可以看到,attach_interface函数最终通过cctxt.call把消息同步发送到消息队列。
3.3.1.2 ZStack消息处理机制
在IaaS软件中,最大的挑战是必须让所有组件都异步,并不只是一部分组件异步。举个例子,如果你在其他服务都是同步的条件下,建立一个异步的存储服务,整个系统性能并不会提升。因为在异步的调用存储服务时,调用的服务自身如果是同步的,那么调用的服务必须等待存储服务完成,才能进行下一步操作,这会使得整个工作流依旧是处于同步状态。
如图3-9所示:一个执行业务流程服务的线程同步调用了存储服务,然而存储服务和外部存储系统是异步通信的,因此它依旧需要等到存储服务返回之后,才能往下执行,这里的异步就等同于同步了。
图3-9 同步和异步都存在的消息处理流程图
ZStack采用了全异步的消息处理机制,异步架构包含了三个模块:异步消息,异步方法,异步HTTP调用。
□异步消息
ZStack使用自己研发的CloudBus消息队列作为一个消息总线连接各类服务,当一个服务调用另一个服务时,源服务发送一条消息给目标服务并注册一个回调函数,然后立即返回。一旦目标服务完成了任务,它返回一条消息触发源服务注册的回调函数。代码如下:
一个服务也可以同时发送一列消息给其他服务,然后异步的等待回复。
甚至,以设定的并行度发送一列消息也是可以实现的。也就是说,含有10条消息的消息列表,可以每次发送2条消息,也就是说第3、4条消息可以在第1、2条消息的回复收到后被发送。
□异步的方法
服务在ZStack中是一等公民,他们通过异步消息进行通信。在服务的内部,有非常多的组件、插件使用方法调用的方式来进行交互,这种方式也是异步的。
回调函数也可以有返回值:
□异步HTTP调用
ZStack使用一组agent去管理外部系统,比如:管理KVM主机的agent,管理控制台代理的agent,管理虚拟路由的agent等,这些agents大部分是搭建在Python CherryPy上的轻量级web服务器,最新的ZStack版本里也有部分使用go开发的agent代理程序。因为如果没有HTML5技术,如websockets技术,是没有办法进行双向通信的,ZStack每个请求的HTTP头部嵌入一个回调的URL,因此,在任务完成后,agents可以发送回复给调用者的URL。
通过以上三种方法,ZStack已经建立了一个可以使所有组件都异步进行操作的全局架构。
综上所述,ZStack和OpenStack都使用了异步的机制来传递消息,不同的是,ZStack更加激进,通过全异步的架构,完全将同步的调用舍弃,从而用最小的线程池保证了最高的并发度,同时任务和任务之间不会相互等待,最大程度地提高了IaaS系统的运行效率。但是随之带来的是程序员写代码时候的难度提升,程序员需要小心翼翼的处理异步的返回结果,以及考虑异步情况下的各种意外情况,避免程序出现bug。
图3-10 全异步的消息处理流程图
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。