Java NIO 与IO 性能对比分析

2021-04-23 05:51赵思远
软件导刊 2021年4期
关键词:占用率缓冲区线程

赵思远

(北京工业大学软件学院,北京 100124)

0 引言

随着互联网的蓬勃发展,网络用户激增[1],很多应用都服务于亿级以上数量级的用户。如此庞大的用户基数导致在高峰时期可达到千万以上的并发访问量,如何处理高并发流量,以保证应用能正常使用,成为企业迫切需要解决的问题[2]。

Java 是Web 应用中广泛采用的一种语言[3],JDK 中提供了NIO(也称为New IO),NIO 为非阻塞IO[4],即IO 过程不会阻塞程序运行,使得CPU 不会因为等待IO 而白白浪费计算资源。使用NIO 编程,并采用适当的模型,可以充分发挥CPU 的性能,提高服务器的并发处理能力。

1 相关工作

罗振兴等[5]在实践的基础上分析Java NIO 的特性,采用事件驱动、非阻塞的IO 多路复用、多线程[6]、安全Cookies、SSL、高速缓存等多种技术,设计并实现了构架在PKI上,基于角色访问控制策略的安全访问控制服务器,但其研究重点在于应用系统的整体设计与实现,对其中的Java服务器及其模型并没有进行清晰、明确的描述;袁劲松等[7]对网络应用中的阻塞通信与非阻塞通信工作机制及实现等问题进行研究与探讨,提出系统实现阻塞与非阻塞通信的方法和步骤,分别给出基于阻塞与非阻塞IO 开发高性能网络应用程序的具体实例,但该研究局限在代码层面,且仅实现了基本的非阻塞服务器,对于高并发、长连接等实际应用中会出现的情况未进行讨论;钱宇虹[8]对java.io包进行简要介绍,指出导致I/O 性能低下的根本原因,提出改进Java IO 性能的策略,但该策略仅针对Java 的传统IO方式,未考虑到在NIO 上的应用;杨帆[9]列举Java 中多种不同的文件复制编程方式,对比其编程复杂程度及执行效率,为编程人员提供参考及编程建议,但其没有对语言底层机制进行分析,且只对100M 文件进行了复制测试,没有对服务器模型及其性能进行讨论。

在高并发服务器领域,迟殿委[10]针对当前Web 应用请求高并发导致响应时间过长,影响用户体验的问题,提出Servlet 异步处理技术,对传统单实例的Servlet 与具有异步特性的Servlet 进行对比分析,并通过JMeter 模拟多线程、高并发访问两种Servlet,从平均响应时间和吞吐量来看,具有异步特性的Servlet 在高并发压力下表现出色;刘俊汐等[11]通过在Linux 服务端使用select 函数管理所有套接字,实现了单线程并发服务器;李明等[12]在socket 编程的基础上,使用epoll 机制和线程池技术[13]设计并实现了一个高并发服务器;陈强等[14]提出使用Netty 开发物联网应用服务器的方法;Jie[15]采用就绪通知机制处理网络流,使用适当的数据结构处理IO 缓冲区,并使用优先级队列[16]实现计时器管理模块,通过适当地组合这些技术,显著提高了SSL VPN 服务器程序的性能。

Java NIO 技术有效解决了CPU 等待IO 时产生的计算资源浪费问题,使得单一线程也能同时处理多个连接[17]。单线程NIO 模型实现简单,但在同一时间只能处理一个IO 事件,无法响应其它用户请求,这在IO 数据量较大或网络条件较差等IO 耗时较多的环境下会严重影响应用性能。在使用NIO 的同时使用多线程技术,则可有效利用多核CPU 的能力提高应用性能。即使有一个CPU 核心在处理IO 任务,仍有其它核心可接受并处理用户请求。但这种编程模型较为复杂,对开发人员要求较高,同时这种按照模型编写的程序可读性及可扩展性较差,在实际生产开发过程中应避免这种情况。

为了解决原生Java NIO 模型带来的问题,业界提出MINA[18]、Netty[19]等框架对NIO 进行包装,提供统一的模型,并进行了一些优化。使用这些成熟的框架可简化NIO程序编写,大大提高应用的可维护性[20]。

