ZStack的伸缩性秘密武器(三):无锁架构

在IaaS(Infrastructure as a Service,即基础设施即服务)软件里许多任务要顺序的执行;例如,当一个起动虚拟机的任务正在运行时,一个结束些虚拟机的任务则必有等待以前的开始任务结束才行。另外一方面,一些任务以须要并发的同时运行;例如,在同一主机上20个建立虚拟机的任务能同时运行。同步和并行在一个分布式系统中是很差控的而且经常须要一个同步软件。针对这个挑战,ZStack提供了一个基于队列的无锁架构,容许任务很容易的来控制它们的并行级别,从一个同步到N个并行都行。java

动机

一个好的IasS软件在任务的同步及并行上须要有精细的控制。大多数状况下,任务之间有依赖关系须要以某一顺序来执行;例如,一个删除卷的任务不能被执行,若是另外一个在此卷上作快照备份的任务正在执行中。有时,任务要并发执行来提高性能;例如,在同一台主机上十个建立虚拟机的任务同时执行一点问题也没有。固然,没有正常的控制,并行任务也会损坏系统;例如,1000个同时执行的建立虚拟机的任务虽不会使系统挂掉但至少致使系统有段时间没有响应。这种同步开发问题在多线程环境是很复杂的,在分布式环境就显得更加复杂了。算法

问题

教科书告诉咱们,锁和信号量是同步和并行的答案;在分布式系统中,处理同步和并行的最直接的想法是,使用某种分布式的协调软件,像 Apache ZooKeeper ,或者在 Redis之上构建的相似软件。 分布式协调软件的使用概况,例如, ZooKeeper,像下面这样:数据库

