如何设计一个异步Web服务——任务调度

接上一篇《如何设计一个异步Web服务——接口部分html

 

Application已经将任务信息发到了Service服务器中,接下来,Service服务器改如何对自身的资源进行合理分配以知足Application对功能、性能、用户体验等各方面的需求呢?服务器

 

能够从以下几个方向入手去考虑:微信

  1. 当task提交到Service后,咱们但愿Service可以尽量快的完成这个task并返回结果。
  2. 当大量task同时提交Service后,咱们但愿Service不要由于须要同时处理大量task致使性能降低,甚至失去响应。
  3. 当有多个task被提交到Service时,咱们不但愿某一个task占用了全部计算资源,致使其余task长时间处于等待状态。

根据上面的要求,咱们会产生以下的设计要求:并发

  1. 根据第一点要求,为了可以尽快完成一个task,咱们可使用多线程(或多进程)技术,将一个task拆分为多个子task而后并行处理,充分利用多核CPU的计算资源。
  2. 根据第二点要求,咱们须要为Service实现一个任务队列,以避免大量并发请求致使Service计算资源被耗尽。同时,大量的并发也会致使CPU为进行资源调度浪费许多计算资源。
  3. 根据第三点要求,虽然Service会使用任务队列对任务进行排队处理,但咱们仍然但愿有少许的task是并行进行的。
  4. 另外,一个task被拆分为许多子task后,若是为每一个子task建立一个单独的线程去处理,会致使CPU将大量时间消耗在线程的建立、销毁过程当中。因此,应该使用线程池(或进程池)技术。

 

下面,咱们就根据上述的这些要求开始设计。异步

 

首先,咱们须要一个http服务器做为接收Application请求的接口。而后,咱们建立一个QueenAnt(蚁后)类来负责任务和资源的调度,同时还须要若干个WorkerAnt(工蚁)类来处理各个具体的task。性能

 

注意,这里的QueenAnt类是静态的,或者也能够用单例模式建立。前面提到咱们须要使用线程池(或进程池)技术,因此,在QueenAnt类被实例化之后首先就须要把这个线程池建立出来,并建立若干线程放入这个池中。其中,每一个线程都会实例化一个WorkerAnt类来等待QueenAnt发过来的task。这个地方还有一个问题,那就是咱们的线程池中到底建立几个线程最优?这个问题我留到后面说明。ui

当这些准备好了之后,Service就能够等待Application的请求了。spa

当Application向Service发出addTask的请求时,http服务器会将这个请求通知给QueenAnt,并返回QueenAnt返回的taskId。线程

QueenAnt在收到task请求后,除了返回taskId,还须要对这个task的相关信息进行初始化,好比设置task的状态信息,将task添加到任务队列等等。

等这些结束之后,QueenAnt就开始针对已经收到的task进行任务调度和资源分配了。咱们定义一个allocateResource方法来处理相关的逻辑。该方法将会指定threadPool中的哪一个具体线程会来处理这个task。这以后,咱们就能够把task相关的数据发给这个指定的thread进行处理了。而当有task完成时,处理该task的线程中的WorkerAnt就会发送相关信息给QueenAnt,调用QueenAnt的taskEndCallback方法,让QueenAnt从新分配资源。

 

当WorkerAnt完成某一个task以后,他须要将这个task的相关信息返回给QueenAnt。同时标记本身为空闲状态,以便QueenAnt再进行资源分配。

QueenAnt在收到WorkerAnt关于task完成的消息后,他也须要更新于这个task的相关状态信息,并在此根据threadPool和taskQueue的具体状况从新进行资源分配。

 

到这里,咱们就经过上图描述的逻辑,知足了设计要求中的第二和第四点要求。那第一和第三点要求呢,就得经过allocateResource这个方法去实现了。

下面咱们详细讲一下allocateResource这个方法的内部逻辑。

这里先声明一下后文的描述方法,咱们把Application发过来的一个任务叫作"task",而把由这个任务拆分出来的许许多多的小任务叫作"子task"。

 

可能有人会产生疑惑,根据设计要求中的第一点,咱们应该把task拆分为子task。可上面的设计中,咱们放入taskQueue的倒是Application传过来的task,是否是差一个拆分的步骤呢?

其实并非这样,这样的设计是由于开头的考虑方向中的第三点和设计要求中第三点,都要求一个task不能够占用全部的计算资源。这样说可能不太好理解,咱们来举个例子:

首先,Application向Service提交了task01,该task共20个子task,须要Service满负荷运行5分钟才能完成。

到第3分钟的时候,Application又向Service提交了task02,该task共4个子task,须要Service满负荷运行1分钟便可完成。

咱们来分析一下这个场景。若是咱们在将task01加入taskQueue以前,就将其拆分为许多的子task。并把threadPool中的资源依次分给这些子task。那么到第3分钟加入task02的各个子task的时候,因为task01的子task没有完成,task02只好处于等待状态。并且须要等task01的几乎全部子task都完成之后,才能进入处理中的状态,这一等就是10分钟。这显然违背了咱们考虑方向中的第三点和设计要求中第三点。

 

那么,怎样设计这个allocateResource的逻辑才能既知足设计要求中的第一点,又能知足第三点了?个人思路是这样的。

 

