如何设计一个异步Web服务——任务调度
接上一篇《如何设计一个异步Web服务——接口部分》
Application已经将任务信息发到了Service服务器中,接下来,Service服务器改如何对自身的资源进行合理分配以满足Application对功能、性能、用户体验等各方面的需求呢?
可以从如下几个方向入手去考虑:
- 当task提交到Service后,我们希望Service能够尽可能快的完成这个task并返回结果。
- 当大量task同时提交Service后,我们希望Service不要因为需要同时处理大量task导致性能下降,甚至失去响应。
- 当有多个task被提交到Service时,我们不希望某一个task占用了所有计算资源,导致其他task长时间处于等待状态。
如需转载,请注明转自:http://www.cnblogs.com/silenttiger/p/4135461.html
根据上面的要求,我们会产生如下的设计要求:
- 根据第一点要求,为了能够尽快完成一个task,我们可以使用多线程(或多进程)技术,将一个task拆分为多个子task然后并行处理,充分利用多核CPU的计算资源。
- 根据第二点要求,我们需要为Service实现一个任务队列,以免大量并发请求导致Service计算资源被耗尽。同时,大量的并发也会导致CPU为进行资源调度浪费许多计算资源。
- 根据第三点要求,虽然Service会使用任务队列对任务进行排队处理,但我们仍然希望有少量的task是并行进行的。
- 另外,一个task被拆分为许多子task后,如果为每个子task创建一个单独的线程去处理,会导致CPU将大量时间消耗在线程的创建、销毁过程中。所以,应该使用线程池(或进程池)技术。
下面,我们就根据上述的这些要求开始设计。
首先,我们需要一个http服务器作为接收Application请求的接口。然后,我们创建一个QueenAnt(蚁后)类来负责任务和资源的调度,同时还需要若干个WorkerAnt(工蚁)类来处理各个具体的task。

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