2 多Reactor 模型

2.1 传统IO 模型

传统IO 方式是面向流的,数据以字节为单位在输入输出流中传输,且流是单向的,如图1 所示。

此外,IO 是阻塞式的,即Blocking IO,所以称之为BIO。当程序发出读写请求后,程序会进入阻塞状态,直到请求完成或中断,如图2 所示。

Fig.2 Blocking IO图2 阻塞式IO

基于传统IO 构建的网络应用为了同时处理多个连接,需要为每个连接创建一个专用线程进行读写操作,接受新连接的线程称为Acceptor,处理socket 的线程称为Handler,如图3 所示。

Fig.3 BIO server model图3 BIO 服务器模型

这种模型在并发量不高时可以很好地处理多个连接,然而一旦并发量增长,连接数增多,将导致服务器线程也随之增长。大量线程处于非活跃状态,却依然占用着系统资源,同时在大量线程中频繁切换也会带来很多额外的系统开销。当连接数继续增长,创建的线程数达到服务器极限,系统资源耗尽,将会导致整个系统卡死,大量连接得不到响应,直到服务器崩溃。因此,传统IO 模型无法处理高并发流量。

2.2 NIO 单线程模型

NIO 是面向缓冲区的,数据必须通过通道读取到缓冲区中才可以被程序使用,且通道是双向的,如图4 所示。

Fig.4 Buffer-oriented NIO图4 面向缓冲区的NIO

通过缓冲区一次读写多个字节可以大大减少数据源访问次数,从而提高执行速度。

同时,NIO 是非阻塞的,读请求在数据未准备好时会立刻返回,不会阻塞程序运行,如图5 所示。

Fig.5 Non-blocking IO图5 非阻塞式IO

NIO 允许SocketChannel 和ServerSocketChannel 被配置为非阻塞式,采用非阻塞模型可以使程序同时监控多个通道,而不会因某个通道陷入阻塞,这种特性使其十分适合应用于网络应用开发。

利用NIO 技术,单一线程可以同时监控多个通道,并循环处理其中已就绪的数据。实际上,系统已通过select、epoll 等方法为程序提供同时监控多个通道的能力,即IO多路复用。在Java NIO 中,将多个通道注册到Selector 中,即可在某个通道就绪时通过select 方法将其选择出来。单线程服务器模型如图6 所示。

Fig.6 NIO single-threaded server model图6 NIO 单线程服务器模型

NIO 单线程模型中所有操作都在主线程中进行。主线程中只有一个Selector,其监听所有通道,并处理全部的accept 和read 事件。完成全部请求所需时间为:

其中,T 为完成全部请求所需时间,N 为并发连接数,taccept为接受一个连接所需时间,tread为每次读取所需的平均时间,cread为每个连接读取次数。

若接受连接的时间为0.1 ms,每个连接进行两次读取,每次读取用时2 ms,则单线程模型处理10 000 个并发连接所需时间为41 s。

2.3 NIO 线程池模型

单线程模型使用一个线程串行地处理所有通道的连接与读取事件,而无法利用CPU 的多核性能。为此,服务器一般采用多线程模型,并通过线程池重用线程,以避免频繁地创建与销毁线程。NIO 线程池服务器模型如图7 所示。

Fig.7 NIO thread pool server model图7 NIO 线程池服务器模型

在NIO 线程池服务器模型中,主线程包含一个Seletor,监听所有通道,并处理accept 事件。当收到read 事件时,主线程将读写操作提交到线程池中执行。完成全部请求所需时间为:

其中,cthread为线程池线程数,一般设置为CPU 核心数,cost 为全部线程的切换总开销。若CPU 核心数为16,线程切换总开销为100ms,则线程池模型处理10 000 个并发连接所需时间为3.6s。

2.4 多Reactor 模型

