本文原发布于 ZStack Blog。 ZSvirt 构建于 ZStack 之上,继承了以下描述的全部架构特性。 理解这些基础设计,有助于更深入地认识 ZSvirt 的性能优势与技术原理。
ZStack 的架构使得其中 99% 的任务能被异步执行。基于这点,ZStack 中单一的管理节点可以管理几十万台物理服务器、上百万台虚拟机,并处理成千上万个并发任务。
动机
对于管理大量硬件和虚拟机的公有云而言,可扩展性是 IaaS 软件必须解决的关键问题之一。对于一个大概拥有 5 万台物理服务器的中型数据中心,预计可能有 150 万台虚拟机、1 万名用户。虽然用户开关虚拟机的频率不会像刷朋友圈一样频繁,但是在某一时刻,IaaS 系统可能有成千上万个任务要处理,这些任务可能来自 API 也可能来自内部组件。在糟糕的情况下,用户为了创建一台新的虚拟机可能需要等待一个小时,因为系统同时被 5000 个任务阻塞,然而线程池仅有 1000 条线程。
问题
首先,我们非常不赞同一些文章里面描写的关于 “一些基础配套设施,尤其是数据库和消息代理(message brokers)限制了 IaaS 的可扩展性” 的观点。首先,对于数据库而言,IaaS 软件的数据量相比 Facebook 和 Twitter 而言只能勉强算中小型。Facebook 和 Twitter 的数据量是万亿级别,IaaS 软件只处于百万级别(对于一些非常大型的数据中心),而 Facebook 和 Twitter 依旧坚强地使用 MySQL 作为它们主要的数据库。其次,对于消息代理而言,ZStack 使用的 RabbitMQ 相对 Apache Kafka 或 ZeroMQ 是一个中型的消息代理,但是它依然可以维持平均每秒 5 万条消息的吞吐量(参见 RabbitMQ Performance Measurements, part 2),对于 IaaS 软件内部通信而言,这不就足够了么?我们认为足够了。
限制 IaaS 可扩展性的主要原因在于:任务执行缓慢。IaaS 软件上的任务运行非常缓慢,通常一项任务完成需要花费几秒甚至几分钟。所以当整个系统被缓慢的任务填满的时候,新任务的延迟非常大是很正常的。执行缓慢的任务通常是由一个很长的任务路径组成的。比如,创建一个虚拟机,需要经过身份验证服务 → 调度器 → 镜像服务 → 存储服务 → 网络服务 → 虚拟机管理程序(Hypervisor),每一个服务可能会花费几秒甚至几分钟去引导外部硬件完成一些操作,这极大地延长了任务执行的时间。
同步 vs 异步
传统的 IaaS 软件使用同步的方式执行任务。它们通常给每一个任务安排一个线程,这个线程只有在之前的任务执行完毕时才会开始执行下一个任务。因为任务执行缓慢,当达到一个任务并发的高峰时,系统会因为线程池容量不足而运行得非常缓慢,新来的任务只能被放在队列中等待被执行。
为了解决这个问题,一个直观的想法是提高线程池容量,但是这个想法在实际中是不可行的——即使现代操作系统允许一个应用程序拥有成千上万条线程,也没有操作系统可以非常有效率地调度它们。随后有一个想法是把线程分发出去,让不同的操作系统上相似的软件分布式地处理任务,因为每一个软件都有它自己的线程池,这样最终增加了整个系统的线程容量。然而,这种横向扩展会带来一定的开销:它增加了管理的复杂度,同时集群软件在软件设计层面依旧是一个挑战。最后,IaaS 软件自身变成了云的瓶颈,而其他的基础设施包括数据库、消息代理和外部的系统(比如成千上万台物理服务器)都足够去处理更多的并发任务。
ZStack 通过异步架构来解决这个问题。如果我们把目光投向 IaaS 软件和数据中心的设备之间的关系,我们会发现 IaaS 软件实际上扮演着一个协调者的角色:它负责协调外部系统,但并不做任何真正耗时的操作。举个例子,存储系统可以分配磁盘容量,镜像系统可以下载镜像模板,虚拟机管理程序可以创建虚拟机。IaaS 软件所做的工作是做决策,然后把子任务分配给不同的外部系统。
比如,对于 KVM,KVM 主机需要执行诸如准备磁盘、准备网络、创建虚拟机等子任务。创建一台虚拟机可能需要花费 5 秒,IaaS 软件花费时间为 0.5 秒,剩下的 4.5 秒被 KVM 主机占用。ZStack 的异步架构使 IaaS 管理软件不用等待那 4.5 秒,它只需要花费 0.5 秒的时间选择让哪一台主机处理这个任务,然后把任务分派给那个主机。一旦主机完成了它的任务,它就会将结果通知给 IaaS 软件。通过异步架构,一个只有 100 条线程容量的线程池就可以处理上千个并发任务。
ZStack 的异步架构
异步操作在计算机科学中是非常常见的操作,异步 I/O、AJAX 等都是一些众所周知的例子。然而,把所有的业务逻辑都建立在异步操作的基础上,尤其是对于 IaaS 这种非常典型的集成软件,是存在很多挑战的。
最大的挑战是必须让所有组件都异步,并不只是一部分组件异步。举个例子,如果你在其他服务都是同步的条件下建立一个异步的存储服务,整个系统性能并不会提升。因为在异步地调用存储服务时,如果调用的服务自身是同步的,那么它必须等待存储服务完成,才能进行下一步操作,这会使得整个工作流依旧处于同步状态。

