【导读】传统RPC性能差的原因有三个,一是网络传输方式是同步阻塞的,二是Java原生序列化性能差,无法跨语言使用,序列化之后体积大等,三是线程模型会占用大量系统资源。所以今天来看以下Netty的高性能是如何建立的?
IO通信的三原则:
1、传输:用什么样的通道发送数据,I/O模型在很大程度上决定了通信的性能。
2、协议:协议的选择不同,性能也不同。相比于公有协议,内部私有协议的性能通常往往更佳。
3、线程:数据报如何读取,编解码在哪个线程执行,Reactor线程模型的不同,性能也不同。
Netty高性能之道:
一、异步非阻塞通信
I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求,与传统的BIO相比,多路复用的最大优势就是系统开销小,无需创建额外的线程。
NIO有阻塞和非阻塞模式,一般来说,低负载,低并发可以选择阻塞模式降低复杂度,高负载、高并发需选择非阻塞模式来撑起系统的性能。
二、高效的Reactor线程模型
常用的Reactor线程模式有三种,分别是:
1、Reactor单线程模型
2、Reactor多线程模型
3、主从Reactor多线程模型
(1)Reactor单线程模型
NioEventLoopGroup group = new NioEventLoopGroup(1);ServerBootstrap server = new ServerBootstrap();server.group(group);
所有的IO操作都在同一个NIO线程上完成,NIO线程职责是接收或发起TCP连接,读取或发送消息。
由于Reactor模式采用的是异步非阻塞IO,所有的IO操作都不会导致阻塞,理论上一个线程就可以独立处理所有IO相关的操作。
此模式不适用于高并发、高负载的场景,原因如下:
1、一个NIO线程同时处理成百上千的链路,性能上无法支撑
2、当负载过重时,处理速度将会变慢,会导致大量客户端连接超时,超时之后往往会进行重发,最终导致大量消息积压和处理超时。
3、可靠性问题,如果NIO线程进入了死循环,会导致不可用。
为了解决上述问题,就有了Reactor多线程模型
(2)Reactor多线程模型
NioEventLoopGroup acceptor = new NioEventLoopGroup(1);NioEventLoopGroup worker = new NioEventLoopGroup();ServerBootstrap server = new ServerBootstrap();server.group(acceptor,worker);
其于单线程模型最大的区别就是有一个Acceptor线程单独处理连接请求,有一组NIO线程负责I/O读写。
在绝大多数场景下,多线程模型可以满足性能需求,但是在高并发的情况下一个NIO线程处理连接请求可能会导致性能问题,为了解决此问题,产生了主从Reactor线程模型。
(3)主从Reactor线程模型
NioEventLoopGroup boss = new NioEventLoopGroup();NioEventLoopGroup worker = new NioEventLoopGroup();ServerBootstrap server = new ServerBootstrap();server.group(boss,worker);
服务端用于接收连接请求的不再是一个单独的NIO线程,而是一个独立的线程池。可解决一个服务端无法有效处理所有连接请求的问题。推荐使用该模型。
三、无锁的串行化设计
并行多线程处理可以提升系统的并发能力,但是,如果处理不当,会有锁竞争的问题。为了解决此问题,Netty通过串行化设计,也就是消息的处理在同一个线程内完成,不进行上下文切换,避免了多线程竞争和同步锁。也就是ChannelPipeline的设计。
Netty的NioEventLoop在读取到消息之后,直接调用ChannelPipeline的fireChannelRead方法,就会一直按照责任链模式执行下去,期间不进行线程切换。
四、高效的并发编程
主要体现在如下几点:
1、Volatile的大量、正确使用
2、CAS和原子类的广泛使用
3、线程安全容器的使用
4、通过读写锁提升并发性能。
五、高性能的序列化框架
影响序列化性能的关键因素如下:
1、序列化之后码流的大小(网络带宽的占用)
2、序列化与反序列化的性能(CPU资源的占用)
3、是否支持跨语言
Netty提供了对Google ProtoBuf、Thrift等优秀序列化框架的支持,之后篇章讲解用法。
六、零拷贝
何为零拷贝?数据都存储在JVM内存里面,如果需要进行数据传输,需要将JVM里面的数据拷贝一份到内核空间中,而NIO提供了在堆外内存存放数据的功能,可以直接进行传输,不需要进行拷贝。
Netty的零拷贝主要体现在三个方面:
(1)Netty的接受和发送都使用DirectBuffers,使用堆内直接内存进行socket读写,不需要进行字节缓冲区的二次拷贝。
(2)CompositeByteBuf,对外将多个ByteBuf封装成一个ByteBuf,对外提供统一的ByteBuf接口。是一个组合Buffer对象,避免了通过内存拷贝的方式将几个小Buffer合并成一个大Buffer。
(3)文件传输,Netty的文件传输类DefaultFileRegion通过transferTo方法将文件发送到目标Channel中。避免了传统通过循环write方式导致的内存拷贝问题。
七、内存池
JVM提供了对内存的分配和回收,但是堆内内存的分配和回收都是比较耗时的操作,为了重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。
通过内存池分配器创建直接内存缓冲区:
PooledByteBufAllocator.DEFAULT.directBuffer(1024);
八、灵活的TCP参数配置
合理的TCP参数在特定场景对于性能的提升可以有显著的效果,例如:
(1)SO_RCVBUF和SO_SNDBUF:通常建议设置为128kb或者256kb
(2)SO_TCPNODELAY:NAGLE算法通过将缓冲区内的小封包自动相连,阻止大量小封包的发送阻塞网络,从而提升网络应用效率,但对于时效强的场景应关闭此算法。
上述就是Netty高性能的基础,来自《Netty权威指南 第2版》一书。