一 摘要
在源码分析-Netty: 架构剖析中,我们介绍了Netty的逻辑架构,本篇将继续深入,从架构层面对Netty的高性能设计和关键代码进行分析,看Netty如何支撑高性能网络通信。
二 RPC调用模型分析
2.1 RPC调用-关键性能瓶颈
涉及到RPC框架的三大核心部分:网络传输方式、序列化、线程模型。接下来我们逐个分析:
2.1.1 网络传输方式
RPC采用的I/O模型。古老的RPC框架,或基于RMI等方式实现的远程过程调用,使用的是同步阻塞I/O(BIO),根据前面对几种I/O模型及演化过程的介绍,可知这通常是效率最低的一种。当客户端的并发压力或网络时延增大以后,同步阻塞I/O会由于频繁wait导致I/O线程多次阻塞,从而I/O处理能力下降。
2.1.2 序列化
Java序列化普遍存在着性能差的问题,列举如下:
1、Java序列化机制是语言专属(内部)的一种对象编解码技术,不能跨语言;
2、与其他开源序列化框架相比,Java序列化后的码流过大,网络传输或持久化磁盘时都会导致过多额外的资源占用;
3、序列化性能差,资源占用高(主要是CPU资源)
2.1.3 线程模型
基于Java实现的服务端,如果采用的是BIO通信模型,通常由一个独立的Acceptor线程来监听客户端链接;当接收到客户端连接请求时,会为其创建一个新的线程处理请求信息,并在处理完成并返回应答(response)消息后,线程销毁。这种一请求一应答的架构模型不具备弹性伸缩能力,当访问量增加时,服务端线程个数和并发请求数成线性增长,同时由于在Java虚拟机中,线程是非常宝贵的系统资源(创建线程、线程间切换时会造成大量开销),当线程数膨胀时系统性能会急剧下降,可能会导致句柄溢出甚至线程堆栈溢出问题,最终导致服务崩溃。
2.2 I/O通信要素
影响I/O性能的因素很多,主要有以下三个:
1、传输
就是I/O模型,BIO、NIO或AIO。
2、协议
通信协议,HTTP等公共协议还是私有协议。协议也直接影响性能。相对公有协议来说,内部私有协议通常可以被设计得性能更优。
3、线程模型
数据报文如何读取?读取后编码和解码怎样分配到线程?编解码后的消息怎样继续下发?
三 Netty的高性能之道
3.1 非阻塞I/O模型
Netty提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现,并且这两种都支持阻塞和非阻塞模式。
NioEventLoop由于聚合了Selector(多路复用器),可以同时并发处理上千个SocketChannel,这可以充分提升I/O线程的运行效率。避免频繁I/O阻塞导致的线程挂起。
3.2 高效的Reactor线程模型
常用的Reactor线程模型有Reactor单线程、Reactor多线程、主从Reactor多线程三种。几种模型的区别分别在于是否有一组NIO线程专门处理I/O操作,以及服务器用于接收客户端连接的是单线程还是线程池。相关内容可查看之前的文章及参考资料,这里暂时不做赘述。
Netty中可以通过在启动辅助类中创建不同的EventLoopGroup并设置参数,支持上述三种Reactor线程模型,以满足不同业务场景的性能诉求。
3.3 无锁化的串行设计
高并发场景,为了正确同步,锁成为常用的解决方案。但锁的使用不当时,会带来不必要的锁竞争,这会导致性能的急剧下降。串行化设计就是一种解决方案,尽可能避免/降低锁竞争带来的性能损耗。所谓串行化,就是消息的处理尽可能在同一个线程内完成,期间不做线程切换,这样就避免了多线程竞争和锁同步。Netty就在I/O线程内进行了串行设计。
通常的理解,串行化会带来CPU利用不高、并行度不够的问题,但实际上,Netty支持通过调整NIO线程池的参数,同时启动多个串行化的线程并运行,这种局部的无锁化串行线程设计在性能上可以优于多个工作线程模型。
Netty串行化设计工作原理如上图所示,涉及NioEventLoop、ChannelPipeline。NioEventLoop读取到消息之后,调用ChannelPipeline的fireChannelRead(Object msg)方法,期间如果用户不主动切换线程,那么就会一直由NioEventLoop调用Handler,不做线程切换。
3.4 高效的并发编程
源码分析-Netty: 并发编程的实践(二)中做过介绍,主要包括以下几点:
1)volatile的大量且正确使用
2)CAS和原子操作类的广泛使用
3)线程安全容器的使用
4)读写锁
3.5 高性能的序列化框架
Netty默认提供了对Google Protobuf的支持,通过扩展编解码接口,用户可以实现其他高性能序列化框架,例如Thrift。
序列化的框架除了Protobuf和Thrift之外还有很多,都旨在空间、性能等消耗上达到最优。影响序列化性能的主要因素有以下几个:
1)序列化后的码流大小——即网络带宽的占用
2)序列化&反序列化的性能——CPU资源占用
3)是否支持跨语言——异构系统对接和开发语言切换
3.6 零拷贝
Netty的零拷贝体现在三个方面:
1、接收和发送ByteBuffer采用DIRECT BUFFERS,也就是使用堆外内存进行Socket读写,不需要进行字节缓冲区的二次拷贝;
2、CompositeByteBuf,对外部将多个ByteBuf封装成一个ByteBuf,提供统一封装后的ByteBuf接口。CompositeByteBuf实际上就是ByteBuf的装饰器。
3、文件传输,Netty中的文件传输类DefaultFileRegion通过transferTo方法将文件发送到目标Channel中。
3.7 内存池
与线程池、连接池类似,都属于池化技术,只不过管理对象是内存。随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是比较轻量级的工作。但对Buffer(缓冲区),特别是堆外直接内存的分配和回收,是比较耗时的操作。为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制,即ByteBuf。
在Netty权威指南第2版中,对使用内存池的ByteBuf与不使用内存池的ByteBuf做了一个性能对比,性能提高达23倍之多。
内存池分配器创建直接内存缓冲,使用的是PooledByteBufAllocator,内存分配的关键代码:
代码语言:javascript复制 public ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
if (initialCapacity == 0 && maxCapacity == 0) {
return this.emptyBuf;
} else {
validate(initialCapacity, maxCapacity);
return this.newDirectBuffer(initialCapacity, maxCapacity);
}
}
this.newDirectBuffer是一个抽象方法,定义在AbstractByteBufAllocator中。两个子类PooledByteBufAllocator和UnpooledByteBufAllocator中分别实现了这个方法。
3.7.1 PooledByteBufAllocator
PooledByteBufAllocator的newDirectBuffer()方法:
代码语言:javascript复制 protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = (PoolThreadCache)this.threadCache.get();
PoolArena<ByteBuffer> directArena = cache.directArena;
Object buf;
if (directArena != null) {
buf = directArena.allocate(cache, initialCapacity, maxCapacity);
} else {
buf = PlatformDependent.hasUnsafe() ? UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) : new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return toLeakAwareBuffer((ByteBuf)buf);
}
可见,是从PoolThreadCache中获取内存区域PoolArena,并调用它的allocate()方法执行内存分配。
3.7.2 UnpooledByteBufAllocator
UnpooledByteBufAllocator中的方法实现:
代码语言:javascript复制protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
Object buf;
if (PlatformDependent.hasUnsafe()) {
buf = this.noCleaner ? new UnpooledByteBufAllocator.InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) : new UnpooledByteBufAllocator.InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
} else {
buf = new UnpooledByteBufAllocator.InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return (ByteBuf)(this.disableLeakDetector ? buf : toLeakAwareBuffer((ByteBuf)buf));
}
3.8 灵活的TCP参数配置能力
Netty中可以配置TCP参数以满足不同的业务场景。相关配置类:ChannelOption。
从上图可见,ChannelOption中定义了多个TCP参数配置,几个常用且对性能影响较大的参数如下:
1)SO_RCVBUF 和 SO_SNDBUF,TCP接收缓冲区的容量上限 和 TCP发送缓冲区的容量上限,通常建议设置为128KB 或 256KB;
2)SO_TCPNODELAY:NAGLE算法通过将缓冲区内的小封包自动相连,组成较大的包,阻止大量小包发送阻塞网络,以达到提高网络应用效率的目的。但实际应用时,由于对时延敏感,所以经常需要关闭NAGLE算法;