在上一结中,我们阐述了IaaS软件中的异步架构,尤其是全异步架构,可以让使得单个IaaS管理节点的组件足以承担大量的发到云平台的请求负载;然而当用户想要去创建一个高可用的生产环境或处理海量的并发请求负载,一个管理节点也许是不够的。解决方案是建立一个可以负载均衡的分布式系统,这种通过添加新的节点来拓展整个系统的能力的方法被称为横向拓展。
设计一个分布式系统不是一件简单的事情。一个分布式系统,特别是一个有状态的系统,必须处理一致性(consistency)、可用性(availability)和分区容忍性(partition tolerance)(CAP理论)的问题,每一个问题都是非常复杂的。与之相反,一个无状态的分布式系统一定程度上降低了复杂度。第一,因为节点不用分享状态,整个系统的一致性是可以保证的。第二,因为节点都是相似的,对于分区问题系统通常是可以容忍的。因此,通常把一个分布式系统设计为无状态的而不是有状态的。但是设计一个无状态的分布式系统通常比设计一个有状态的分布式系统难得多。但为了解决CAP问题,我们更加倾向于使用无状态的设计。
为了讨论方便,在本章中,“服务”和“服务实例”是可以互换的。在讨论什么是无状态的服务之前,我们首先理解什么是“状态”。对提供同样功能的服务来说,当整个系统的服务实例不止一个的时候,资源会被分发到不同的服务实例中。如图3-11所示,假设有10 000台虚拟机和两个虚拟机服务实例,理想状态下每个实例会管理5 000台虚拟机。
如图3-12所示,因为有两个服务实例,在向一个虚拟机发出请求前,请求者必须知道哪个实例管理哪个虚拟机,否则,他将不知道向哪个实例发出请求。类似“哪个服务实例管理哪个资源”的信息就是我们所说的状态。如果一个服务是有状态的,每个服务维护自己的状态。请求者必须可以获取到当前的状态信息。当服务实例的数量改变的时候,服务需要去改变状态。
状态的改变是危险且易错的,这通常限制了整个系统的可拓展性。为了使整个系统的可靠性和横向拓展性增强,把状态和服务分离开,使得服务无状态是比较理想的解决办法。无状态的服务使请求者不需要询问向哪里发送请求,当新添一个新的服务实例或者删除一个旧的服务实例的时候,服务之间也不用交换状态。因此,对一个云平台来说,最佳实践是尽可能的使用无状态服务。
图3-11 同步和异步都存在的消息处理流程图
图3-12 服务的状态及服务状态的改变
3.3.2.1 OpenStack分布式设计
OpenStack的服务分为两种类型,其中,nova-api,nova-conductor,glance-api,keystoneapi,neutron-api和nova-scheduler等服务是无状态服务。而neutron-l3-agent、neutronmetadata-agent、nova-compute、cinder-volume等服务仍然是有状态服务。对于无状态服务,一般使用Keepalived+HAProxy实现分布式设计。如下图所示:
如图3-13所示,OpenStack完全依赖Haproxy和Keepalived,对外暴露一个VIP提供服务,对内通过HAProxy负载均衡流量到OpenStack的服务。生产环境中,OpenStack建议至少部署三台控制节点。
了解了架构,我们也看一下OpenStack如何来做具体的配置,对OpenStack来说,需要在每个控制节点(这里以三个为例),配置对应的/etc/haproxy/haproxy.cfg文件,三个控制节点上的配置文件内容需保持一致。以下是配置文件的一个示例:
图3-13 OpenStack分布式服务设计
从上述配置可以看到,需要对OpenStack的每一个无状态服务配置对应的访问IP,负载均衡方法,以及对应的服务所在地址。OpenStack的分布式集群默认使用主备方式,三个控制节点只有一个对外提供服务,另外两个出于standby状态。OpenStack官方的建议是这样做,可以保证写请求只有一个节点发起,生产环境上OpenStack还没有办法处理多节点的同时写请求场景。
3.3.2.2 ZStack分布式设计(www.xing528.com)
正如我们在3.2.4所论述的,ZStack和其他云平台的一个典型区别是使用了进程内微服务架构,因此,一个ZStack管理节点进程包含了所有的云平台服务。针对ZStack的实现逻辑,无状态服务的核心实现技术,除了使用Haproxy+Keepalived提供VIP之外,最重要的是一致性哈希算法。当系统启动的时候,每一个管理节点将被分配一个version 4 UUID(管理节点UUID),这个UUID将和服务名称拼在一起在消息代理上注册一个服务队列。例如,一个管理节点可能有类似下面的服务队列:
读者应该已经注意到所有的队列都是以一个相同的管理节点的UUID结尾的。
主机,磁盘,虚拟机等资源也有特定的UUID。和资源相关的消息通常在服务之间传递,在发送一个消息之前,发送者必须基于资源的UUID选择一个接收服务,一致性哈希算法这时候就发挥作用了。
一致性哈希是一种较特别的哈希,当一个哈希表的大小发生变化时,只有一部分键需要被重新映射。如图3-14,在ZStack中,管理节点组成了一个一致性哈希环:
图3-14 ZStack一致性哈希环
每一个管理节点维护了一份包含系统中所有管理节点的UUID的环拷贝,当一个管理节点添加或删除的时候,一个生命周期事件将通过消息代理广播到其他的管理节点,这将导致这些节点拓展或者收缩环去描述当前系统的状态。当发送一条消息时,发送者将使用资源的UUID哈希得出目标管理节点的UUID。例如,当VM的UUID是932763162d054c04adaab6ab498c9139时发送一个StartVm InstanceMsg,伪代码如下所示:
如果3-15所示,有了哈希环,资源UUID相同的消息将被映射到特定管理节点的相同服务中。
图3-15 一致性哈希环映射消息到服务
当环收缩或者拓展的时候,因为哈希环的固有特性,仅有小部分节点将被影响。
因为使用一致性哈希环,发送者不需要知道哪个服务实例将处理这条消息,因为服务实例将被哈希计算出来。服务也不用维护、交换他们管理的资源信息,并且因为选择正确的服务实例可以由哈希环完成,服务只需要单纯的处理消息。因此,服务变得极其简单且无状态。
除了包含资源UUID的消息(例如StartVm InstanceMsg,DownloadImageMsg)以外,有一种不包含资源UUID的消息,这种消息通常是创造性的消息(例如CreateVolumeMsg)和不进行资源操作的消息(例如AllocateHostMsg),因为这些消息可以被发送到任意管理节点的服务中,他们就被发送到本地的管理节点,因为发送者和接收者在同一个节点上,接收者在发送者发送消息时一定是可用的。
图3-16 APIPortal工作流程图
如图3-16所示,对于API消息(如APIStartVm InstanceMsg),有一个特别的处理方法是他们经常和一个重要的服务ID api.portal绑在一起发送。在消息代理中,一个称为zstack.message.api.portal的全局的队列被所有管理节点的API服务共享,带有api.portal的消息将通过一致性哈希环把消息映射到正确的服务中,从而实现负载均衡。通过上面这种方式,ZStack隐藏了API客户端消息选路的实现,减少了ZStack API客户端代码。
综上所述,分布式系统的设计最佳实践是采用无状态服务,当使用一致性哈希环来映射请求到对应的服务中,这个过程将大大减少系统管理员在节点变化时候的干预,降低出错的可能性。并且,因为管理节点共享的信息非常少,集群能够实现非常灵活横向扩展。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。