问题是,对于锁或信号量, 线程须要等待其它线程释放它们正在使用的锁或信号量。在ZStack 的伸缩性秘密(第一部分)异步架构(ZStack's Scalability Secrets Part 1: Asynchronous Architectue) 一文中,咱们解释了,ZStack是一种异步软件,线程不会因等待其它线程的完成而阻塞;所以,锁和信号量不是可行的选项。同时,咱们也关心使用分布式协调软件的复杂性和拓展性,想象一下,一个满载100,000个须要锁的任务的系统,这既不容易,也不易拓展。编程

同步的vs. 同步化的:在 ZStack 的伸缩性秘密(第一部分)异步架构(ZStack's Scalability Secrets Part 1: Asynchronous Architecture)一文中, 咱们讨论了 同步的 vs. 异步的,在本文中,咱们将会讨论 同步的 vs. 并行的。“同步的”和“同步化的”有时候是可互换的使用,可是它们是不一样的。在咱们的场景中,“同步的”是在讨论,关于执行一个任务是否会阻塞线程的问题;“同步化的”是在讨论,关于一个任务是否排它的执行的问题。若是一个任务在完成前,一直占据一个线程的全部时间,这就是一个同步的任务;若是一个任务不能和其它任务在同一时间执行,这就是一个同步化的任务。api

无锁架构的基础

使用一致性哈希算法,来保证同一个服务实例可以处理全部到达同一资源的消息,这就是无锁架构的基础。经过这种方法汇集到达某一节点的消息,能够减小从分布式系统到多线程环境的同步,并行化的复杂性(更多细节见ZStack的伸缩性秘密(第二部分):无状态服务)。网络

工做队列:传统解决方案

注意:在深刻了解细节以前,请注意,咱们即将要谈论的队列,和在 ZStack 的伸缩性秘密(第二部分)无状态服务(ZStack's Scalability Secrets Part 2: Stateless Services)一文中提到的RabbitMQ消息队列,没有任何关联。消息队列是RabbitMQ的术语;ZStack的队列则是内部数据结构。数据结构

在ZStack中的任务是由消息驱动的,聚合消息让相关的任务能够在一样的节点执行,减轻了经典的线程池并发编程的压力。为了不锁竞争,ZStack使用工做队列替代锁和信号量。同步化的任务能够一个接一个的执行,它们由基于内存的工做队列维护:多线程

注意:工做队列能够同时执行同步化的和并行的任务。若是并行级别为1,那么队列就是同步化的;若是并行级别大于1,那么队列是并行的;若是并行级别为0,那么队列就是无限并行的。架构

基于内存的同步队列

在Zstack中有两种工做队列;一种是同步队列,任务返回结果才认定为结束(一般使用Java Runnable接口来实现):并发

thdf.syncSubmit(new SyncTask<Object>() {    

 @Override     

public String getSyncSignature() {       

      return "api.worker";    

 }   


  @Override     

public int getSyncLevel() {       

      return apiWorkerNum;   

  }    


 @Override    

 public String getName() {      

       return "api.worker";    

 }    



@Override     

public Object call() throws Exception {        

 if (msg.getClass() == APIIsReadyToGoMsg.class) {           

      handle((APIIsReadyToGoMsg) msg);        

 } else {        

     try {                

             dispatchMessage((APIMessage) msg);      

       } catch (Throwable t) {               

              bus.logExceptionWithMessageDump(msg, t);           

              bus.replyErrorByMessageType(msg, errf.throwableToInternalError(t));    

         }         }       

  /* When method call() returns, the next task will be proceeded immediately */                   return null;   

  } });

强调: 在同步队列中,工做线程继续读取下个Runnable,只要前一个Runnable.run()方法返回结果,而且直接队列为空了才返回线程池。由于任务在执行时会取得工做线程,队列是同步的.

基于内存的异步队列

另外一种是异常工做队列,当它发出一个完成通知才认为结束:

thdf.chainSubmit(new ChainTask(msg) {    

 @Override     

public String getName() {        

     return String.format("start-vm-%s", self.getUuid());    

 }    


 @Override    

 public String getSyncSignature() {        

     return syncThreadName;     

}     


@Override     

public void run(SyncTaskChain chain) {         

    startVm(msg, chain);                

  /* the next task will be proceeded only after startVm() method calls chain.next() */    

 } });

强调: 在异步队列中,ChainTask.run(SyncTaskChain chain) 方法可能在作一些异步 操做后当即返回;例如,发送消息和一个注册的回调函数.在run()方法返回值后,工做线程回到线程池中;可是,以前的任务可能还没完成,没有任务可以被处理,直到以前的任务发出一个通知(如调用SyncTaskChain.next())。由于任务不会阻塞工做线程等待其完成,队列是异步的。

基于数据库的异步队列

基于内存的工做队列简单快速,它知足了在单一管理节点99%的同步和并行的须要; 然而,与建立资源相关的任务,可能须要在不一样管理节点之间作同步。一致性哈希环基于资源UUID来工做,若是资源未被建立,它将没法得知哪一个节点应该处理这个建立的工做。在大多数状况下,若是要建立的资源不依赖于其它未完成的任务,ZStack会选择,此建立任务的提交者所在的本地节点,来完成这个工做。不幸的是,这些不间断的任务依赖于名为虚拟路由VM的特殊资源; 例如,若是使用一样的L3网络的多个用户VM,由运行于不一样管理节点的任务建立而成,同时在L3网络上并没有虚拟路由VM,那么建立虚拟路由VM的任务则可能由多个管理节点提交。在这种状况下,因为存在分布式同步的问题,ZStack使用基于数据库的做业队列,这样来自不一样管理节点的任务就能够实现全局同步。

数据库做业队列只有异步的形式;也就是说,只有前一个任务发出一个完成通知后,下一个任务才能执行。

注意: 因为任务存储在数据库之中,因此数据库做业队列的速度比较慢;幸运的是,只有建立虚拟路由VM的任务须要它。

限制

虽然基于无锁架构的队列能够处理99.99%的时间同步,可是有一个争用条件从一致的散列算法中产生:一个新加入的节点将分担一部分相邻节点的工做量,这就是一致的散列环的扩张的结果。

在这个例子中,在三个节点加入后,之前的目标定位从节点2转到了节点3;在此期间,若是对于资源的一个旧任务依旧工做在节点2上,可是对于相同资源的任务提交到节点3,这就会形成争用状态。然而,这种情况并非你想像中的那么坏。首先,冲突任务不多地存在规则的系统中,好比,一个健全的 UI 不容许你阻止一个正在运行的 VM。而后,每个 ZStack 资源都有状态,一个开始就处于问题状态的任务会出现错误;好比,若是一个 VM 是中止状态,一个附加任务量的任务就会马上出错。第三,代理--大多数任务的传送地,有额外的附加机制;好比,虚拟路由器代理会同步全部的修改 DHCP 配置文件的请求,即便咱们已经有了虚拟路由器在管理节点端的工做队列。最后,提早规划你的操做是持续管理云的关键;操做团队能够在推出云以前快速产生足够的管理节点;若是他们真的须要动态添加一个新的节点,这样作的时候,工做量仍是比较小的。

总结

在这篇文章里,展现了创建在基于内存工做队列和基于数据库的无锁结构。没有涉及复杂的分布式协做软件,ZStack 尽量地在争用条件下的屏蔽任务中配合提高性能。

相关文章
相关标签/搜索