死磕 NIO— Reactor 模式就一定意味着高性能吗?

Posted Java 技术驿站

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了死磕 NIO— Reactor 模式就一定意味着高性能吗?相关的知识,希望对你有一定的参考价值。

大家好,我是大明哥,我又来了。

为什么是 Reactor

一般所有的网络服务,一般分为如下几个步骤:

  • 读请求(read request)

  • 读解析(read decode)

  • 处理程序(process service)

  • 应答编码 (encode reply)

  • 发送应答(send reply)

接下来,大明哥就来分析解决这个问题的最佳实践。

单线程模式

对于很多小伙伴来说,最简单,最传统的方式就是一个方法来处理所有的请求,这种实现方式最简单,也是最保险的方式。

这种方式实现起来虽然简单,但是性能不行,如果其中有一个请求因为某种原因阻塞了,则他后面的所有请求都会阻塞在那里,同时他也没法利用多 CPU 的性能,性能严重不足。

多线程模式

单线程的性能肯定不行,那就调整为多线程方式。

每来一个请求就会创建一个线程来处理,这种方式虽然不会像 单线程模式 一样,一个线程会阻塞所有的请求,但是他依然很大的问题:

  • 当客户端多,并发大的时候,需要创建大量线程来处理,线程的创建和销毁也很消耗资源,会导致整个系统的的资源占用较大

  • 同样无法应对高性能和高并发

线程池模式

既然多线程模式需要创建这么多线程,那么我们控制创建线程的个数,采用资源复用 线程池 的方式,也就是我们不需要再为每一个连接创建一个线程,而是创建一个线程池,将连接分配给线程,然后一个线程可以处理多个链接。

这种线程池的方式虽然解决了系统资源占用的问题,但是他依然带了了一个新的问题,每一个线程如何高效地处理请求呢?在上篇文章中 【死磕NIO】— 阻塞IO,非阻塞IO,IO复用,信号驱动IO,异步IO,这你真的分的清楚吗?我们提到过在单个线程中如果当前连接在进行read操作时,如果没有数据可读,则会发生阻塞,那么线程就没有办法继续处理其他连接的业务了。那么怎么解决?将 read 操作改为非阻塞的方式,既然改为了非阻塞方式,那线程如何知道read 操作有数据可读了呢?

  • 第一种方式,则是不断的去轮询,但是轮询要消耗 CPU的,而且随着轮询的线程多了,轮询的效率会越来越低

  • 第二种方式,事件驱动。当线程关心的事件发生了,比如read 有数据可读了,则通知相对应的线程进行处理

Reactor 模式

第二种方式就是 I/O多路复用。I/O多路复用就是通过一种机制,一个线程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知线程进行相应的读写操作。目前支持 IO多路复用技术有:

  • Linux:selectpollepoll

  • MAC:kqueue

  • Windows:select

监听线程帮助我们监听哪些线程的事件已发生,发生后则通知相对应的线程进行处理,这样就可以避免进行很多无用的操作。对处理线程而言,整个处理过程只有调用 selectpollepoll 的时候才会阻塞,其他时段,他可以处理其他的事情,这样整个线程会被充分利用起来,这样就高效很多了。

什么是 Reactor模式

上面讲了 Reactor 模式的演变,那什么是 Reactor 模式呢?

wiki上是这样定义的:

Reactor 模式也叫做反应器设计模式,它是一种为处理服务请求并发提交到一个或者多个服务处理程序的事件设计模式。当请求抵达后,服务处理程序使用解多路分配策略,然后同步地派发这些请求至相关的请求处理程序。

简要概括就是: 将消息放到了一个队列中,通过异步线程池对其进行消费。暂时理解成下面这个样子:

对于Reactor模式来说,他并没队列,每当有一个 Event 输入到 Server端时,Service Handler 会将其转发(dispatch)相对应的handler进行处理。

