ZStack--可拓展性秘密武器3:无锁架构

很多IaaS软件中的任务需要按照顺序执行,例如,当一个启动虚拟机的任务正在运行时,关闭同一台虚拟机的任务必须等候前面开启虚拟机的任务完成。另一方面,一些任务应该支持并行完成;例如,20个在同一台主机上创建虚拟机的任务可以同时运行。在一个分布式系统中同时实现串行化和细粒度的并行化并不容易,这通常需要借助分布式调度软件。面对挑战,ZStack提出基于队列的无锁架构,使得任务本身可以简单的控制他们的并行化等级为1(串行的)…N(并行的)。

动机

好的IaaS软件应该能对任务的串行化和并行化进行细粒度的控制。通常,因为任务之间有依赖关系,任务希望以特定的序列被执行。比如,如果当一个对某个磁盘进行快照的任务正在运行时,那么删除该磁盘的操作不能被执行。有些时候,为了提升性能,任务应该被并行地执行;比如,在同一台主机上有十个创建虚拟机的任务,这些任务可以同时运行并不会产生任何问题。然而,如果不进行合理的控制,并行化会对系统造成一定的伤害;比如,1000个在一台主机上创建虚拟机的并发任务,将毫无疑问的摧毁整个系统,或者导致整个系统长时间没有任何回应。这种并发编程问题在多线程的环境中是复杂的,并且在分布式系统环境中将更加复杂。

问题

教科书教导我们解决同步和并行问题的答案锁(lock)和信号量(semaphore)。在分布式系统中为了解决此问题,一个直接的想法是使用一些类似Apache ZooKeeper的分布式调度软件或一些基于Redis的类似软件。拿ZooKeeper来举例子,使用分布式调度软件的概览如下:

在这里插入图片描述

问题在于,当使用锁和信号量的时候,一个线程需要去等待其他线程释放他们所持有的锁或者信号量。在“ZStack可拓展性秘密武器1:异步架构”中我们阐述了ZStack是一个异步的软件,没有线程将因为等待其他线程的完成而被阻塞,所以使用锁和信号量并不是一个可行的方案。我们也关注使用分布式调度软件后,系统的复杂度和可拓展性。假设一个系统充满了10000个需要锁的任务,这并不方便处理、拓展性也不强。

     同步(Synchronous)与串行化(Synchronized):在“ZStack可拓展性秘密武器1:异步架构”中,我们讨论同步(Synchronous)和异步,在这篇文章中,我们将讨论串行化(Synchronized)和并行化,同步(Synchronous)与串行化(Synchronized)某些时候是可以互换的,但是他们是不同的,在我们的文档中同步(Synchronous)是指一个任务将会在运行时阻塞线程,而串行化(Synchronized)是指一个任务执行时是互斥的。如果一个任务一直占用一个线程直到任务完成,它是同步的(Synchronous)任务。如果一个任务在其他任务执行时不能被同时执行,它是串行的(Synchronized)任务。

无锁架构的基础

     无锁架构的基础是一致性哈希算法,因为一致性哈希算法保证了对应同一资源的所有消息,总是被同一服务实例处理。这种聚合消息到特定节点的做法,降低了同步与并行化的复杂度,因为处理环境从分布式系统变为多线程。(详见“ZStack可拓展性秘密武器2:无状态服务”)。

在这里插入图片描述

工作流:传统而又清楚的解决方案

备注:在深入讲解细节之前,请注意我们将要讲的队列和“ZStack可拓展性秘密武器2:无状态服务”中提到的RabbitMQ的消息队列是没有任何关联的。消息队列就是RabbitMQ的一个术语,ZStack的队列指的是内部的数据结构。

因为ZStack是消息驱动的,聚合消息使得相关联的任务在同一节点执行,避免了需要使用线程池进行并发编程的经典问题。为了避免竞争锁,ZStack将使用工作队列而不是锁和信号量的方式。串行的(synchronized)任务以工作队列的方式保存在内存中,最终一个个的被执行。

在这里插入图片描述

工作队列在保存任务的同时也保存并行度,使得并行的任务能以一个定义好的并行度执行。下面的例子展示了一个并行度为4的队列。

在这里插入图片描述

备注:串行的(synchronized)和并行的任务都可以被工作队列执行,当并行度等于1的时候,队列是串行的(synchronized);并行度大于1的时候,队列是并行的;并行度等于0的时候,队列的并行度不被限制。

基于内存的同步队列

在ZStack中有两种类型的工作队列;一种是同步的(synchronous)工作队列,即一个任务(通常是一个Java 可执行程序)在它返回后才被认为已经完成。

thdf.syncSubmit(new SyncTask() {

@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;

}

});

强调:在同步(synchronous)队列中,一个Runnable.run()返回后,工作线程将继续抓取下一个可执行程序,一直到整个队列为空时,该线程将被放回线程池。因为任务将一直占用一个工作线程,所以队列是同步的(synchronous)。

基于内存的异步队列

另一种异步工作队列的方式指的是,当任务发布了一个完整的通知,就被认为已经完成。

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.99%的解决一个管理节点内的串行和并行问题,一致性哈希算法会导致系统进入混乱的状态:一个新加入的节点将因为一致性哈希算法接管临近节点的一部分工作。

在这里插入图片描述

在这个样例中,node 3新加入后,之前在node2上的一部分任务将转移到node3上,在这个时候,如果一个与某资源相关的旧任务仍然运行在node2上,但是与同一资源相关的新任务被提交到node3中,这种情况下就会发生混乱。然而,情景并没有想象的那么糟糕。首先,互相冲突的任务在一个正常的系统中很少存在,比如,一个健壮的UI应该不允许你在启动一台虚拟机的同时关闭这台虚拟机。第二,每一个ZStack的资源有他们的状况(state)/状态(status),如果任务开始的时候,资源处于错误的状况/状态,这将会导致报错。比如,如果一个虚拟机在停止状态,给该虚拟机绑定磁盘的任务将立即返回错误。第三,处理很多任务传输的代理(agents),有一个附加的串行机制;例如:尽管我们在管理节点上已经有了云路由的工作队列,云路由代理(agent)将串行所有修改DHCP配置文件的请求。最后,提前规划好操作是持续管理云的关键,操作组可以在启动云之前启动足够的管理节点,如果不需要动态添加一个新的节点,当云在运行的时候添加管理节点的几率是较小的。

总结

本文主要介绍了无锁架构,它是建立在基于内存的工作队列的基础上的。ZStack没有使用复杂的分布式调度软件,尽可能的提升了自己的性能,同时也预防了任务异常的产生。

猜你喜欢

转载自blog.csdn.net/zstack_org/article/details/86623405
今日推荐