Netty 原理
Netty 是一个高性能、异步事件驱动的NIO 框架,基于JAVA NIO 提供的API 实现。它提供了对TCP、UDP 和文件传输的支持,作为一个异步NIO 框架,Netty 的所有IO 操作都是异步非阻塞的,通过Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得IO 操作结果。
Netty 高性能
在IO 编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者IO 多路复用技术进行处理。IO 多路复用技术通过把多个IO 的阻塞复用到同一个select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
与Socket 类和ServerSocket 类相对应,NIO也提供了SocketChannel 和ServerSocketChannel两种不同的套接字通道实现。
多路复用的通讯方式
Netty 架构按照Reactor 模式设计和实现,它的服务端通信序列图如下:
客户端通信序列图如下:
Netty 的IO 线程NioEventLoop 由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端Channel,由于读写操作都是非阻塞的,这就可以充分提升IO 线程的运行效率,避免由于频繁IO 阻塞导致的线程挂起。
异步通信 NIO
由于Netty 采用了异步通信模式,一个IO 线程可以并发处理N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
零拷贝DIRECT BUFFERS (使用堆外直接内存)
- Netty 的接收和发送ByteBuffer 采用DIRECT BUFFERS,使用堆外直接内存进行Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket 读写,JVM 会将堆内存Buffer 拷贝一份到直接内存中,然后才写入Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
- Netty 提供了组合Buffer 对象,可以聚合多个ByteBuffer 对象,用户可以像操作一个Buffer 那样方便的对组合Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小Buffer 合并成一个大的Buffer。
- Netty 的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write 方式导致的内存拷贝问题
内存池(基于内存池的缓冲区重用机制)
随着JVM 虚拟机和JIT 即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制。
高效的 Reactor线程模型
常用的Reactor 线程模型有三种,Reactor 单线程模型, Reactor 多线程模型, 主从Reactor 多线程模型。
Reactor 单线程模型:
Reactor 单线程模型,指的是所有的IO 操作都在同一个NIO 线程上面完成,NIO 线程的职责如下:
- 作为NIO 服务端,接收客户端的TCP 连接;
- 作为NIO 客户端,向服务端发起TCP 连接;
- 读取通信对端的请求或者应答消息;
-
向通信对端发送消息请求或者应答消息。
由于Reactor 模式使用的是异步非阻塞IO,所有的IO 操作都不会导致阻塞,理论上一个线程可以独立处理所有IO 相关的操作。从架构层面看,一个NIO 线程确实可以完成其承担的职责。例如,通过Acceptor 接收客户端的TCP 连接请求消息,链路建立成功之后,通过Dispatch 将对应的ByteBuffer派发到指定的Handler 上进行消息解码。用户Handler 可以通过NIO 线程将消息发送给客户端。
Reactor 多线程模型
Rector 多线程模型与单线程模型最大的区别就是有一组NIO 线程处理IO 操作。 有专门一个NIO 线程-Acceptor 线程用于监听服务端,接收客户端的TCP 连接请求; 网络IO 操作-读、写等由一个NIO 线程池负责,线程池可以采用标准的JDK 线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO 线程负责消息的读取、解码、编码和发送;
主从Reactor多线程模型
服务端用于接收客户端连接的不再是个1 个单独的NIO 线程,而是一个独立的NIO 线程池。
Acceptor 接收到客户端TCP 连接请求处理完成后(可能包含接入认证等),将新创建的
SocketChannel 注册到IO 线程池(sub reactor 线程池)的某个IO 线程上,由它负责
SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor 线程池的IO 线程上,由IO 线程负责后续的IO 操作。
无锁设计、线程锁定
Netty 采用了串行无锁化设计,在IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU 利用率不高,并发程度不够。但是,通过调整NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
Netty 的NioEventLoop 读取到消息之后,直接调用ChannelPipeline 的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop 调用到用户的Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
高性能的序列化框架
Netty 默认提供了对Google Protobuf 的支持,通过扩展Netty 的编解码接口,用户可以实现其它的高性能序列化框架,例如Thrift 的压缩二进制编解码框架。
- SO_RCVBUF 和SO_SNDBUF:通常建议值为128K 或者256K。
小包封大包,防止网络阻塞 - SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法。
软中断 Hash 值和 CPU 绑定 - 软中断:开启RPS 后可以实现软中断,提升网络吞吐量。RPS 根据数据包的源地址,目的地址以及目的和源端口,计算出一个hash 值,然后根据这个hash 值来选择软中断运行的cpu,从上层来看,也就是说将每个连接和cpu 绑定,并通过这个hash 值,来均衡软中断在多个cpu 上,提升网络并行处理性能。