以上单线程模型和线程池模型中均只使用一个Selector,即所有通道的accept 和read 事件都由一个Selector 进行处理。当大量通道频繁读写,且每次读写的数据量很小时,会导致Selector 线程负载很高,而其它只进行读写操作的线程负载很低。在某些实际的网络应用中,每个连接单次读取的数据量很少,所需的读取时间很短,但读取次数多,数据交换频繁,如网络游戏或大型物联网系统等。在这种场景下,线程池模型会频繁地切换线程,此时切换线程的开销便不能忽视。为解决这个问题,本文提出多Reactor 模型,如图8 所示。

多Reactor 服务器模型中包含一个主线程和一个线程池。主线程中的Selector 只监听ServerSocketChannel 的accept 事件,当连接到来时,会将SocketChannel 分发至线程池中的某个线程,所以主线程又称为分发器。线程池中每个线程也拥有自己的Selector,负责监听分配到该线程SocketChannel 的read 事件。

多Reactor 服务器模型中包含多个Selector,将通道平均分配至多个线程,缓解了主线程的分发压力。通道被分配给工作线程后不会再移动,从而避免了频繁切换线程的开销,且每个工作线程内部都类似于简单、高效的NIO 单线程模型。因此,多Reactor 服务器在保留多线程服务器优势的同时,可以降低资源消耗。

Fig.8 Multi-Reactor server model图8 多Reactor 服务器模型

3 性能测试及结果

3.1 文件读取性能测试

文件读取性能测试的目的在于探究面向流的IO 和面向缓冲区的NIO 在读取数据方面的性能差异。

实验使用多种方式对不同大小的文件进行读取与复制,文件大小分别为1KB、1MB、10MB、100MB、500MB 和1GB。读取方式包括无缓冲IO、单字节读取的缓冲IO、储存到数组的缓冲IO、NIO、直接缓冲区NIO,复制方式除以上方式外,还包括双映射缓冲区、单映射缓冲区以及transferTo。记录操作所需时间,每个测试执行8 次,记录后5次平均值作为最终结果,单位为ms。读取测试结果如表1所示。

Table 1 Read test results表1 读取测试结果

复制测试结果如表2 所示。

Table 2 Copy test results表2 复制测试结果

测试结果显示,使用缓冲区的文件读取无论是IO 还是NIO,不会有太大的性能差异,使用直接缓冲区的NIO比普通NIO 速度稍快。文件复制测试结果也符合该结论,而且使用单映射缓冲区相较于双映射缓冲区可减少一半的时间开销。

直接缓冲区也称为映射缓冲区,其不在用户空间内,而是系统内存上的一块区域。通过直接缓冲区,用户可直接对该部分内存进行操作。通常程序读取数据需要将数据读取到内核,再复制到用户空间,如图9 所示。

Fig.9 Read data from file图9 从文件中读取数据

使用直接缓冲区,不需要将数据从内核空间复制到用户空间,即零拷贝,这在进行大文件复制等操作时具有显著优势,如图10 所示。

Fig.10 Copy files using direct buffer图10 使用直接缓冲区复制文件

3.2 网络通信性能测试

网络通信性能测试的目的在于探究传统IO 服务器模型与多种NIO 服务器模型之间在处理高并发网络请求方面的性能差异。

实验使用多种模型构建echo 服务器,包括BIO 多线程、BIO 线程池、NIO 单线程、NIO 线程池、NIO 多Reactor和Netty。使用Jmeter[21]对服务器进行分布式压力测试,先向服务器发送“hello”,延迟1s 后再发送“bye”,最后关闭连接。设置Jmeter 线程数控制并发级别,测试持续时间为120s,使用htop 和visualVM 监控服务器运行状态,记录服务器资源使用情况和请求响应情况。测试机器全部为云主机,为防止客户机产生性能瓶颈,将服务器配置为2 核8GB 内存,客户机配置为4 核8GB 内存。

Table 3 BIO multi-thread model表3 BIO 多线程模型

由表3 结果可见,服务器的线程数随并发数增长,但在4 000 并发时,线程数并没有达到4 000,可见服务器能创建的线程数已达到上限。而且TPS 在4 000 并发时与2 000 并发时差距不大,说明服务器在2 000 并发时就已基本到达了性能瓶颈。

