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万条消息的吞吐量,对于IaaS软件内部通信而言这不就足够了么?我们认为足够了。

限制IaaS可拓展性的主要原因在于:任务执行缓慢。IaaS软件上的任务运行非常缓慢,通常一项任务完成需要花费几秒甚至几分钟。所以当整个系统被缓慢的任务填满的时候,新任务的延迟非常大是很正常的。执行缓慢的任务通常是由一个很长的任务路径组成的,比如,创建一个虚拟机,需要经过身份验证服务调度器镜像服务存储服务网络服务虚拟机管理程序,每一个服务可能会花费几秒甚至几分钟去引导外部硬件完成一些操作,这极大的延长了任务执行的时间。

其实,IaaS 伸缩性问题的根源在于:任务处理慢。确实是,在 IaaS 软件系统中任务处理非常慢,慢到要有几秒甚至是几分钟才能完成。因此,当系统中全是这种慢慢处理的任务时候,当然就带来了新任务的巨大的延迟。而这种慢处理的任务源于任务路径过长。举例说明,创建虚拟机,一般要经过以下路径 身份服务(service)—>规划器(scheduler)-> 图象服务(service)->存储服务->网络服务->系统管理(Hypervisor); 每个服务都会花费几秒甚至几分钟来操作外部硬件,这就导致了超长的任务处理时长。

同步 vs 异步

传统的IaaS软件使用同步的方式执行任务,他们通常给每一个任务安排一个线程,这个线程只有在之前的任务执行完毕时才会开始执行下一个任务。因为任务执行缓慢,当达到一个任务并发的高峰时,系统会因为线程池容量不足,运行非常缓慢,新来的任务只能被放在队列中等待被执行。

为了解决这个问题,一个直观的想法是提高线程池容量,但是这个想法在实际中是不可行的,即使现代操作系统允许一个应用程序拥有成千上万条线程,没有操作系统可以非常有效率的调度他们。随后有一个想法是把线程分发出去,让不同的操作系统上相似的软件分布式的处理线程,因为每一个软件都有它自己的线程池,这样最终增加了整个系统的线程容量。然而,分发会带来一定的开销,它增加了管理的复杂度,同时集群软件在软件设计层面依旧是一个挑战。最后,IaaS软件自身变成云的瓶颈,而其他的基础设施包括数据库,消息代理和外部的系统(比如成千台物理服务器)都足够去处理更多的并发任务。

ZStack通过异步架构来解决这个问题,如果我们把目光投向IaaS软件和数据中心的设备之间的关系,我们会发现IaaS软件实际上扮演着一个协调者的角色,它负责协调外部系统但并不做任何真正耗时的操作。举个例子,存储系统可以分配磁盘容量,镜像系统可以下载镜像模板,虚拟机管理程序可以创建虚拟机。IaaS软件所做的工作是做决策然后把子任务分配给不同的外部系统。比如,对于KVM,KVM主机需要执行诸如准备磁盘、准备网络、创建虚拟机等子任务。创建一台虚拟机可能需要花费5s,IaaS软件花费时间为0.5s,剩下的4.5s被KVM主机占用,ZStack的异步架构使IaaS管理软件不用等待4.5s,它只需要花费0.5s的时间选择让哪一台主机处理这个任务,然后把任务分派给那个主机。一旦主机完成了它的任务,它将结果通知给IaaS软件。通过异步架构,一个只有100条线程容量的线程池可以处理上千数的并发任务。

ZStack 的异步方法

异步操作在计算机科学中是非常常见的操作,异步I/O,AJAX等都是一些众所周知的例子。然而,把所有的业务逻辑都建立在异步操作的基础上,尤其是对于IaaS这种非常典型的集成软件,是存在很多挑战的。

最大的挑战是必须让所有组件都异步,并不只是一部分组件异步。举个例子,如果你在其他服务都是同步的条件下,建立一个异步的存储服务,整个系统性能并不会提升。因为在异步的调用存储服务时,调用的服务自身如果是同步的,那么调用的服务必须等待存储服务完成,才能进行下一步操作,这会使得整个工作流依旧是处于同步状态。

【ZStack】1.ZStack的伸缩性秘密武器:异步架构 - 图1

图:线程中,业务流程服务要调用存储服务,直到存储服务返回了,线程才能结束。 虽然,存储服务通过异步方式和外部存储系统交互。

ZStack’s 异步架构包含三部分:

  • 异步消息
  • 异步方法
  • 异步 HTTP 调用

1. 异步消息

ZStack 使用 RabbitMQ 作为消息总线以便连接各个服务。当某个服务调用另一个服务时,源服务发消息给目的服务并注册一个回调函数,然后马上返回;一旦目的服务完成了任务,它就会通过触发回调函数来回复任务结果。

  1. AttachNicToVmOnHypervisorMsg amsg = new AttachNicToVmOnHypervisorMsg();
  2. amsg.setVmUuid(self.getUuid());
  3. amsg.setHostUuid(self.getHostUuid());
  4. amsg.setNics(msg.getNics());
  5. bus.makeTargetServiceIdByResourceUuid(amsg, HostConstant.SERVICE_ID, self.getHostUuid());
  6. bus.send(amsg, new CloudBusCallBack(msg) {
  7. @Override
  8. public void run(MessageReply reply) {
  9. AttachNicToVmReply r = new AttachNicToVmReply();
  10. if (!reply.isSuccess()) {
  11. r.setError(errf.instantiateErrorCode(VmErrors.ATTACH_NETWORK_ERROR, r.getError()));
  12. }
  13. bus.reply(msg, r);
  14. }
  15. });

