每个ZStack服务都是无状态的,让服务高可用以及横向拓展(scale out)可以很简单,只需要启动剩余的服务实例,然后进行负载均衡即可。此外,ZStack将所有的服务打包到名为管理节点(management node)的单个进程,它让部署和管理变得超级简单。
动机
在ZStack的伸缩性秘密武器——一、异步架构(ZStack's Scalability Secrets Part 1: Asynchronous Architecture)一文中, 我们已经详细解释了异步架构,它让单个ZStack管理节点能胜任大多数的云端工作负载。然而,当用户希望建立高可用的生产环境,或者处理超级大的并发工作负载的时候,一个管理节点是不够的。解决方案是,构建一个分布式的系统,这样工作负载可以延展到每一个单一管理节点。这种增加新节点来拓展整个系统的容量的方式称为 横向拓展(scale out).
问题
设计一个分布式的系统并不容易。一个分布式的系统,特别是一个有状态的系统,必须处理一致性,可用性,以及分区容忍性(请查看 CAP理论(CAP theorem)),所有这些都很复杂。相反,一个无状态的分布式系统,在某种程度上摆脱了这种复杂性。首先,因为在节点之间无需状态共享,系统自然保持了一致性;其次,由于节点之间是类似的,当系统遇到一个分区问题通常也是OK的。鉴于此,一个分布式的系统,通常更倾向于保持无状态而不是有状态。但是,设计一个无状态的分布式系统也是很困难的,同时,常常比设计有状态的分布式系统更加困难。提升了消息总线(message bus)和数据库优势的ZStack,构建了一个包含了无状态服务的无状态分布式系统。
由于无状态服务是保证整个系统无状态的根基,在讨论它是什么之前,让我们先了解下什么是“状态”。在ZStack里面,资源,如主机,虚拟机,镜像,以及用户,都是由单个服务管理的;当系统中存在多余一个服务实例的时候,资源会被划分为不同的实例。例如,假如有10,000个虚拟机和两个虚拟机服务实例,理想的情况下,每个实例将会管理5000个虚拟机:
由于存在两个服务实例,在向虚拟机发送请求之前,请求者必须知道哪一个实例正在管理虚拟机;否则,它将无法知道将请求发往何处。像 ”哪个服务实例正在管理什么资源“ 的认知,正是我们正在谈论的状态。如果服务是有状态的,状态也就显现在服务之中。请求者需要在某个地方咨询这些状态。当服务实例的数目发生变化的时候,服务需要交换状态,例如,当一个新的服务实例加入,或者当前的服务实例脱离的时候。
状态交换是让人担忧的,它很容易导致错误,常常会限制系统的可拓展性。为了让系统更可靠,同时更易于横向拓展,理想的方式是,通过彼此分隔状态来让服务保持无状态(查看 服务无状态原则(Service Statelessness Principle)。 有了无状态的服务,请求者不再需要询问何处发送请求;当新的服务实例加入,或者旧的服务实例脱离的时候,服务也不再需要交换状态。
注意:在接下来的内容中,为了简单起见,术语“服务”和“服务实例”交换着使用。
服务和管理节点
服务,通过中央消息总线(central message bus)--RabbitMQ,来彼此通讯,它们是ZStack中的“第一等公民”。
不像通常的微服务架构,其每个服务都在单独的进程或单独的机器上运行,ZStack将所有的服务打包到一个名为管理节点的单一进程。对于这个号称 进程中的微服务(in-process microservices)架构,我们有充分的理由,你可以参看进程中的微服务架构(The In-Process Microservices Architecture)。
一个管理节点是一个完整功能的ZStack软件。由于包含了无状态服务,管理节点没有共享状态,但是有心跳记录,以及一致性哈希算法环(consistent hashing ring)--接下来我们将详细介绍。 心跳用来监控管理节点的“健康”(译者注:即此管理节点是否存活,是否正常运转),只要一个管理节点在给定的间隔内停止更新心跳,其它的管理节点将会驱除它,同时开始接管它所管理的资源。
无状态服务
实现无状态服务的核心技术,特别是对于ZStack的业务逻辑,就是一致性哈希算法(consistent hashing algorithm)。在启动的时候,每个管理节点都会被分配一个 版本4UUID(version 4 UUID)(管理节点UUID),它会和服务名一起,在消息总线上注册一个服务队列。例如,管理节点可能注册如下所示的服务队列:
zstack.message.ansible.3694776ab31a45709259254a018913ca
zstack.message.api.portal
zstack.message.applianceVm.3694776ab31a45709259254a018913ca
zstack.message.cloudbus.3694776ab31a45709259254a018913ca
zstack.message.cluster.3694776ab31a45709259254a018913ca
zstack.message.configuration.3694776ab31a45709259254a018913ca
zstack.message.console.3694776ab31a45709259254a018913ca
zstack.message.eip.3694776ab31a45709259254a018913ca
zstack.message.globalConfig.3694776ab31a45709259254a018913ca
zstack.message.host.3694776ab31a45709259254a018913ca
zstack.message.host.allocator.3694776ab31a45709259254a018913ca
zstack.message.identity.3694776ab31a45709259254a018913ca
zstack.message.image.3694776ab31a45709259254a018913ca
zstack.message.managementNode.3694776ab31a45709259254a018913ca
zstack.message.network.l2.3694776ab31a45709259254a018913ca
zstack.message.network.l2.vlan.3694776ab31a45709259254a018913ca
zstack.message.network.l3.3694776ab31a45709259254a018913ca
zstack.message.network.service.3694776ab31a45709259254a018913ca
zstack.message.portForwarding.3694776ab31a45709259254a018913ca
zstack.message.query.3694776ab31a45709259254a018913ca
zstack.message.securityGroup.3694776ab31a45709259254a018913ca
zstack.message.snapshot.volume.3694776ab31a45709259254a018913ca
zstack.message.storage.backup.3694776ab31a45709259254a018913ca
说明:你应该注意到,所有队列都以同样的UUID结尾,那是管理节点的UUID。
资源,如主机,容量,虚拟机,也是通过UUID来标识的。消息,常常和资源相关联,是在服务间传递的。在发送消息之前,发送者必须选择基于资源的UUID的接收者服务,这时,一致性哈希算法就开始登场了。
一致性哈希(Consistent hashing)是一种特别的哈希,当哈希表调整大小的时候,就会用到一致性哈希,其中只有一部分键(key)需要重新映射。关于一致性哈希的更多内容,更详细的请参阅 这里。在ZStack之中,管理节点由一致性哈希环组成,如下所示:
每个管理节点都维护一份一致性哈希环的拷贝,这个环包含了系统中所有管理节点的UUID。当管理节点加入或者脱离的时候,生命周期事件(lifecycle event)就会通过消息总线广播到其它节点,这样使得这些节点扩展或者收缩环,以呈现当前系统的状态。当发送消息的时候,发送者服务将使用资源的UUID,通过哈希的方式得出目标管理节点的UUID。例如,发送VM的UUID为932763162d054c04adaab6ab498c9139的StartVmInstanceMsg,伪代码如下:
msg = new StartVmInstanceMsg(); destinationManagementNodeUUID = consistent_hashing_algorithm("932763162d054c04adaab6ab498c9139"); msg.setServiceId("vmInstance." + destinationManagementNodeUUID); cloudBus.send(msg)
如果有一个稳定的环,那么包含同样资源UUID的消息就总是会路由到某个管理节点上同样的服务,这就是ZStack无锁架构的基础(参阅 ZStack的伸缩性秘密(第三部分):无锁架构(Stack's Scalability Secrets Part 3: Lock-free Architecture)。
当一致性哈希环收缩或释放的时候,由于一致性哈希的特性,只有少数节点受到轻微影响。
由于一致性哈希环,发送者无需知道哪一个服务实例即将处理消息;取而代之的是,这将会被处理掉。服务无需维护和交换,关于它们正在管理什么资源的信息;它们所需要做的就是,处理即将到来的消息,因为环能够保证消息找到正确的服务实例。这就是服务如何变得超级简单和保持无状态的。
除包含资源UUID的消息之外(如 StartVmInstanceMsg, DownloadImageMsg),也有一类无资源UUID的消息,通常是创建型的消息(如 CreateVolumeMsg)和非资源消息(如 AllocateHostMsg)--它们不会操控单独的资源。考虑到这些消息可以发送到任意管理节点的服务,它们可能被故意发送到本地的管理节点,由于发送者和接收者在同样的节点,当发送者发送消息的时候,接收者当然也是可达的。
对 API 消息(例如:APIStartVmInstanceMsg)来说,有一个特殊的处理,它们总是发送一个众所周知的服务 ID api.portal 。在消息总线上,一个全局的队列被叫做 zstack.message.api.portal ,它被所有的管理节点 API 服务所共享,消息服务 ID api.portal 将会自动对其中的一个API服务做负载均衡,这个服务还会路由转发消息到正确的目的地,并使用了一致性哈希环(consistent hashing ring)。通过这种做法,ZStack 隐藏了来自 API 客户端消息路由转发的细节,并简化了写一个ZStack API 客户端的工作。
msg = new APICreateVmInstanceMsg()
msg.setServiceId("api.portal")
cloudBus.send(msg)
摘要
在这篇文章中,我们证明了Zstack 构建伸缩性的分布式系统。因为管理节点共享的信息比较少,很容易建立一个大的集群,可能有几十个甚至几百个管理节点。然而实际上,在私有云方面,两个管理节点可以有很好的扩展性;在公共云方面,管理员能根据工作量创建一个管理节点。依靠异步架构和无状态的服务,Zstack能够处理大量的并发任务,现有的IaaS软件则不能处理。