这篇文章并不主要介绍线程池模型和Reactor模型的细节,而是试图从本质上区分两者的区别。
首先还是要简要描述下这两者,配合一些例子说明:
-
线程池模型是servlet规范确定的模型,在其它很多框架中也常见,例如dubbo。线程池模型规定一个请求由一条线程服务,直到请求执行完。为了减少创建线程的开销和限定最大线程数,一般会使用线程池来做。这意味着,如果一个请求的业务代码中,sleep了一段时间或者阻塞IO,那么这条线程也只能等当前代码处理完成,并不能被分配去处理其它请求。
这就好比你去银行柜台办理业务,轮到你时,从你接受服务到结束服务,柜台服务员始终只服务于你,即便你在中途填一份文件需要一两分钟、或者接个紧急电话需要三两分钟,柜台服务员都不会在此期间去服务其它客户。
-
Reactor模型是异步IO复用,在nginx、haproxy等web服务器中很常见,netty框架也是此模型。Reactor模型一般使用一个线程来接入新的客户端,再使用N个线程来服务客户端,一般N是CPU核数的1到2倍,每个线程可以来回处理多个客户端的请求。
如果拿上面的银行柜台办理业务场景改造一下,就变成了:有一个大堂经理,他来接待每一位新客户,然后按一定规则将客户分配给一个柜台服务员。一个柜台服务员可以同时服务多名客户:客户A向服务员提出请求,服务员处理后发给客户A一份表格让A填写;此时服务员转去处理客户B的请求;直到客户A填完表格了,喊一声服务员,服务员就过来处理客户A的表格。如此反复。
上面是改造银行柜台的例子,和银行实际做法不太符合,只是为了方便大家理解过渡。再举个实际Reactor模型的例子:淘宝卖家的客服。消费者通过旺旺可以和卖家客服在线聊天,客服这边是个多客户接入系统,一次可以接入几十个客户,当她收到客户的消息时,她就会点开窗口回复客户,回复完后她就换到其它窗口回复其它客户,直到这个客户有新的消息过来。
初略来看,可能不少人会觉得Reactor模型性能要比线程池高,支持的并发数更高。这个结论是需要在特定的条件下才成立的。所以网上流行的一些如Reactor模型性能比传统的线程池高的说法,是不够准确的。较为准确的描述应当是:Reactor可以在既定资源下发挥更大的效率。在java中,每个线程默认占1M左右的内存,每个进程最多能开2000左右的线程,再高就可能导致OOM或其它问题,因此线程是比较珍贵的资源。在线程池模型中,如果有一个请求卡住了,把默认tomcat 200个线程占满了,后面的请求就无法被处理了。这就好像银行开了10个柜台,其中办理某个业务比较慢,需要半个小时而且是卡住在客户填写资料这边,期间柜台服务员很空闲,然后刚好有20个人来办这个业务,那么这1个小时内,其他人就办理不了业务了。这就造成了一个现象:柜台服务员很空闲,外面又有很多客户没法办理业务。这个问题,就只能用Reactor模型解决。
再回到技术上,以nginx为例,它就必须、也只能用Reactor模型。nginx无法控制后面代理的web服务器每个请求的处理时间,即便nginx开了2000个线程,只要有1个请求慢到10秒钟,那么200个并发就可以把nginx堵死,造成nginx无法响应其它请求,表现为僵死状态。
所以,只要是网关、或者说网络请求的中间层,那必须是Reactor模型。这样当后台服务慢时,它增长的资源就只有TCP连接数,这个在操作系统层面是IO文件描述符,由linux维护,一般有epoll模型,保证处理请求的时间和资源不会随着连接数增长而暴涨。
话说回来,既然Reactor优势这么明显了,那为何还有线程池模型的存在呢?既然线程池模型还在大量使用,那就说明它有它特有的优势。在线程池模型下,我们可以使用ThreadLocal,将当前请求的上下文信息等存到里面,代替层层传参的方式。此外,线程池模型下,业务逻辑的编写也比Reactor模型要简单明了很多,很大提高了开发效率。鉴于这2个优势,以线程池模型为代表的servlet规范,在Java业务开发领域,依旧是主流。
最后说下结论:鉴于这两者的区别,一般的网络结构中的所有中间转发层都用Reactor模型,实际业务处理层用多线程模型。中间网络层可以有多层。其实细看tomcat的内部接口,它也是由Reactor模型(接入新客户时)+多线程模型(实际处理业务时)构成的。