ZStack 的异步架构包含三部分:异步消息、异步方法、异步 HTTP 调用。
1. 异步消息
ZStack 使用 RabbitMQ 作为消息总线来连接各个服务。当某个服务调用另一个服务时,源服务发送消息给目的服务并注册一个回调函数,然后马上返回;一旦目的服务完成了任务,它就会通过触发回调函数来回复任务结果。例如:
AttachNicToVmOnHypervisorMsg amsg = new AttachNicToVmOnHypervisorMsg();
amsg.setVmUuid(self.getUuid());
amsg.setHostUuid(self.getHostUuid());
amsg.setNics(msg.getNics());
bus.makeTargetServiceIdByResourceUuid(amsg, HostConstant.SERVICE_ID, self.getHostUuid());
bus.send(amsg, new CloudBusCallBack(msg) {
@Override
public void run(MessageReply reply) {
AttachNicToVmReply r = new AttachNicToVmReply();
if (!reply.isSuccess()) {
r.setError(errf.instantiateErrorCode(VmErrors.ATTACH_NETWORK_ERROR, r.getError()));
}
bus.reply(msg, r);
}
});
单个服务也可以发送一串消息给其他服务,并异步地等待回复:
final ImageInventory inv = ImageInventory.valueOf(ivo);
final List<DownloadImageMsg> dmsgs = CollectionUtils.transformToList(msg.getBackupStorageUuids(), new Function<DownloadImageMsg, String>() {
@Override
public DownloadImageMsg call(String arg) {
DownloadImageMsg dmsg = new DownloadImageMsg(inv);
dmsg.setBackupStorageUuid(arg);
bus.makeTargetServiceIdByResourceUuid(dmsg, BackupStorageConstant.SERVICE_ID, arg);
return dmsg;
}
});
bus.send(dmsgs, new CloudBusListCallBack(msg) {
@Override
public void run(List<MessageReply> replies) {
/* do something */
}
}
更进一步,也能发送具有一定并行度的消息串。比如,一串十个的消息能够两两发送,第 3、4 个消息只有在收到第 1、2 个消息的回复之后才会一起发出。
final List<ConnectHostMsg> msgs = new ArrayList<ConnectHostMsg>(hostsToLoad.size());
for (String uuid : hostsToLoad) {
ConnectHostMsg connectMsg = new ConnectHostMsg(uuid);
connectMsg.setNewAdd(false);
connectMsg.setServiceId(serviceId);
connectMsg.setStartPingTaskOnFailure(true);
msgs.add(connectMsg);
}
bus.send(msgs, HostGlobalConfig.HOST_LOAD_PARALLELISM_DEGREE.value(Integer.class), new CloudBusSteppingCallback() {
@Override
public void run(NeedReplyMessage msg, MessageReply reply) {
/* do something */
}
});
2. 异步方法
服务,作为 ZStack 中的“一等公民”,它们之间通过异步消息通信;但是在服务内部,一系列的互相关联的组件、插件是通过异步方法调用来交互的:
protected void startVm(final APIStartVmInstanceMsg msg, final SyncTaskChain taskChain) {
startVm(msg, new Completion(taskChain) {
@Override
public void success() {
VmInstanceInventory inv = VmInstanceInventory.valueOf(self);
APIStartVmInstanceEvent evt = new APIStartVmInstanceEvent(msg.getId());
evt.setInventory(inv);
bus.publish(evt);
taskChain.next();
}
@Override
public void fail(ErrorCode errorCode) {
APIStartVmInstanceEvent evt = new APIStartVmInstanceEvent(msg.getId());
evt.setErrorCode(errf.instantiateErrorCode(VmErrors.START_ERROR, errorCode));
bus.publish(evt);
taskChain.next();
}
});
}
同样,回调也能携带返回值:
public void createApplianceVm(ApplianceVmSpec spec, final ReturnValueCompletion<ApplianceVmInventory> completion) {
CreateApplianceVmJob job = new CreateApplianceVmJob();
job.setSpec(spec);
if (!spec.isSyncCreate()) {
job.run(new ReturnValueCompletion<Object>(completion) {
@Override
public void success(Object returnValue) {
completion.success((ApplianceVmInventory) returnValue);
}
@Override
public void fail(ErrorCode errorCode) {
completion.fail(errorCode);
}
});
} else {
jobf.execute(spec.getName(), OWNER, job, completion, ApplianceVmInventory.class);
}
}
3. 异步 HTTP 调用
ZStack 使用了很多代理(agent)来管理外部系统,例如:管理 KVM 主机的代理、管理 Console Proxy 的代理、管理虚拟路由的代理等等。这些代理都是构建在 Python CherryPy 之上的轻量级 Web 服务器。因为没有类似 HTML5 中的 Web Sockets 技术就无法实现双向通信,ZStack 就为每个请求在 HTTP 的包头放置一个回调 URL。这样,任务结束后,代理就能够把应答发送给调用者的 URL。
RefreshFirewallCmd cmd = new RefreshFirewallCmd();
List<ApplianceVmFirewallRuleTO> tos = new RuleCombiner().merge();
cmd.setRules(tos);
resf.asyncJsonPost(buildUrl(ApplianceVmConstant.REFRESH_FIREWALL_PATH), cmd, new JsonAsyncRESTCallback<RefreshFirewallRsp>(msg, completion) {
@Override
public void fail(ErrorCode err) {
/* handle failures */
}
@Override
public void success(RefreshFirewallRsp ret) {
/* do something */
}
@Override
public Class<RefreshFirewallRsp> getReturnClass() {
return RefreshFirewallRsp.class;
}
});
通过这三个异步方式,ZStack 已经构建了一个分层架构,保证了所有组件都能够实现异步操作:

总结
为了解决由缓慢且并发的任务引起的 IaaS 软件可扩展性受限的问题,我们演示了 ZStack 的异步架构。我们使用模拟器进行测试后发现,一个具有 1000 条线程的 ZStack 管理节点可以轻松处理创建 100 万台虚拟机时产生的 10000 个并发任务。虽然单一管理节点的扩展性已经可以满足大多数云的负载需要,但考虑到系统需要高可用性以及承受巨大的负载量(例如 10 万个并发任务),我们仍需要一组管理节点来满足这些需求。如需了解 ZStack 的无状态服务,请阅读下一篇 ZStack 的伸缩性秘密(二):无状态服务。