当这些准备好了以后,Service就可以等待Application的请求了。
当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分配资源的时候,应遵守以下几个准则:
- taskQueue中处于等待状态的task应该尽可能的少。
- 同时进行的task的数量不得超过threadPool中线程的总数。
- 每个task都应该至少有一个线程在处理其子task。
- 在满足以上条件的情况下,threadRequirement最小的task分配到所有剩余的空闲线程资源。
这样说可能有些抽象。我们还是来举个上面那个例子,假设threadPool中共4个线程,task01的threadRequirement为20,task02的threadRequirement为2。过程如下:
- QueenAnt收到task01的请求后开始调用allocateResource方法。且当前threadPool中有空闲的线程资源。
- 根据准则1,我们看到taskQueue中当前有一个task,就是task01。
- 当前没有正在进行的task数量没有达到threadPool中的线程总数4,满足准则2。于是将task01从taskQueue中取出准备为其分配线程资源。
- 为满足准则3,我们将threadPool中的一个空闲线程thread01分配给task01的一个子task:childTask01。
- 为满足准则4,我们将threadPool中剩下的所有空闲线程都分配给task01,这样以来,task01的4个子task,childTask01~childTask04同时接受处理。
- 一分钟后,childTask01~childTask04相继结束,taskEndCallback被触发,allocateResource再次被调用。重复上面的步骤3和步骤4。childTask05~childTask08开始接受处理。
- 又一分钟后,childTask05~childTask08相继结束,taskEndCallback被触发,allocateResource再次被调用。重复上面的步骤3和步骤4。childTask09~childTask12开始接受处理。
- 一秒钟后,QueenAnt收到task02的请求后开始调用allocateResource方法。但当前threadPool中没有空闲的线程资源,所以方法退出,task02停留在taskQueue中等待。
- 大概59秒以后,task01的一个childTask09结束,taskEndCallback被触发,allocateResource再次被调用。
- 根据准则2,当前正在进行的task只有task01,远没达到threadPool中线程的总数4,所以我们可以将task02从taskQueue中取出准备为其分配线程资源。
- 根据准则3,每个task至少分配一个线程资源,而当前task02的runningThread为0。所以我们把刚才处理task01中childTask09的线程thread01分配给task02。就这样,task02也开始运行起来了。
- 接下来,threadPool中的thread02和thread03也相继完成了task01的childTask10和childTask11,并触发taskEndCallback调用allocateResource。
- 此时,我们根据准则4,会把刚刚释放出来的thread02和thread03两个线程资源都分配给task02。这时,task01只有一个线程资源thread04在处理,而其他三个线程资源都被用来处理task02了。
- 再接下来,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
欢迎关注我的微信公众号:老虎的小窝
如何设计一个异步Web服务——任务调度的更多相关文章
- 如何设计一个异步Web服务——接口部分
需求比较简单,提供一个异步Web服务供使用者调用.比如说,某应用程序需要批量地给图片加lomo效果.由于加lomo效果这个操作非常消耗CPU资源,所以我们需要把这个加lomo效果的程序逻辑放到一台单独 ...
- 深入理解Tornado——一个异步web服务器
本人的第一次翻译,转载请注明出处:http://www.cnblogs.com/yiwenshengmei/archive/2011/06/08/understanding_tornado.html原 ...
- 《Introduction to Tornado》中文翻译计划——第五章:异步Web服务
http://www.pythoner.com/294.html 本文为<Introduction to Tornado>中文翻译,将在https://github.com/alioth3 ...
- Python开发【Tornado】:异步Web服务(一)
异步Web服务 前言: 到目前为止,我们已经看到了许多使Tornado成为一个Web应用强有力框架的功能.它的简单性.易用性和便捷性使其有足够的理由成为许多Web项目的不错的选择.然而,Tornado ...
- [译]Spring Boot 构建一个RESTful Web服务
翻译地址:https://spring.io/guides/gs/rest-service/ 构建一个RESTful Web服务 本指南将指导您完成使用spring创建一个“hello world”R ...
- WPF 多线程 UI:设计一个异步加载 UI 的容器
对于 WPF 程序,如果你有某一个 UI 控件非常复杂,很有可能会卡住主 UI,给用户软件很卡的感受.但如果此时能有一个加载动画,那么就不会感受到那么卡顿了.UI 的卡住不同于 IO 操作或者密集的 ...
- Python开发【Tornado】:异步Web服务(二)
真正的 Tornado 异步非阻塞 前言: 其中 Tornado 的定义是 Web 框架和异步网络库,其中他具备有异步非阻塞能力,能解决他两个框架请求阻塞的问题,在需要并发能力时候就应该使用 Torn ...
- 第五章:异步Web服务
到目前为止,我们已经看到了许多使Tornado成为一个Web应用强有力框架的功能.它的简单性.易用性和便捷性使其有足够的理由成为许多Web项目的不错的选择.然而,Tornado受到最多关注的功能是其异 ...
- tornado之异步web服务一
大部分Web应用(包括我们之前的例子)都是阻塞性质的,也就是说当一个请求被处理时,这个进程就会被挂起直至请求完成.在大多数情况下,Tornado处理的Web请求完成得足够快使得这个问题并不需要被关注. ...
随机推荐
- 乘风破浪:LeetCode真题_041_First Missing Positive
乘风破浪:LeetCode真题_041_First Missing Positive 一.前言 这次的题目之所以说是难,其实还是在于对于某些空间和时间的限制. 二.First Missing Posi ...
- 4星|《门口的野蛮人2》:美国宝万之争专业户KKR公司的疯狂借债收购史
门口的野蛮人2:KKR与资本暴利的崛起(珍藏版) 英文版是1992年出的.主要内容是1977-1998年之间KKR在美国的杠杆收购简史.从KKR创立开始,讲到1990年KKR差点倒闭.国内A股市场上前 ...
- css常见效果
1.ul li横排 /* ul li以横排显示 */ /* 所有class为menu的div中的ul样式 */ div.menu ul { list-style:none; /* 去掉ul前面的符号 ...
- 用JS制作《飞机大作战》游戏_第4讲(创建敌方飞机、敌方飞机发射子弹、玩家子弹击中敌方小飞机,小飞机死亡)-陈远波
一.创建敌方飞机 1.思考创建思路: 创建敌方飞机思路与创建玩家飞机思路一样: (1)思考敌方飞机具备什么属性: 敌方飞机的图片.坐标.飞行速度.状态(是否被击中) 设置小飞机被击中时消失时间.飞机可 ...
- windows下安装python3 新手上路
本文只针对刚刚拿到“驾照”的实习生 老司机回去开车.. 下载python 地址:https://www.python.org/ 选择Downloads下的windows 选择自己合适的版本 下面的是 ...
- SGU刷题之路开启
VJ小组:SGU---48h/题 每道做出的题目写成题解,将传送门更新在这里. SGU:101 - 200 SGU - 107 水题 解题报告 SGU - 105 找规律水题 解题报告 SGU - 1 ...
- windows下vi/vim编辑器的基本操作
windows下vi/vim编辑器的基本操作 Contents 1. 工具准备(下载gvim) 2. vi/vim基本入门 2.1. 安装 2.2. 基本使用 3. vi/vim基本命令表 1 工具准 ...
- 使用filter对请求设置编码
一.复习 加强方法: 1)继承 2)装饰模式 3)动态代理. 使用装饰模式: 1)要求装饰者和被装饰者实现同一个接口或者继承同一个类. 2)装饰者要求被装饰者的引用. 3)对于要加强的方法进行加强,对 ...
- 《Java程序设计》第2周学习总结(Markdown语法修改版)
20175105 2018-2019-2 <Java程序设计>第2周学习总结 Vim操作的一些总结 这些天通过学习,对于vim的操作有了很大的提升,下面我把vim的比较常见的操作方式做了归 ...
- js 正则表达式验证密码、邮箱格式.....
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...