如何设计一个异步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请求完成得足够快使得这个问题并不需要被关注. ...
随机推荐
- python自学——列表
#以下是我自己在联系列表中所编写的语句:names=["zangsan",'lisi','wangermazi','Xiaoliuzi','dabiaoge','牛erbiaodi ...
- 系统监控工具-glances
作为运维不仅要时时监控系统,还需要优雅的装13,当别人还在命令的小黑框里敲打那个top命令去查看系统性能的时候,我们带你使用web端监控系统性能 glances 是利用Python语言开发,的监控工具 ...
- Windows10 家庭版没有本地组策略解决方法
windows 家庭版默认是没有本地组策略的, win+R运行gpedit.msc提示不存在 下面的代码保存一个cmdorbat 脚本文件add_gpedit.cmd, 并执行 add_gpedit. ...
- Will Georgia Tech's $7K online M.S. in computer science program make the grade?
https://newatlas.com/georgia-tech--graduate-computer-science-degree-mooc/28763/ Georgia Tech to offe ...
- Windows XP添加硬盘后系统不能识别(没有任何反应)
解决方法: 1.右键我的电脑--管理--设备管理器--IDE ATA/ATAPI控制器,启用次要IDE通道和主要IDE通道,打开属性,在高级设置里,将设备类型设置为自动检测,重启. 2.硬盘格式为GP ...
- vc MFC 通过IDispatch调用默认成员函数
CComPtr<IDispatch> spDisp(IDispatch *); if(!spDisp) return; DISPPARAMS dispParam={0}; //没有参数 V ...
- [C++] stack和queue的常用函数
参考资料: STL 在 OI 中的应用 stack stack 后入先出(LIFO)栈 头文件: #include<stack> 定义: stack<int> s; 函数: 函 ...
- text/html & text/plain的区别
需要了解的概念 Content-Type:用于定义用户的浏览器或相关设备如何显示将要加载的数据,或者如何处理将要加载的数据 MIME:MIME类型就是设定某种扩展名的文件用一种应用程序来打开的方式类型 ...
- 微信发红包 PHP 实现
最近做生日营销,需要微信发红包,特此从网上找了一篇教程 首先你的有个服务号,并且开通了微信支付,我在这就不说怎么去申请和开通了,我是看了微信官方文档后,想看官方文档的朋友可以到下面这个链接 https ...
- php输出日志
error_log('你要输出的信息', 3, 'E:\work\jiajiayue\Application\Api\Controller\1.txt');die; php error_log记录日志 ...