Java异步NIO框架Netty实现高性能高并发

1. 背景

1.1. 惊人的性能数据

近期一个圈内朋友通过私信告诉我,通过使用Netty4 + Thrift压缩二进制编解码技术,他们实现了10W TPS(1K的复杂POJO对象)的跨节点远程服务调用。相比于传统基于Java序列化+BIO(同步堵塞IO)的通信框架。性能提升了8倍多。

其实,我对这个数据并不感到吃惊,依据我5年多的NIO编程经验。通过选择合适的NIO框架,加上高性能的压缩二进制编解码技术,精心的设计Reactor线程模型,达到上述性能指标是全然有可能的。

以下我们就一起来看下Netty是怎样支持10W TPS的跨节点远程服务调用的,在正式開始解说之前,我们先简介下Netty。

1.2. Netty基础入门

 

Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架。Netty的全部IO操作都是异步非堵塞的,通过Future-Listener机制。用户能够方便的主动获取或者通过通知机制获得IO操作结果。

作为当前最流行的NIO框架。Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。

2. Netty高性能之道

2.1. RPC调用的性能模型分析

2.1.1. 传统RPC调用性能差的三宗罪

网络传输方式问题:传统的RPC框架或者基于RMI等方式的远程服务(过程)调用採用了同步堵塞IO。当client的并发压力或者网络时延增大之后。同步堵塞IO会因为频繁的wait导致IO线程常常性的堵塞。因为线程无法高效的工作,IO处理能力自然下降。

 

以下,我们通过BIO通信模型图看下BIO通信的弊端:

图2-1 BIO通信模型图

採用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听client的连接。接收到client连接之后为client连接创建一个新的线程处理请求消息,处理完毕之后,返回应答消息给client,线程销毁,这就是典型的一请求一应答模型。该架构最大的问题就是不具备弹性伸缩能力,当并发訪问量添加后,服务端的线程个数和并发訪问数成线性正比,因为线程是JAVA虚拟机很宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降。随着并发量的继续添加。可能会发生句柄溢出、线程堆栈溢出等问题,并导致server终于宕机。

序列化方式问题:Java序列化存在例如以下几个典型问题:

1) Java序列化机制是Java内部的一种对象编解码技术,无法跨语言使用;比如对于异构系统之间的对接,Java序列化后的码流须要可以通过其他语言反序列化成原始对象(副本),眼下非常难支持。

2) 相比于其他开源的序列化框架,Java序列化后的码流太大,不管是网络传输还是持久化到磁盘,都会导致额外的资源占用;

3) 序列化性能差(CPU资源占用高)。

线程模型问题:因为採用同步堵塞IO,这会导致每一个TCP连接都占用1个线程,因为线程资源是JVM虚拟机很宝贵的资源,当IO读写堵塞导致线程无法及时释放时,会导致系统性能急剧下降,严重的甚至会导致虚拟机无法创建新的线程。

2.1.2. 高性能的三个主题

1) 传输:用什么样的通道将数据发送给对方。BIO、NIO或者AIO。IO模型在非常大程度上决定了框架的性能。

2) 协议:採用什么样的通信协议,HTTP或者内部私有协议。协议的选择不同。性能模型也不同。相比于公有协议,内部私有协议的性能通常能够被设计的更优。

3) 线程:数据报怎样读取?读取之后的编解码在哪个线程进行。编解码后的消息怎样派发,Reactor线程模型的不同,对性能的影响也很大。

图2-2 RPC调用性能三要素

2.2. Netty高性能之道

2.2.1. 异步非堵塞通信

在IO编程过程中,当须要同一时候处理多个client接入请求时,能够利用多线程或者IO多路复用技术进行处理。IO多路复用技术通过把多个IO的堵塞复用到同一个select的堵塞上,从而使得系统在单线程的情况下能够同一时候处理多个client请求。

与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小。系统不须要创建新的额外进程或者线程。也不须要维护这些进程和线程的执行,减少了系统的维护工作量,节省了系统资源。

JDK1.4提供了对非堵塞IO(NIO)的支持。JDK1.5_update10版本号使用epoll替代了传统的select/poll。极大的提升了NIO通信的性能。

JDK NIO通信模型例如以下所看到的:

图2-3 NIO的多路复用模型图

与Socket类和ServerSocket类相相应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。

这两种新增的通道都支持堵塞和非堵塞两种模式。