首先,咱们给task加上两个属性threadRequirement和runningThread。threadRequirement表示,为了完成这个task,若是给其每一个子task分配一个线程,那么一共须要多少个线程,随着子task的完成,这个数值会愈来愈小,最后变为0即表示这个task已经所有完成。runningThread表示,当前有几个线程正在处理这个task的子task。

 

而后,allocateResource这个方法有两个地方会调用,一是当Service收到新的task请求的时候。二是当某个子task完成,QueenAnt中的taskEndCallback被调用的时候。

allocateResource在给task分配资源的时候,应遵照如下几个准则:

  1. taskQueue中处于等待状态的task应该尽量的少。
  2. 同时进行的task的数量不得超过threadPool中线程的总数。
  3. 每一个task都应该至少有一个线程在处理其子task。
  4. 在知足以上条件的状况下,threadRequirement最小的task分配到全部剩余的空闲线程资源。

 

这样说可能有些抽象。咱们仍是来举个上面那个例子,假设threadPool中共4个线程,task01的threadRequirement为20,task02的threadRequirement为2。过程以下:

  1. QueenAnt收到task01的请求后开始调用allocateResource方法。且当前threadPool中有空闲的线程资源。
  2. 根据准则1,咱们看到taskQueue中当前有一个task,就是task01。
  3. 当前没有正在进行的task数量没有达到threadPool中的线程总数4,知足准则2。因而将task01从taskQueue中取出准备为其分配线程资源。
  4. 为知足准则3,咱们将threadPool中的一个空闲线程thread01分配给task01的一个子task:childTask01。
  5. 为知足准则4,咱们将threadPool中剩下的全部空闲线程都分配给task01,这样以来,task01的4个子task,childTask01~childTask04同时接受处理。
  6. 一分钟后,childTask01~childTask04相继结束,taskEndCallback被触发,allocateResource再次被调用。重复上面的步骤3和步骤4。childTask05~childTask08开始接受处理。
  7. 又一分钟后,childTask05~childTask08相继结束,taskEndCallback被触发,allocateResource再次被调用。重复上面的步骤3和步骤4。childTask09~childTask12开始接受处理。
  8. 一秒钟后,QueenAnt收到task02的请求后开始调用allocateResource方法。但当前threadPool中没有空闲的线程资源,因此方法退出,task02停留在taskQueue中等待。
  9. 大概59秒之后,task01的一个childTask09结束,taskEndCallback被触发,allocateResource再次被调用。
  10. 根据准则2,当前正在进行的task只有task01,远没达到threadPool中线程的总数4,因此咱们能够将task02从taskQueue中取出准备为其分配线程资源。
  11. 根据准则3,每一个task至少分配一个线程资源,而当前task02的runningThread为0。因此咱们把刚才处理task01中childTask09的线程thread01分配给task02。就这样,task02也开始运行起来了。
  12. 接下来,threadPool中的thread02和thread03也相继完成了task01的childTask10和childTask11,并触发taskEndCallback调用allocateResource。
  13. 此时,咱们根据准则4,会把刚刚释放出来的thread02和thread03两个线程资源都分配给task02。这时,task01只有一个线程资源thread04在处理,而其余三个线程资源都被用来处理task02了。
  14. 再接下来,thread04处理完task01的childTask12后,根据准则3又会被分配给task01处理childTask13。

 

大概的逻辑就是上面这样了。步骤看起来虽然略显复杂,但其实只有掌握了前面说的4个准则,allocateResource的逻辑仍是很好实现:

 

至此,关于Service任务调度和资源分配的设计也结束了。

 

下面,咱们来讲一下前面遗留的一个问题:线程池中到底建立几个线程最优?

为何最后要特别来谈谈这个问题,是由于市面上有一种叫作超线程的CPU虚拟化的技术。好比Intel公司的酷睿i3系列CPU,明明是两个物理核心,在Windows的任务管理中,或在Linux系统的top命令下,显示的倒是4个核心,由于CPU在硬件层面将两个物理核心模拟为4个逻辑核心了。根据咱们上面的设计,天然是但愿threadPool中的线程数量越多越好,但是也不能太多。由于多个线程同时争用一个CPU核心的资源是没有必要的。因此,若是是4核的CPU,咱们通常会起4个线程放入threadPool。但是在这种使用了超线程技术的CPU平台上,若是你把线程数目配置为与CPU逻辑核心数目一致倒是没有必要的。我在i3平台上实测数据以下:

线程数

总耗时(s)

CPU 0 使用率

CPU 1 使用率

CPU 2 使用率

CPU 3 使用率

1

22.4

1

0

98

0

2

12.6

2

98

0

97

3

11.2

78

64

97

4

4

10.5

98

99

100

98

 

我想上面的数据已经很好地说明了问题,虽然是4个逻辑核心,虽然你可让4个线程同时运行,但其实在CPU物理层面,同时运行的指令最多就两个。也就是4个线程中每两个线程去争用一个物理核心的运算资源。

其结果就是,性能上的微小进步却带来了CPU使用率的大幅飙升,反而使得用来做为接口的httpServer响应时间变长。

 

如需转载,请注明转自:http://www.cnblogs.com/silenttiger/p/4135461.html

 

欢迎关注个人微信公众号:老虎的小窝
微信公众号 老虎的小窝

相关文章
相关标签/搜索