单个服务也可以发送一串消息给其他服务 ,并异步的等待回复。

  1. final ImageInventory inv = ImageInventory.valueOf(ivo);
  2. final List<DownloadImageMsg> dmsgs = CollectionUtils.transformToList(msg.getBackupStorageUuids(), new Function<DownloadImageMsg, String>() {
  3. @Override
  4. public DownloadImageMsg call(String arg) {
  5. DownloadImageMsg dmsg = new DownloadImageMsg(inv);
  6. dmsg.setBackupStorageUuid(arg);
  7. bus.makeTargetServiceIdByResourceUuid(dmsg, BackupStorageConstant.SERVICE_ID, arg);
  8. return dmsg;
  9. }
  10. });
  11. bus.send(dmsgs, new CloudBusListCallBack(msg) {
  12. @Override
  13. public void run(List<MessageReply> replies) {
  14. /* do something */
  15. }
  16. }

更进一步,也能发送具有一定并行性的消息串。 比如,一串十个的消息,能够两两发送,第三,第四个消息只有第一,第二个消息收到后在一起发出。

  1. final List<ConnectHostMsg> msgs = new ArrayList<ConnectHostMsg>(hostsToLoad.size());
  2. for (String uuid : hostsToLoad) {
  3. ConnectHostMsg connectMsg = new ConnectHostMsg(uuid);
  4. connectMsg.setNewAdd(false);
  5. connectMsg.setServiceId(serviceId);
  6. connectMsg.setStartPingTaskOnFailure(true);
  7. msgs.add(connectMsg);
  8. }
  9. bus.send(msgs, HostGlobalConfig.HOST_LOAD_PARALLELISM_DEGREE.value(Integer.class), new CloudBusSteppingCallback() {
  10. @Override
  11. public void run(NeedReplyMessage msg, MessageReply reply) {
  12. /* do something */
  13. }
  14. });

2. 异步方法

ZStack 服务,就像以上段一所示,它们之间通过异步消息通信; 对于服务内部,一系列的互相关联的组件,插件是通过异步方法调用来交互的。

  1. protected void startVm(final APIStartVmInstanceMsg msg, final SyncTaskChain taskChain) {
  2. startVm(msg, new Completion(taskChain) {
  3. @Override
  4. public void success() {
  5. VmInstanceInventory inv = VmInstanceInventory.valueOf(self);
  6. APIStartVmInstanceEvent evt = new APIStartVmInstanceEvent(msg.getId());
  7. evt.setInventory(inv);
  8. bus.publish(evt);
  9. taskChain.next();
  10. }
  11. @Override
  12. public void fail(ErrorCode errorCode) {
  13. APIStartVmInstanceEvent evt = new APIStartVmInstanceEvent(msg.getId());
  14. evt.setErrorCode(errf.instantiateErrorCode(VmErrors.START_ERROR, errorCode));
  15. bus.publish(evt);
  16. taskChain.next();
  17. }
  18. });
  19. }

同样, 回调也能包含返回值:

  1. public void createApplianceVm(ApplianceVmSpec spec, final ReturnValueCompletion<ApplianceVmInventory> completion) {
  2. CreateApplianceVmJob job = new CreateApplianceVmJob();
  3. job.setSpec(spec);
  4. if (!spec.isSyncCreate()) {
  5. job.run(new ReturnValueCompletion<Object>(completion) {
  6. @Override
  7. public void success(Object returnValue) {
  8. completion.success((ApplianceVmInventory) returnValue);
  9. }
  10. @Override
  11. public void fail(ErrorCode errorCode) {
  12. completion.fail(errorCode);
  13. }
  14. });
  15. } else {
  16. jobf.execute(spec.getName(), OWNER, job, completion, ApplianceVmInventory.class);
  17. }
  18. }

3. 异步HTTP调用

ZStack 使用了很多代理来管理外部系统。 例如: 管理 KVM 主机的代理,管理 Console Proxy 的代理,管理虚拟路由的代理等等。这些代理都是构建在 Python CherryPy 上的轻量级的 Web 服务器。因为,没有类似 HTML5 中的 Web Sockets 技术就不能实现双向通信,ZStack 就为每个请求,放置了一个回调 URL 在 HTTP 的包头 。这样,任务结束后,代理就能够发送应答给调用者的 URL。

  1. RefreshFirewallCmd cmd = new RefreshFirewallCmd();
  2. List<ApplianceVmFirewallRuleTO> tos = new RuleCombiner().merge();
  3. cmd.setRules(tos);
  4. resf.asyncJsonPost(buildUrl(ApplianceVmConstant.REFRESH_FIREWALL_PATH), cmd, new JsonAsyncRESTCallback<RefreshFirewallRsp>(msg, completion) {
  5. @Override
  6. public void fail(ErrorCode err) {
  7. /* handle failures */
  8. }
  9. @Override
  10. public void success(RefreshFirewallRsp ret) {
  11. /* do something */
  12. }
  13. @Override
  14. public Class<RefreshFirewallRsp> getReturnClass() {
  15. return RefreshFirewallRsp.class;
  16. }
  17. });

通过这三个异步方式,ZStack 已经构建了一个分层架构,保证所有组件能够实现异步操作。

【ZStack】1.ZStack的伸缩性秘密武器:异步架构 - 图2

总结

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