Table 4 NIO single-thread server表4 NIO 单线程服务器

由测试结果(见表4)可见,与BIO 服务器相比,NIO 服务器在资源占用、请求响应时间和TPS 上都具有优势。

以4 000 并发为例,BIO 多线程服务器有3 002 个线程,CPU 占用率达到112%,完成“hello”请求306 322 个,平均响应时间为573 ms,TPS 为1 826,与2 000 并发时差别不大,已达到性能瓶颈;NIO 单线程服务器只有53 个线程,CPU 占用率为17.7%,完成“hello”请求477 970 个,平均响应时间为0.33 ms,TPS 为3 977。可见NIO 单线程服务器用更少的资源完成了更多请求,而且响应时间极短。

通过横向对比发现,每秒完成数随着并发数而增长,在10 000 并发时,TPS 也接近10 000,且CPU 使用率只有50.80%,说明还没有到达性能瓶颈,NIO 单线程服务器能支撑10 000 以上的并发。由此可见,NIO 服务器与BIO 服务器相比具有绝对优势。

设定并发级别为10 000,多种NIO 服务器模型对比测试结果如表5 所示。

Fig.11 TPS of NIO and BIO under different concurrency levels图11 不同并发级别下NIO 与BIO 的TPS

Table 5 Comparison test results of multiple NIO models under 10 000 concurrent表5 10 000 并发下多种NIO 模型对比测试结果

测试结果显示,在10 000 并发时各模型的处理能力相近,TPS 均在9 800 附近,表明10 000 并发没有达到NIO服务器的性能瓶颈,在CPU 占用率上出现的差异表明不同模型在相同压力下会产生不同的系统负载。

由于该测试在内网环境下进行,且读取的数据量较少,所以每次读取耗时很短,单线程模型不会因读取数据而出现显著的阻塞情况,因此单线程模型的请求完成数并未明显低于多线程模型。此外,由于单线程模型没有线程切换的额外开销,所以在完成相同数量请求的情况下,系统负载最低。

与单线程模型相比,线程池模型的CPU 占用率较高,TPS 反而更低,这是因为本测试没有网络速度慢和单次读写数据过大等IO 瓶颈,线程池的优势未得到发挥,反而因频繁切换线程而引入了额外的系统开销。

本文提出的多Reactor 模型相较于原有的NIO 单线程模型和线程池模型有较大改进。首先,CPU 占用率与线程池模型相比由90.70% 降低到68.50%,仅略高于单线程模型;其次,平均响应时间和TPS 与单线程模型相比均有提升,同时避免了单线程模型会出现IO 操作阻塞其它连接的情况。这是由于多Reactor 模型的每个工作线程都相当于一个NIO 单线程服务器,其内部不会发生线程切换,所以效率很高,从整体来看是多个服务器并行,充分利用多核CPU 提高并发性能。

4 结语

在大文件读取方面,面向缓冲区的NIO 与使用缓冲区的传统流IO 相比性能差距不大;在大文件复制方面,通过使用直接缓冲区可以实现“零拷贝”,大大加快了复制速度;在网络服务器方面,NIO 在高并发场景下与传统IO 相比具有资源占用率低、响应速度快、吞吐量高等特点。本文设计的多Reactor 服务器模型既避免了单线程模型读写操作阻塞其它连接的问题,提高了并发性能,又避免了线程池模型频繁切换线程带来的额外开销,降低了系统负载。利用设计良好的NIO 服务器模型,可适应各种应用场景。此外,还可利用成熟的框架降低开发难度,快速开发实现高性能服务器。

猜你喜欢
占用率缓冲区线程
降低CE设备子接口占用率的研究与应用
嫩江重要省界缓冲区水质单因子评价法研究
浅谈linux多线程协作
解析交换机CPU占用率
基于排队论的区域路内停车最优泊位占用率研究
关键链技术缓冲区的确定方法研究
基于上下文定界的Fork/Join并行性的并发程序可达性分析*
阿朗CDMA寻呼信道瘦身增效优化
Linux线程实现技术研究
地理信息系统绘图缓冲区技术设计与实现