NP-22-Reactor模型

讲到高性能IO绕不开Reactor模式,它是大多数IO相关组件如Netty、Redis在使用的IO模式,为什么需要这种模式,它是如何设计来解决高性能并发的呢?

最最原始的网络编程思路就是服务器用一个while循环,不断监听端口是否有新的套接字连接,如果有,那么就调用一个处理函数处理,类似:

1
2
3
4
while(true){ 
socket = accept();
handle(socket)
}

这种方法的最大问题是无法并发,效率太低,如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低。
之后,想到了使用多线程,也就是很经典的connection per thread,每一个连接用一个线程处理,类似:

1
2
3
4
while(true){ 
socket = accept();
new thread(socket);
}

Tomcat服务器的早期版本确实是这样实现的。多线程的方式确实一定程度上极大地提高了服务器的吞吐量,因为之前的请求在read阻塞以后,不会影响到后续的请求,因为他们在不同的线程中。这也是为什么通常会讲“一个线程只能对应一个socket”的原因。最开始对这句话很不理解,一个线程中创建多个socket不行吗?语法上确实可以,但是实际上没有用,每一个socket都是阻塞的,所以在一个线程里只能处理一个socket,就算accept了多个也没用,前一个socket被阻塞了,后面的是无法被执行到的。

缺点在于资源要求太高,系统中创建线程是需要比较高的系统资源的,如果连接数太高,系统无法承受,而且,线程的反复创建-销毁也需要代价。
线程池本身可以缓解线程创建-销毁的代价,这样优化确实会好很多,不过还是存在一些问题的,就是线程的粒度太大。每一个线程把一次交互的事情全部做了,包括读取和返回,甚至连接,但是如果线程不够,有了新的连接,也无法得到处理,所以,目前的方案线程里可以看成要做三件事:连接,读取和写入。

一个连接一个线程的粒度太大了,限制了吞吐量,应该把一次连接的操作分为更细的粒度或者过程。在Reactor中,这些被拆分的子过程对应的是handler,每一种handler会出处理一种event。这里会有一个全局的管理者selector,我们需要把channel注册感兴趣的事件,那么这个selector就会不断在channel上检测是否有该类型的事件发生,如果没有,那么主线程就会被阻塞,否则就会调用相应的事件处理函数即handler来处理。典型的事件有连接,读取和写入,当然我们就需要为这些事件分别提供处理器,每一个处理器可以采用一个线程的方式实现。一个连接来了,先是被读取线程或者handler处理,然后再执行写入,那么之前的读取线程就可以被后面的请求复用,吞吐量就提高了。

Reactor单线程模型

几乎所有的网络连接都会经过: 接受用户请求——>读请求内容——>解码——>计算处理——>编码回复——>回复的过程。Reactor单线程模型指的是所有的这些操作都是在一个线程中去完成的。

采用基于事件驱动的设计,当有事件触发时,才会调用处理器进行数据处理。一般是通过将ServerSocketChannel注册到Selector上,并监听Accept事件,当Selector检测到Accept事件后,即是收到了客户端请求后,然后将客户端的SocketChannel注册到Selector,并监听Read、Write事件,然后根据客户端的读写响应响应事件。

image

如上,通过Acceptor接收客户端的连接,当链路建立完成后通过Dispatch将对应的ByteBuffer派发到指定的handler上,进行消息解码。用户线程消息编码后通过nio线程将消息发送给客户端。由于只有单个线程,所以处理器中的业务需要能够快速处理完。

  • Reactor:负责响应IO事件,当检测到一个新的事件,将其发送给相应的Handler去处理。
  • Handler:负责处理非阻塞的行为,标识系统管理的资源;同时将handler与事件绑定。
  • Reactor为单个线程,需要处理accept连接,同时发送请求到处理器中。

在一些小容量的应用场景下,可以使用单线程模型,但对于高负载,大并发的应用场景确不合适,主要原因如下:

  1. 一个nio线程同时处理成百上千的链路,性能无法满足,即便nio线程的cpu达到100%,也无法满足海量的消息编码、解码、读取和发送。
  2. nio线程负载过重,处理速度变慢,这会导致大量的客户端的连接超时,超时之后往往会进行重发,这更加加重了nio线程的负载,最终导致大量消息积压和处理超时,成为性能瓶颈。
  3. 可靠性问题:一旦nio线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

Reactor多线程模型

它与单线程模型最大的区别就是多了一组nio线程池来处理io操作,其模型如下:

image

特点如下:

  1. 有一个nio线程负责处理客户端的连接;
  2. 增加了一组nio线程池来处理网络io操作;

在绝大数情况下,Reactor多线程模型可以满足性能需求。但是,在个别特殊场景中,一个nio线程负责监听和处理所有客户端的连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下Acceptor线程会存在性能不足的问题,为了解决这个问题,产生了第三种模型,主从Reactor模型

Reactor主从多线程模型

Reactor主从多线程模型,用于监听客户端的连接不在是一个Nio线程了,它是一个nio线程池进行监听客户端的连接包括安全认证,当链路建立后就将网络读取的操作放在另外一个线程去进行读取。其模型如下:

image

利用多线程模型可以有效处理一个线程无法处理多个客户端连接请求的情况,在netty官方Demo中,推荐使用该线程模型。

netty的线程模型不是一层不变的,它取决于用户的启动参数配置。通过设置不同的启动参数,netty可以同时支持Reactor单线程模型、多线程模型、主从Reactor线程模型。