Reactor的组件主要包括三个:

  • Reactor:派发器,将 client端的事件分发给相对应的Handler

  • Acceptor:请求连接器,Reactor 接收到 client 连接事件后,会将其转发给 Acceptor,Acceptor 则会接受 Client 的连接,建立对应的Handler,并向 Reactor注册此Handler

  • Handler:请求处理器,负责事件的处理。

模型大致如下图:

Reactor 模式

Reactor 模型中的Reactor可以是多个也可以是单个,Handler同样可以是单线程也可以是多线程,所以组合的模式大致有如下四种:

  • 单Reactor单线程/进程

  • 单Reactor多线程/进程

  • 多Reactor单线程/进程

  • 多Reactor多线程/进程

其中第三种多Reactor单线程并没有什么实际的意思,所以大明哥重点介绍第一、二、四种。

单Reactor单线程/进程

  • Reactor 线程通过 select (IO多路复用接口)监听事件,收到事件后通过Dispatch 来分发事件,事件会分发给Acceptor和Handler 两个组件,具体是哪个组件要看事件的类型。

  • 如果事件类型为建立连接,则将事件分发给Acceptor,Acceptor会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件。

  • 如果时间类型不是建立连接,则将该事件交由当前连接的Handler来处理。

优缺点

  • 优点:该模型是将所有处理逻辑放在一个线程中实现,模型简单,没有多线程、进程通信、竞争的问题

  • 缺点

    • 由于只有一个线程,无法充分利用CPU,性能堪忧。同时Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。

    • 还有一个比较严重的可靠性问题,如果线程意外终止,或者进入死循环,则会导致整个线程都无法接受和处理事件了,造成节点故障。

单Reactor多线程/进程

单线程存在性能瓶颈,那我们就引入多线程方案。

Reactor 接受请求后,根据请求类型来进行分发,分发逻辑与 单Reactor单线程 模型一样,不同之处在于Handler不在进行业务处理了,它只负责接受和发送,Handler接受数据后,会将数据发送给 Worker 线程池中的线程处理,该线程才是处理业务的真正线程,线程将业务处理完成后,将数据发送给Handler,然后Handler 再send出去。

优缺点

  • 优点:由于Handler使用了多线程模式,则可以利用充分利用CPU的性能

  • 缺点:

    • Handler使用多线程模式,则会涉及到数据共享的问题,需要考虑互斥,实现肯定比 单Reactor单线程模式复杂一些

    • 单Reactor,一个线程处理事件监听、分发、响应,对于高并发场景,容易造成性能瓶颈

多Reactor多线程/进程

单Reactor多线程模式解决了Handler单线程的性能问题,但是Reactor还是单线程的,对于高并发场景还是会有性能瓶颈,所以需要对Reactor调整为 多线程模式

  • 主线程中的MainReactor对象通过select监听事件,接收到事件后通过Dispatch进行分发,如果事件类型为建立连接则将事件分发给Acceptor 进行连接建立

  • 如果收到的事件不是连接,则他将事件分发个某个SubReactor,SubrReactor 将连接加入到连接队列进行监听,并创建Handler进行各种事件处理

  • 如果有新的事件发生,SubReactor 则会调用当前连接的Handler来进行处理。Handler 通过read 读取数据后,将数据发送给Worker线程进行处理,Worker线程池则会分配线程进行业务处理,处理完成后返回结果,Handler接受结果后,通过send发送给客户端

优缺点

  • 优点:该模式主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理,同时主线程和子线程的交互也很简单,子线程接收主线程的连接后,只管业务处理即可,无须关注主线程

  • 缺点:模型复杂

这种模式适用于高并发场景,广泛运用于各种项目中,如大名鼎鼎的Netty。

Reactor 优缺点

Reactor模式有如下优点:

  • 响应快,不必为单个同步时间所阻塞

  • 可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销

  • 扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源

  • 复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性

虽然Reactor有诸多优点,但是由于他的IO读写数据时还是在同一个线程中实现的,如果当前线程出现了一个长时间的IO数据读写,则会影响其他的client。那怎么解决呢?请静候下一篇文章。

参考资料

