Reactor 单线程模型,是指所有的 I/O 操作都在同一个 NIO 线程上面完成。 NIO 线程的职责如下。
作为NIO服务端,接收客户端的TCP连接; 作为NIO客户端,向服务端发起TCP连接;读取通信对端的请求或者应答消息;向通信对端发送消息请求或者应答消息。在一些小容量应用场景下,可以使用单线程模型。但是这对于高负载、大并 发的应用场景却不合适,主要原因如下:
一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的 CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送。当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超 时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致 大量消息积压和处理超时,成为系统的性能瓶颈。可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统 通信模块不可用,不能接收和处理外部消息,造成节点故障。Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程来处理 I/O操作
Reactor 多线程模型的特点如下:
有专门一个NIO线程——Acceptor线程用于监听服务端,接收客户端的TCP连接请求网络I/O操作——读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP连接请求并处理完成后(可能包含接入认证等),将新创建的 SocketChannel 注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责 SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅用于客户端的登录、 握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池 的 I/O 线程上,由 I/O 线程负责后续的 I/O 操作。
为了尽可能地提升性能,Netty 在很多地方进行了无锁化的设计,例如在 I/O 线程内部进行串行操作,避免多线程竞争导致的性能下降问题。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线 程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程 设计相比一个队列—多个工作线程的模型性能更优。
Netty 的 NioEventLoop 读 取 到 消 息 之 后, 直 接 调 用 ChannelPipeline 的 fireChannelRead (Object msg)。只要用户不主动切换线程,一直都是由 NioEventLoop 调用用户的 Handler,期间不进行线程切换。这种串行化处理方式 避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
时间可控的简单业务直接在 I/O 线程上处理,如果业务非常简单,执行时间非常短,不需要与外部网元交互、访问数据库和磁盘,不需要等待其它资源,则建议直接在业务 ChannelHandler 中执行,不需要再启业务的线程或者线程池。避免线程上下文切换,也不存在线程并发问题。
复杂和时间不可控业务建议投递到后端业务线程池统一处理,对于此类业务,不建议直接在业务ChannelHandler 中启动线程或者线程池处理,建议将不同的 业务统一封装成 Task,统一投递到后端的业务线程池中进行处理。过多的业务 ChannelHandler 会带来开发效率和可维护性问题,不要把 Netty 当作业务容器, 对于大多数复杂的业务产品,仍然需要集成或者开发自己的业务容器,做好和 Netty 的架构分层。
业务线程避免直接操作 ChannelHandler,对于 ChannelHandler,IO 线程和 业务线程都可能会操作,因为业务通常是多线程模型,这样就会存在多线程操作 ChannelHandler。为了尽量避免多线程并发问题,建议按照 Netty 自身的做法, 通过将操作封装成独立的 Task 由 NioEventLoop 统一执行,而不是业务线程直接操作.
Netty 采用了典型的三层网络架构进行设计和开发
基于 Netty 的基础 NIO 框架,可以方便地进行应用层协议定制,例如 HTTP 协议栈、Thrift 协议栈、FTP 协议栈等。这些扩展不需要修改 Netty 的源码,直 接基于 Netty 的二进制类库即可实现协议的扩展和定制。 目前,业界存在大量的基于 Netty 框架开发的协议,例如基于 Netty 的 HTTP 协议、Dubbo 协议、RocketMQ 内部私有协议等。