本文原发布于 ZStack Blog。 ZSvirt 构建于 ZStack 之上,继承了以下描述的全部架构特性。 理解这些基础设计,有助于更深入地认识 ZSvirt 的性能优势与技术原理。

每一个 ZStack 服务都是无状态的,让服务高可用以及横向扩展(scale out)可以很简单,只需要启动冗余的服务实例,然后进行负载均衡即可。此外,ZStack 将所有的服务打包到名为管理节点(management node)的单个进程,它让部署和管理变得极其简单。

动机

ZStack 的伸缩性秘密(一):异步架构 一文中,我们已经详细解释了异步架构,它让单个 ZStack 管理节点能胜任大多数云的工作负载。然而,当用户希望建立高可用的生产环境,或者处理超大规模的并发工作负载时,一个管理节点是不够的。解决方案是构建一个分布式系统,这样工作负载可以延展到每一个单一管理节点。这种通过增加新节点来拓展整个系统容量的方式,称为横向扩展(scale out)。

问题

设计一个分布式系统并不容易。一个分布式系统,特别是一个有状态的系统,必须处理一致性、可用性以及分区容忍性(请查看 CAP 理论),所有这些都很复杂。相反,一个无状态的分布式系统在某种程度上摆脱了这种复杂性。首先,因为在节点之间无需状态共享,系统自然保持了一致性;其次,由于节点之间是类似的,当系统遇到一个分区问题时通常也是可以容忍的。鉴于此,一个分布式系统通常更倾向于保持无状态而不是有状态。但是,设计一个无状态的分布式系统也是很困难的,而且常常比设计有状态的分布式系统更加困难。借助消息总线(message bus)和数据库优势的 ZStack,构建了一个包含了无状态服务的无状态分布式系统。

由于无状态服务是保证整个系统无状态的根基,在讨论它是什么之前,让我们先了解一下什么是“状态”。在 ZStack 里面,资源(如主机、虚拟机、镜像以及用户)都是由单个服务管理的;当系统中存在多于一个服务实例的时候,资源会被划分到不同的实例。例如,假如有 10,000 个虚拟机和两个虚拟机服务实例,理想的情况下,每个实例将会管理 5000 个虚拟机:

资源在服务实例间拆分

由于存在两个服务实例,在向虚拟机发送请求之前,请求者必须知道哪一个实例正在管理这台虚拟机;否则,它将无法知道将请求发往何处。像“哪个服务实例正在管理什么资源”这样的认知,正是我们正在谈论的状态。如果服务是有状态的,状态也就存在于服务之中,请求者需要在某个地方查询这些状态。当服务实例的数目发生变化的时候(例如,当一个新的服务实例加入,或者当前的服务实例脱离的时候),服务之间需要交换状态。

有状态服务的状态交换

状态交换是让人担忧的,它很容易导致错误,常常会限制系统的可扩展性。为了让系统更可靠,同时更易于横向扩展,理想的方式是通过彼此分离状态来让服务保持无状态(查看 服务无状态原则)。有了无状态的服务,请求者不再需要询问向何处发送请求;当新的服务实例加入或者旧的服务实例脱离的时候,服务也不再需要交换状态。

注意:在接下来的内容中,为了简单起见,术语“服务”和“服务实例”会交替使用。

服务和管理节点

服务通过中央消息总线(central message bus)——RabbitMQ——来彼此通讯,它们是 ZStack 中的“一等公民”。

服务通过消息总线通信

不像通常的微服务架构那样让每个服务都在单独的进程或单独的机器上运行,ZStack 将所有的服务打包到一个名为管理节点的单一进程。对于这个所谓的进程内微服务(in-process microservices)架构,我们有充分的理由,你可以参阅 进程内微服务架构

一个管理节点是一个功能完整的 ZStack 软件。由于包含了无状态服务,管理节点不共享任何状态,但是有心跳记录,以及一致性哈希环(consistent hashing ring)——接下来我们将详细介绍。心跳用来监控管理节点的健康状态,只要一个管理节点在给定的间隔内停止更新心跳,其它的管理节点就会驱逐它,同时开始接管它所管理的资源。

管理节点心跳

无状态服务

实现无状态服务的核心技术,特别是对于 ZStack 的业务逻辑,就是一致性哈希算法(consistent hashing algorithm)。在启动的时候,每个管理节点都会被分配一个版本 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 的伸缩性秘密(三):无锁架构)。

稳定的环消息路由

当一致性哈希环收缩或扩展的时候,由于一致性哈希的特性,只有少数节点受到轻微影响。

由于一致性哈希环的存在,发送者无需知道哪一个服务实例即将处理消息,取而代之的是,这个消息将会被哈希出来。服务无需维护和交换“它们正在管理什么资源”的信息;它们所需要做的只是处理即将到来的消息,因为环能够保证消息找到正确的服务实例。这就是服务如何变得极其简单和无状态的。

除了包含资源 UUID 的消息之外(如 StartVmInstanceMsg、DownloadImageMsg),也有一类没有资源 UUID 的消息,通常是创建型的消息(如 CreateVolumeMsg)和非资源消息(如 AllocateHostMsg),它们不会操控单独的资源。考虑到这些消息可以发送到任意管理节点上的服务,它们会被故意发送到本地的管理节点——由于发送者和接收者在同一个节点上,当发送者发送消息的时候,接收者当然是可达的。

对于 API 消息(例如 APIStartVmInstanceMsg)来说,有一个特殊的处理:它们总是发送到众所周知的服务 ID api.portal。在消息总线上,一个名为 zstack.message.api.portal 的全局队列被所有管理节点的 API 服务所共享,带有服务 ID api.portal 的消息将会自动负载均衡到其中一个 API 服务,然后这个服务会使用一致性哈希环将消息路由转发到正确的目的地。通过这种做法,ZStack 对 API 客户端隐藏了消息路由的细节,并简化了编写 ZStack API 客户端的工作。

msg = new APICreateVmInstanceMsg()
msg.setServiceId("api.portal")
cloudBus.send(msg)

API 消息路由

总结

在这篇文章中,我们展示了 ZStack 如何通过构建无状态的分布式系统来实现横向扩展。因为管理节点共享的信息非常少,很容易建立一个包含几十个甚至几百个管理节点的集群。然而实际上,对于私有云来说,两个管理节点就足以满足高可用和扩展性的需求;对于公有云来说,管理员可以根据工作负载创建一个管理节点集群。依靠异步架构和无状态的服务,ZStack 能够处理现有 IaaS 软件难以处理的超大规模并发任务。


ZStack 核心架构系列