借助Tomcat聊一下高性能IO模型-Reactor

​ 目前在硬件资源上,比如CPU和内存都有了很高的一个扩展,所以网络带宽以及IO成为了制约性能最重要的一个因素,了解高性能的IO模型对于开发人员来说是不可缺少的。


1 传统IO流程

image-20210831193521484

​ 可以看出在传统的IO处理过程中,首先服务端要等待客户端建立连接(一次阻塞),建立好连接后,要等待客户端的数据发送(一次阻塞),最终才能执行自身的业务处理(一次阻塞)并且向客户端返回数据。

​ 整个过程下来一共发生了三次阻塞过程,这使得程序无法充分利用硬件资源,并且在阻塞时还会发生系统调用带来上下文的切换过程,最终导致服务端能够同时处理的Socket是有限制的。

​ 而现在的技术演进,不管是操作系统级别的多路复用,还是程序设计上的Reactor模型,其实归根结底就是为了减少这些阻塞过程的。

2 Reactor模型

如果清楚了上图中的三个阻塞过程,那么你将会对Reactor中的三个组件感到无比的清晰易懂。

  1. Reactor:Reactor负责监听并分发来自Socket的事件(包括建立连接、可读、可写),并将该事件分发给不同的事件处理器进行后续处理。Reactor在底层可以使用操作系统提供的多路复用,可能够做到仅使用单个线程就能处理庞大规模的Socket事件。
  2. Acceptor:Acceptor只负责建立与客户端的链接,并进行注册读事件将IO后续操作传递给Handler处理,使得建立Socket的操作可以异步处理,降低了阻塞2的时间。
  3. Handler:Handler使用额外的线程或者线程池进行处理读写事件来降低对IO处理本身的阻塞时间(也就是阻塞3)。

是不是有一点流水线的感觉,每个组件只负责特定的功能,并使用异步 + 队列的方式尽可能的压缩了原本的阻塞过程。

同时Reactor模型自身也有很多变种,比方说Redis使用的是单线程Reactor单线程Handler(6.0.1以后支持了多线程Reactor),Tomcat使用的是单线程Reactor多线程Handler。

这个主要看自身所支持的业务特点,比方说我的具体IO处理速度非常快,像Redis一样,能控制在1ms以内,那么单Handler也就够了。再比方Tomcat是用作web服务器的,服务端的请求过程一般会在200-500ms左右,这样阻塞时间就很长了,所以就需要用到多线程Handler减少单个请求的阻塞时间。

接下来我会根据Tomcat的IO处理模块来详细讲述Reactor模型的应用。

3 Tomat IO模型

tomcat

开局一张图,我一直认为图是最直观的(理解Tomcat模型,需要阅读人员具备对Java NIO有基本了解)。

Tomcat中的Acceptor,类似于Acceptor功能,相当于Tomcat主线程,会不断轮询的监听客户端请求并建立Socket。当有Socket建立好以后,会分发到Poller中的阻塞队列中(可以理解为生产者-消费者模型)。

Tomcat中的Poller类似于Reactor作用,首先会循环处理当前阻塞队列中的PollerEvent,并将相应的事件注册到自己的Selector中。

之后Poller就会轮询调用Selector.selectedKeys()方法,来获取到当前可以处理的事件并进行分发操作,如果是可读事件,就会用Tomcat中的Executor线程池进行异步处理,来进行Servlet请求发送。

从上图可以看出,一个完整的Web请求在Acceptor、Poller、Executor的配合下几乎没有阻塞过程,这也是Reactor被誉为高性能IO模型的原因,可以说是将硬件资源利用到了极致。

扩展知识–IO多路复用

在多路复用技术问世之前,如果在我们程序中,想要同时去处理多个Socket的链接,那么只能通过新增线程的方式去实现,如下图所示,每建立好一个连接,就需要消耗一个额外线程去处理。

simple-io

线程也是一种资源,是有限的。如果服务器想要具有成千上万的并发,那么消耗的线程数会无比的巨大(就算使用上了线程池,也会耗费很多的线程)。为了能满足开发人员高并发的需求,操作系统在底层为我们提供了多路复用的支持,能够让我们仅消耗非常少的线程就能同时处理大规模的Socket。

以下是linux主要提供的三种多路复用技术(可能还有其他的,像kqueue之类的,不过这些我就没怎么了解了)

我这里也只是简单提一下,本篇文章主要不是讲IO复用,有个印象即可。

  1. select 每次调用时,向操作系统提供一个socket列表,操作系统会返回每个socket当前的状态,相当于说由操作系统帮你去轮询进行判断哪些socket有事件触发了。
  2. poll 跟select类似,向操作系统提供socket列表由操作系统去判断读写事件,但是没有最大数量限制。
  3. epoll 不需要每次传入整个socket列表,改为注册方式,由内核去维护当前所有已注册的socket以及相应的事件回调,效率得到明显的提升。