3.2.2 Orleans任务调度器
Orleans调度器是一个定制化的TPL任务调度器,它是Orleans运行时保证其所承载的应用程序逻辑(Grain中开发者所定义的业务逻辑)和部分运行时逻辑能够以单线程语义运行的核心组件。它由两级调度器组合而成:第一级为负责调度执行系统活动的全局Orleans任务调度器(Orleans Task Scheduler,OTS);第二级为各Grain实例自身的实例任务调度器(Activation Task Scheduler,ATS),ATS保证了Grain实例内部逻辑的单线程执行语义。
1. 工作项与调度上下文
Orleans使用了一种称之为工作项(Work Item)的概念来描述调度器的输入对象,每个新请求都将以工作项的形式入队调度,该工作项实际上只是对该请求响应逻辑中的第一个任务进行的简单转换和包装,并加入了一些任务调度所需的公共上下文信息(包括调用者、活动名称及日志对象等)以及一些后续调度动作所需的额外信息(如调用型工作项在执行后的任务逻辑),在Orleans运行时中共有以下5种类型的工作项。
• 调用型工作项(Invoke Work Item):这是最常见的工作项类型,表示对应用程序请求的执行。
• 请求/响应工作项(Request/Response Work Item):代表对系统级别请求(即对系统目标的请求)的执行。
• 任务工作项(Task Work Item):表示在OTS调度器中可以直接执行的任务,它实际上只是.NET运行时所定义的Task对象的简单数据封装。
• 工作项组(Work Item Group):通过一定的数据封装包含了一组共享同一个任务调度器的工作项,通常用来表示同一个ATS下的所有工作项。
• 闭包工作项(Closure Work Item):闭包逻辑(任意Lambda表达式)封装成的工作项,用于将闭包逻辑在系统上下文中调度执行。
调度上下文(Scheduling Context)实际上是对任务逻辑执行目标的一种标识,在Orleans运行时中实际只存在三种调度上下文类型:Grain激活实例数据(代表工作项需要在Grain激活实例上运行)、系统目标(代表工作项的执行目标是系统对象)和空白上下文(对应普通任务工作项)。
2. 应用程序任务的调度
对于常规的应用程序请求的响应任务,即业务逻辑中客户端通过Grain Reference对Grain实例发起的远程过程调用请求,Orleans任务调度器将以图3-5所示的步骤对该请求/响应任务进行调度执行:
1)Orleans运行时将客户端请求的目标函数与Grain实例包装成一个待执行的调用型工作项,并通过QueueWorkItem方法提交给OTS进行任务调度。
2)OTS根据同时传入的任务调度上下文将任务对象转发至该Grain实例对应的ATS对象进行任务调度。
3)该Grain实例的ATS对象有一个与之唯一绑定的工作项组对象,该工作项组对象中维护了一个先进先出(FIFO)的本地任务项队列,ATS将接收到的任务对象放入任务项队列中。
4)工作项组对象在接收到新的任务请求时,将通过ScheduleExecution方法通知OTS本地有待执行的请求任务。
5)OTS接收到工作项组对象调度请求后,将工作项组对象放入Orleans的私有线程池中等待运行。
6)Orleans私有线程池线程从任务队列中拾取工作项组对象后,调用该对象的Execute方法,再将此任务队列中队首的工作项投递至与该任务组对象绑定的ATS。
7)ATS在接收到请求任务对象调度请求后,将该对象放入.NET运行时托管线程池中执行。
8)任务执行完毕后,若工作项组对象队列中仍有待执行的请求任务,则将再次通知OTS并等待后续任务调度。
•图3-5 Orleans运行时的应用程序任务调度过程
3. 系统任务的调度
由于Orleans运行时对内部系统调用逻辑并不需要保证单线程执行语义,因此Orleans运行时对系统请求及系统任务的调度逻辑相对应用程序请求的任务调度而言要精简不少,其调度过程如图3-6所示。与应用程序请求的任务调度过程类似,系统任务调度的入口仍然是OTS的QueueWorkItem方法:
•图3-6 Orleans运行时中系统任务的调度过程
1)OTS通过任务调度上下文对系统任务的调度请求进行识别,并通过标准TPL调度器逻辑调用自身QueueTask方法进行处理。
2)OTS在QueueTask方法内部将系统任务对象包装成任务工作项。
3)待执行的任务工作项将交给Orleans的私有线程池直接执行。
4)任务对象将在Orleans私有线程池中执行完毕并返回。
4. Orleans线程模型
在Orleans运行时中实际存在着两组固定大小的线程池(见图3-7),即系统线程池和应用线程池。其中,系统线程池直接接受并执行OTS本地调度的系统级工作项,而应用线程池则实际执行通过ATS调度入队的工作组任务项,应用线程池内的工作线程实际只是将工作组任务项队列中的队首工作项取出,并交由.NET运行时的托管线程池运行,在任务执行期间该工作线程将被阻塞,而在.NET托管线程池中执行的任务是响应外部请求的调用工作任务流中的任务,任何.NET任务的内联/子任务都将默认由父级任务的调度器调度,因此调用工作任务所产生的后续任务也都将被排列于同一个任务组中依次执行。
通过上述任务调度逻辑可以看出,当有多个对Grain实例的应用的并发请求时,OTS将会把所有请求转发至同一个ATS实例上,并以阻塞入队的方式依次存入与之绑定的工作组对象中,工作组对象作为所有请求任务的集合将再次被OTS放入应用线程池中运行。对于单个Grain实例上的并发请求,则会由应用线程池内的任务执行逻辑确保依次运行,从而实现了Orleans对于应用处理的单线程执行语义。
在此可以注意到,由于应用线程池线程是以阻塞的方式等待应用任务的执行,因此Silo服务器对不同Grain实例请求的并发度将由应用线程池的线程数目决定,与之类似,系统级工作项的最大并发度也由系统线程池内的线程数量决定。
从系统实现角度而言,应用线程池的存在起到任务缓冲的作用,即系统管理员可以通过调整应用线程池的大小限制单个Silo的实际应用请求并发度,若移除Orleans应用线程池,虽然仍然可以保证应用处理逻辑的单线程执行语义,但容易造成在大量针对不同Grain实例请求到来时给予.NET系统线程池较大的调度压力。实际上Orleans的系统线程池和应用线程池都是由.NET运行时线程池改造而来的,其线程对象内部也实现了全局/本地任务队列的任务窃取算法和响应的任务内联优化,从而进一步提高了Orleans系统的整体性能。
•图3-7 Orleans运行时的线程模型
在Orleans系统中,系统线程池的大小被固定为2,而应用线程池的大小默认为max(4,服务器CPU数量),以最大程度减少线程切换造成的性能损失。
在最新版本的Orleans运行时中,已经将Orleans私有线程逻辑托管至.NET线程池中运行(即将Orleans私有线程池合并至.NET运行时的托管线程池中),并受益于.NET运行时的优化,Orleans运行时的执行效率得到了显著提升。