PS:如果你觉得文章对你有所帮助,别忘了推荐或者分享,因为有你的支持,才是我续写下篇的动力和源泉!

死磕 NIO— Proactor模式是什么?很牛逼吗?

大家好,我是大明哥。

上篇文章我们分析了高性能 IO模型Reactor模式,了解了什么是Reactor 模式以及它的三种常见的模式,这篇文章,大明再介绍另外一种高性能IO模型: Proactor

为什么是 Proactor 模式

上篇文章 【死磕 NIO】— Reactor 模式就一定意味着高性能吗?大明哥分析了 Reactor模式,我们知道Reactor性能确实非常高,适合高并发场景,但是它依然存在一个问题,那就是它是 同步IO。同步IO会有一个什么问题呢?同步IO需要线程自己等待内核准备好数据,在内核准备数据的过程中,当前线程是阻塞的,这样就会导致如果某个线程因为读取IO的时间过长(比如读取文件、写文件),则它势必会影响其他线程的执行。如果对 同步IO、 异步IO 不了解的同学,可以看如下两篇文章:

既然 同步IO有缺陷,那我们是不是可以调整为 异步IO呢?完全可以,这就是 Proactor 模式

什么是 Proactor 模式

Proactor 模式整体与Reactor 模式一致,区别就在于Proactor模式将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。模型如下:

  • Procator Initiator:负责创建Handler和Procator,并将Procator和Handler都通过Asynchronous operation processor注册到内核。

  • Handler:执行业务流程的业务处理器。

  • Asynchronous operation processor:负责处理注册请求,并完成IO操作。完成IO操作后会通知Procator。

  • Procator:根据不同的事件类型回调不同的handler进行业务处理。

这里需要注意的是: Proactor关注的不是就绪事件,而是完成事件,这是区分Reactor模式的关键点

然而可惜的是,Linux下的异步 I/O 是不完善的,aio 系列函数是由 POSIX 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步,并且仅仅支持基于本地文件的 aio 异步操作,网络编程中的 socket是不支持的,这也使得基于 Linux 的高性能网络程序都是使用 Reactor 方案。

而 Windows 里实现了一套完整的支持 socket 的异步编程接口,这套接口就是 IOCP,是由操作系统级别实现的异步 I/O,真正意义上异步 I/O,因此在 Windows 里实现高性能网络程序可以使用效率更高的 Proactor 方案。

优缺点

  • 优点

    • 性能确实是强大,效率也高
  • 缺点

    • 复杂。性能好,效率高,东西是好东西,但是使用起来就是复杂。

    • 操作系统支持。上面提到过,Linux系统对异步IO支持不是很好,不是很完善

Proactor 模式与Reactor 模式

Proactor模式与Reactor模式 的区别有如下几点:

  • Reactor 模式注册的是文件描述符的就绪事件。当Reactor 模式有事件发生时,它需要判断当前事件是读事件还是写事件,然后在调用系统的read或者write将数据从内核中拷贝到用户数据区,然后进行业务处理。

  • Proactor模式注册的则是完成事件。即发起异步操作后,操作系统将在内核态完成I/O并拷贝数据到用户提供的缓冲区中,完成后通知Proactor进行回调,用户只需要处理后续的业务即可。

  • Reactor模式实现同步I/O多路分发

  • Proactor模式实现异步I/O分发

在 Linux 操作系统下实现高并发网络编程依然以Reactor 模式为主。

参考资料

以上是关于死磕 NIO— Reactor 模式就一定意味着高性能吗?的主要内容,如果未能解决你的问题,请参考以下文章

死磕 NIO— Reactor 模式就一定意味着高性能吗?

死磕 NIO— Reactor 模式就一定意味着高性能吗?

死磕 NIO— Reactor 模式就一定意味着高性能吗?

死磕 NIO— Proactor模式是什么?很牛逼吗?

死磕 NIO— Proactor模式是什么?很牛逼吗?

死磕 NIO— Proactor模式是什么?很牛逼吗?