ESFramework 开发手册(11) -- 服务端信息处理模型
基于ESFramework进行二次开发时,主要是通过自定义信息来实现业务逻辑功能,而自定义消息的处理则是通过ICustomizeHandler接口的HandleInformation(或HandleQuery)方法来进行的。但是,ICustomizeHandler接口的这两个方法是如何被框架调用的了?在介绍之前,我们先要了解一下.NET的线程池(ThreadPool)。
一. 线程池
.NET线程池中有两种类型的线程:工作者线程、完成端口IOCP线程。通过ThreadPool的静态方法SetMinThreads、SetMaxThreads可以对线程池做一些简单的设置。
一般在程序启动时,我们可以调用 System.Threading.ThreadPool.SetMaxThreads(100, 100); 将最大的工作者线程和IOCP线程数量设置为100。
然后,在服务端运行的过程中,可定时调用(比如每秒一次)ThreadPool的GetAvailableThreads方法,来查看线程中可用的(即,空闲的)工作者线程和IOCP线程的数量。这样就可以实时监控线程池中线程的状态。
ESFramework对ICustomizeHandler接口的调用都是在线程池的IOCP线程或工作者线程中进行的,至于究竟使用的是哪种类型的线程,取决于信息处理模型的设定。
另外说一下,ESFramework框架本身使用了数个工作者线程,以维持网络引擎的正常运转,但是,ESFramework框架自身的运转并不消耗任何IOCP线程。
二. CustomizeInfoHandleMode
ESFramework服务端为信息处理提供了两种方案,这两种方案通过CustomizeInfoHandleMode枚举进行定义,并对应该枚举有两个取值:IocpDirectly、TaskQueue。
我们知道,ESFramework内核是基于IOCP(Windows系统中最高效的模型)的,即服务端接收到的信息都是在IOCP线程中提交的, 对于提交的信息:
(1)如果信息处理模型被指定为IocpDirectly,则框架将直接在该IOCP线程中调用ICustomizeHandler接口来处理该信息。
(2)如果信息处理模型被指定为TaskQueue,则框架首先会将信息放入到一个任务队列中。然后由工作者线程从队列中依次取出信息,并调用ICustomizeHandler接口进行处理。
这里可以看出,这两种方案有几个明显的区别:
(1)IocpDirectly方案,ICustomizeHandler的调用是在IOCP线程中进行的;而TaskQueue方案,ICustomizeHandler的调用是在工作者线程中进行的。
(2)对于TaskQueue方案,IOCP线程的工作仅仅是将接收的信息放入到任务队列中。
我们可以通过IRapidServerEngine的Advanced控制器的CustomizeInfoHandleMode属性来指定要使用的方案(默认是IocpDirectly), 像这样:
rapidServerEngine.Advanced.CustomizeInfoHandleMode = CustomizeInfoHandleMode.TaskQueue;
对于二次开发人员而言,从一种方案切换到另一种方案,体现在代码上只是上面属性设置的修改,不需要在做任何其它的事情,框架内部会自动完成相应的工作。下面我们将深入这两种方案。
三. IocpDirectly方案
无论是使用IocpDirectly方案还是TaskQueue方案,一个客户端连接最多用到一个IOCP线程(提交接收到的消息的时候才用到)。但是对于IocpDirectly方案,由于提交消息是直接进入ICustomizeHandler的HandleInformation(或HandleQuery)方法调用,所以,IocpDirectly方案有以下特点:
(1)针对一个具体的TCP连接而言,信息处理是串行的。只有当前一个信息处理完成(即HandleInformation / HandleQuery方法返回)后,才处理该连接上的下一个请求信息。
(2)针对所有的TCP连接而言,信息处理是并行的。即同时有多个HandleInformation / HandleQuery方法 正在执行。
(3)通过观察IOCP线程的使用个数,就可以知道有多少个信息正在被处理。
(4)如果某个信息处理的耗时超过了心跳超时的设定,则对应的客户端会被当作心跳超时掉线,其TCP连接将被服务端主动断开。
四. TaskQueue方案
在TaskQueue方案中,IOCP线程不再重要,我们要关注的是任务队列和工作者线程。前面我们说道,工作者线程从队列中依次取出消息然后调用ICustomizeHandler接口进行处理。用于从事这一工作的工作者线程的数量是可以设定的,通过IRapidServerEngine的Advanced控制器的QueueWorkerThreadCount属性(默认是20), 像这样:
rapidServerEngine.Advanced.QueueWorkerThreadCount = 50;
当然,这个设定的数量必须不能超过ThreadPool的SetMaxThreads方法设定的最大工作者线程数。
另外,通过IRapidServerEngine的Advanced控制器提供的GetTaskQueueInfo方法,我们可以获取任务队列的当前状态:
/// <summary> /// 当CustomizeInfoHandleMode设置为TaskQueue时,获取队列中待处理的任务个数,以及历史中最大的待处理任务个数。 /// </summary> /// <param name="taskCount">待处理的任务个数</param> /// <param name="maxTaskCount">历史中最大的待处理任务个数</param> void GetTaskQueueInfo(out int taskCount, out int maxTaskCount);
那么,相比于IocpDirectly方案,TaskQueue方案有以下特点:
(1)针对一个具体的TCP连接而言,信息的处理也不是串行的,而且,后接收到的信息有可能先处理完。
(2)通过与空闲时(比如,程序启动时)线程池中可用的工作者线程数的对比,就可以知道有多少个信息正在被处理。
(3)如果某些信息处理缓慢,那么,通过GetTaskQueueInfo方法可看到队列中待处理的任务越来越多,同时可以观察到当前进程的内存消耗越来越大。从客户端表现来看,就是响应越来越慢。
五. 生产 - 消费模型
我们可以使用生产-消费模型来进行分析,从网络接收到消息,相当于生产消息,HandleInformation / HandleQuery方法的执行相当于是消费消息。决定服务端吞吐量的因素有很多,但最首要也是最核心的因素就是服务端消费消息的能力。每秒消费掉的消息数量越大,吞吐量也就越大。
当生产消息的速度小于或等于消息消费的速度时,服务端的业务处理是从容不迫的。
但是,当生产消息的速度大于或远远大于消息消费的速度时,并且这一状况一直持续下去时,可以想想会发生什么状况?我们简单推导一下。
1. TaskQueue方案
(1)为任务队列服务的工作者线程都将处于忙碌状态。
(2)队列中待处理的任务越来越多。
(3)内存消耗越来越大,可能导致进程引发OutOfMemoryException(内存溢出)异常。
(4)从客户端来看,新的请求无法被服务端响应。
2. IocpDirectly方案
(1)线程池中可用的IOCP线程会越来越少。
(2)每个TCP连接的Socket缓冲区中排队等待接收的消息会越来越多,当这个缓冲区满时,客户端再向服务端发送消息的方法调用将被阻塞。
(3)如果某些消息处理的异常缓慢,则可能导致某些客户端心跳超时掉线。
当生产消息的速度大于或远远大于消息消费的速度时,提升服务端信息处理能力(这涉及到的诸多方面的优化)是唯一的解决方案。但是,在未提升服务器的处理能力之前,对于这种情况,IocpDirectly方案和TaskQueue方案哪种更好一点了?
虽然,无论哪种方案,客户端的体验都将比较差,但是相比而言,我们推荐是IocpDirectly方案,原因在于:基于上面描述IocpDirectly方案的第(2)点,客户端发送消息的方法调用将被阻塞,这意味着会倒逼客户端降低生产消息的速度。采用IocpDirectly方案是ESFramework框架的默认设置。
下一篇:ESFramework 开发手册(12) -- 服务端性能诊断
上一篇:ESFramework 开发手册(10) -- 安全机制
-----------------------------------------------------------------------------------------------------------------------------------------------
Q Q:168757008