堵塞模式使用很easy,可是性能和可靠性都不好。非堵塞模式正好相反。开发者一般能够依据自己的须要来选择合适的模式,一般来说。低负载、低并发的应用程序能够选择同步堵塞IO以减少编程复杂度。可是对于高负载、高并发的网络应用,须要使用NIO的非堵塞模式进行开发。

Netty架构依照Reactor模式设计和实现。它的服务端通信序列图例如以下:

图2-3 NIO服务端通信序列图

client通信序列图例如以下:

图2-4 NIOclient通信序列图

Netty的IO线程NioEventLoop因为聚合了多路复用器Selector,能够同一时候并发处理成百上千个clientChannel,因为读写操作都是非堵塞的。这就能够充分提升IO线程的执行效率。避免因为频繁IO堵塞导致的线程挂起。

另外,因为Netty採用了异步通信模式,一个IO线程能够并发处理N个client连接和读写操作,这从根本上攻克了传统同步堵塞IO一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

2.2.2. 零拷贝

非常多用户都听说过Netty具有“零拷贝”功能。可是具体体如今哪里又说不清楚。本小节就具体对Netty的“零拷贝”功能进行解说。

Netty的“零拷贝”主要体如今例如以下三个方面:

1) Netty的接收和发送ByteBuffer採用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不须要进行字节缓冲区的二次拷贝。假设使用传统的堆内存(HEAP BUFFERS)进行Socket读写。JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。

相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

2) Netty提供了组合Buffer对象,能够聚合多个ByteBuffer对象,用户能够像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。

3) Netty的文件传输採用了transferTo方法,它能够直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

以下。我们对上述三种“零拷贝”进行说明,先看Netty 接收Buffer的创建:

图2-5 异步消息读取“零拷贝”

每循环读取一次消息。就通过ByteBufAllocator的ioBuffer方法获取ByteBuf对象,以下继续看它的接口定义:

图2-6 ByteBufAllocator 通过ioBuffer分配堆外内存

当进行Socket IO读写的时候,为了避免从堆内存拷贝一份副本到直接内存。Netty的ByteBuf分配器直接创建非堆内存避免缓冲区的二次拷贝,通过“零拷贝”来提升读写性能。

以下我们继续看另外一种“零拷贝”的实现CompositeByteBuf,它对外将多个ByteBuf封装成一个ByteBuf。对外提供统一封装后的ByteBuf接口,它的类定义例如以下:

图2-7 CompositeByteBuf类继承关系

通过继承关系我们能够看出CompositeByteBuf实际就是个ByteBuf的包装器。它将多个ByteBuf组合成一个集合,然后对外提供统一的ByteBuf接口,相关定义例如以下:

图2-8 CompositeByteBuf类定义

加入ByteBuf,不须要做内存拷贝。相关代码例如以下:

图2-9 新增ByteBuf的“零拷贝”

最后,我们看下文件传输的“零拷贝”:

图2-10 文件传输“零拷贝”

Netty文件传输DefaultFileRegion通过transferTo方法将文件发送到目标Channel中。以下重点看FileChannel的transferTo方法。它的API DOC说明例如以下:

图2-11 文件传输 “零拷贝”

对于非常多操作系统它直接将文件缓冲区的内容发送到目标Channel中,而不须要通过拷贝的方式,这是一种更加高效的传输方式。它实现了文件传输的“零拷贝”。

2.2.3. 内存池

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个很轻量级的工作。

可是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收。是一件耗时的操作。为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。以下我们一起看下Netty ByteBuf的实现:

图2-12 内存池ByteBuf

Netty提供了多种内存管理策略,通过在启动辅助类中配置相关參数。能够实现差异化的定制。

以下通过性能測试,我们看下基于内存池循环利用的ByteBuf和普通ByteBuf的性能差异。

用例一,使用内存池分配器创建直接内存缓冲区:

图2-13 基于内存池的非堆内存缓冲区測试用例

用例二,使用非堆内存分配器创建的直接内存缓冲区:

图2-14 基于非内存池创建的非堆内存缓冲区測试用例

各运行300万次,性能对照结果例如以下所看到的:

图2-15 内存池和非内存池缓冲区写入性能对照

性能測试表明,採用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。

以下我们一起简单分析下Netty内存池的内存分配:

猜你喜欢

转载自www.cnblogs.com/abc1168/p/9705830.html