java nio时间服务_性能优化篇-使用NIO提升服务性能

1、概述

在《性能优化篇-理论基础》中,我们知道了提升服务性能的两个思路,分别是提升服务并发能力和降低请求的响应时间(RT)。一个请求的RT包括两部分,等待时间和执行时间。本文就从降低服务的等待时间,分析NIO是如何提升服务的性能的。

在分析之前,我们看一下网络请求在服务端是如何被处理的,如下图所示,当一个服务端主机网卡收到网络包时,首先根据以太网包的MAC地址判断是否为自己的包,如果不是则丢弃。接下来,网络层会判断IP包中的IP地址与自己是否相同,如果相同则将数据交给上一层协议处理。然后,传输层的根据TCP包中的端口号确定具体的应用程序。最后应用程序根据应用层的协议解析数据,并处理数据。

在TCP/IP协议中,数据链路层由网卡负责,网络层和传输层由操作系统负责,应用层由各个应用程序负责。在操作系统中,为每个应用程序的监听端口都维护了两个队列,分别是未完成连接队列和已完成连接队列,因此在客户端和服务端完成TCP三次握手后,连接处于ESTABLISHED状态,操作系统会将其放入该应用程序的已完成连接队列的尾部,等待应用程序处理。

应用程序调用操作系统提供的套接字的accept()函数,从已完成连接队列的头部取出一个ESTABLISHED状态的连接,然后进行数据的读写操作。因此如果每当一个连接到达时,应用程序都能马上进行处理,则可以减少请求在队列中的等待时间,从而降低请求的响应时间。

2、Linux网络IO模型

服务端基本上使用Linux作为操作系统,各个应用程序的网络IO模型也是基于操作系统提供的IO模型,因此理解Linux的网络IO模型是十分必要的。

2.1、阻塞IO模型

在阻塞IO模型下,服务端应用程序读取客户端发送数据的过程如下图所示,应用程序在调用操作系统提供的recvfrom函数后,线程就会处于阻塞状态,直到数据准备完毕,返回数据结果。

2.2、非阻塞IO模型

在非阻塞IO模型下,服务端应用程序读取客户端发送数据的过程如下图所示,应用程序在调用操作系统提供的recvfrom函数后,如果数据没有准备好,线程会立刻返回,不会被阻塞,从而线程可以继续处理其他连接请求。但是由于非阻塞IO模型采用的是轮询机制,因此如果轮询间隔时间较长,会影响数据读取的即时性,如果间隔时间较短,则会浪费大量的CPU资源。

2.3、IO多路复用模型

在IO多路复用模型下,如下图所示,操作系统将一个或多个网络连接注册到一个selector上,服务端应用程序调用操作系统的select方法,只要selector上有满足可读条件的连接,则select方法就返回达到可读条件的连接,如果没有满足可读条件的连接,则select方法就阻塞,直到出现满足可读条件的连接。select方法返回可读条件的客户端连接后,应用程序再调用recvfrom方法读取数据。

2.4、信号驱动IO模型

在IO复用模型下,服务端应用程序读取客户端发送数据的过程如下图所示,应用程序在调用操作系统提供的sigaction函数后,线程会立刻返回,并得到一个SIGIO信号,从而线程可以继续处理其他连接请求。当内核空间数据准备好后,会通过回调SIGIO来通知应用程序,然后应用程序调用recvfrom方法读取数据。

2.5、异步IO模型

从前面的IO模型可知,网络数据读取到应用程序中分成了两部分操作,数据从网卡拷贝到内核空间和数据从内核空间拷贝到用户空间,前面的IO模型,都是在解决应用程序在数据从网卡拷贝到内核空间这个过程的阻塞问题,但是依然没有解决在数据从内核空间拷贝到用户空间这期间的阻塞问题,因此异步IO的目的就是实现在数据拷贝到用户空间完毕后,再通知应用程序直接使用数据,可以简单理解为下图模式。

3、Java的网络IO模型

目前,Java提供了三种IO模型,BIO、NIO和AIO,BIO模型采用的是同步阻塞模型,底层基于操作系统的阻塞IO模型,在JDk1.4版本开始提供NIO,底层基于操作系统的IO多路复用模型,在JDk1.7版本开始提供AIO,底层基于操作系统的异步IO模型。

3.1、BIO网络编程

BIO模型的代码demo如下所示:

public class BioServer {

public void start(int port) {

ServerSocket serverSocket;

try {

serverSocket = new ServerSocket(port);

while (true) {

Socket socket = serverSocket.accept();

new Thread(new ServerHandler(socket)).start();

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

class ServerHandler implements Runnable {

private Socket socket;

public ServerHandler(Socket socket) {

this.socket = socket;

}

@Override

public void run() {

try {

BufferedReader in =new BufferedReader(new InputStreamReader(socket.getInputStream()));

while (true) {

String body = in.readLine();

System.out.println(body);

if (body == null) {

break;

}

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

主线程执行Socket socket = serverSocket.accept();代码不断从已完成连接队列的头部取出一个ESTABLISHED状态的客户端连接,但是,由于BIO是同步阻塞模式,在读数据时线程会被阻塞,为了同时能够处理多个客户端连接,避免连接在队列中等待时间过长导致响应很慢,因此对每个客户端连接都必须新创建一个线程来处理其数据的读写,也就是代码中的newThread(newServerHandler(socket)).start();

但是线程频繁创建和销毁会增加CPU和内存的开销,并且线程创建太多,导致上下文切换严重,CPU花费在上下文切换上的时间甚至有可能超过处理实际业务的时间,也就是著名的C10K问题。在《Java基础篇-线程与线程池》中我们提到了线程池的优势,因此实际业务中代码会优化成如下形式。

public class BioServer {

private ThreadPoolExecutor poolExecutor;

public BioServer(ThreadPoolExecutor poolExecutor) {

int cpuCores = Runtime.getRuntime().availableProcessors();

this.poolExecutor = new ThreadPoolExecutor(cpuCores * 2, cpuCores * 4, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(9999));

}

public void start(int port) {

ServerSocket serverSocket;

try {

serverSocket = new ServerSocket(port);

while (true) {

Socket socket = serverSocket.accept();

poolExecutor.submit(new ServerHandler(socket));

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

虽然线程池能够解决线程频繁创建的问题,但是服务端的并发能力还是受限于线程数量,一旦客户端并发连接数过多,请求依然会进入线程池的阻塞队列中等待被处理,同样会出现响应慢的问题。

3.2、NIO网络编程

由于BIO模型始终无法提供高性能的服务端处理能力,因此JDK1.4版本开始提供了基于操作系统IO多路复用的NIO模型,并且在JDK1.5版本将selector的实现由poll优化为了epoll(select、poll、epoll是操作系统提供的IO多路复用的系统方法,epoll解决了select和poll存在的效率低问题),JDK称NIO为New IO,因为它是一种新的IO模型,而业界很多人称之为Non-Blocking IO(非阻塞IO),本文也称之为非阻塞IO。通过2.3可知,其实NIO主要是通过轮询注册到selector上的网络连接,只要存在可读/可写条件的连接就立刻返回,从而避免了BIO的阻塞问题。因为不存在阻塞(数据从内核空间拷贝到用户空间还是存在等待时间,但是因为是内存操作,所以很快),所以服务端线程就是在不停的执行CPU操作,属于CPU密集型,因此即使是单线程的服务端也可以并发处理大量的客户端请求。

可能有的读者还是不太能理解为什么NIO单线程就能处理大量的客户端请求,我们看一下以下这两个图。假设有6个客户端同时到达服务端,完成TCP三次握手后,放在了ESTABLISHED队列中,在BIO模型中,服务端从队列头部依次取出1~6个连接进行处理,如果连接1由于网络拥塞或者传输大文件,导致网卡需要等待数据包,最终线程不得不阻塞等待。但是此时,可能2~6的数据包都已经准备好了,就等待被应用程序处理了,却因为线程处理客户端1的被阻塞,从而使得2~6也不得不等着,严重增加了请求的等待时间,从而增加了请求响应时间。BIO模型请求处理示意图

在NIO模型中,所有的连接会在selector上注册,并有3个标记状态,A代表TCP握手完毕,连接建立成功,R代表数据从从网卡拷贝到内核完毕,满足可读条件,W代表上一次(如果没有,则忽略)数据包已经发送,满足可写条件。 如下图所示,虽然连接1先到来,但是连接1由于网络拥塞或者传输大文件,导致网卡需要等待数据包,因此其R状态未被标识达到可读条件,而连接3和连接6虽然都是随后到达的连接,但是由于数据包已经准备好,其R状态标识为可读。因为服务端不断轮询selector,只要有满足A、R、W条件的任何一个连接就返回做响应的处理,因此服务端会把3、6取出,并进行处理,因此服务端的线程基本上都在执行业务逻辑处理,而并不会受到网络IO的影响而阻塞。(图中示意的selector是linux的select/poll机制,这种机制会出现一个问题,当selector上注册了几十万的客户端连接时,而满足条件的可能只有1/10,业务前面几万个连接中都没有符合条件的,则前面几万次状态判断是无意义的,会浪费查询时间,因此Linux后面使用了epoll优化了此问题,即selector上注册的都是已经满足条件的连接,JDK1.5也将NIO升级为使用epoll命令了,本文只是为了方便描述)NIO模型请求处理示意图

Java NIO的demo代码如下所示:

//创建选择器Selector selector = Selector.open();

//启动应用程序,监听本机9999端口ServerSocketChannel serverSocket = ServerSocketChannel.open();

serverSocket.socket().bind(new InetSocketAddress("127.0.0.1", 9999));

serverSocket.configureBlocking(false);

//需要先向selector注册你要监听哪些条件的连接,告诉操作系统凡是需要我处理连接都把连接当前状态记录到selector上serverSocket.register(selector, SelectionKey.OP_ACCEPT);

while (true) {

//轮询selector,select方法会阻塞,直到seletor上有符合条件的连接 selector.select();

//如果此次轮询的时候,selector上有多个符合条件的连接,select方法会方法一个列表 Iterator selectorKeys = this.selector.selectedKeys().iterator();

while (selectorKeys.hasNext()) {

SelectionKey selectionKey = (SelectionKey) selectorKeys.next();

if (!selectionKey.isValid()) {

continue;

}

if (selectionKey.isAcceptable()) {

accept(selector, selectionKey);

}

if (selectionKey.isReadable()) {

read(selector, selectionKey);

}

if (selectionKey.isWritable()) {

write(selector, selectionKey,":hello Client, I am Server!");

}

}

}

private void accept(Selector selector, SelectionKey key) {

ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

SocketChannel clientChannel = serverSocketChannel.accept();

if (clientChannel == null) {

return;

}

clientChannel.configureBlocking(false);

clientChannel.register(selector, SelectionKey.OP_READ);

}

private void read(Selector selector, SelectionKey key) {

ByteBuffer buf = ByteBuffer.allocate(512);

SocketChannel clientChannel = (SocketChannel) key.channel();

//从通道里面读取数据到缓冲区并返回读取字节数 int count = clientChannel.read(buf);

String input = new String(buf.array()).trim();

System.out.println(Thread.currentThread().getName() + ": Client say " + input);

}

private void write(Selector selector, SelectionKey key, String message) {

ByteBuffer buf = ByteBuffer.allocate(512);

buf.put(message.getBytes());

SocketChannel clientChannel = (SocketChannel) key.channel();

if (clientChannel == null) {

return;

}

int length = 0;

while ((length = clientChannel.write(buf)) != 0) {

System.out.println("写入长度:" + length);

}

clientChannel.close();

}

3.3、AIO网络编程

class AioServer implements Runnable {

private AsynchronousServerSocketChannel serverSocketChannel;

@Override

public void run() {

try {

serverSocketChannel = AsynchronousServerSocketChannel.open();

serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 9999));

//accept是异步操作,因此传入一个CompletionHandler的实现类接收回调通知 serverSocketChannel.accept(this, new AcceptCompletionHandler());

System.out.println(Thread.currentThread().getName() + ": " + "start listen: 127.0.0.1:9999");

} catch (IOException e) {

e.printStackTrace();

}

}

}

class AcceptCompletionHandler implements CompletionHandler {

@Override

public void completed(AsynchronousSocketChannel channel, AioServer attachment) {

try {

//系统回调此方法,表示有新的客户端连接已经处于establish状态了 System.out.println(Thread.currentThread().getName() + ": " + "start process establish connection " +

channel.getRemoteAddress().toString());

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

//read是异步操作,因此传入一个CompletionHandler的实现类接收回调通知 channel.read(byteBuffer, byteBuffer, new AsyncServerHandler(channel));

//继续调用serverSocketChannel的accept方法,用以继续接收其他的客户端连接 attachment.serverSocketChannel.accept(attachment, this);

} catch (Exception e) {

e.printStackTrace();

}

}

@Override

public void failed(Throwable exc, AioServer attachment) {

exc.printStackTrace();

}

}

class AsyncServerHandler implements CompletionHandler {

private AsynchronousSocketChannel channel;

public AsyncServerHandler(AsynchronousSocketChannel channel) {

this.channel = channel;

}

@Override

public void completed(Integer result, ByteBuffer byteBuffer) {

// 读客户端数据 byteBuffer.flip();

String input = new String(byteBuffer.array()).trim();

System.out.println(Thread.currentThread().getName() + ": Client say " + input);

// 写数据到客户端 channel.write( ByteBuffer.wrap( "hi client, server received your message".getBytes() ) );

}

@Override

public void failed(Throwable exc, ByteBuffer attachment) {

exc.printStackTrace();

}

}

我们看一下打印的结果,可以看到每个步骤的线程都不一样,如下所示:

由于Linux对AIO的支持并不是十分稳定,并且对于服务端来说,处理的是大量的并发连接,使用IO多路复用机制已经能够很好的利用CPU,并且成熟稳定,因此业界目前并不推荐首选AIO作为服务端提升网络IO性能的IO模式。

4、Netty

通过第3节的分析可以知道,BIO模型是无法应对高并发的业务场景的,NIO模型通过IO多路复用机制,解决了线程阻塞问题,可以很好的应对高并发的业务场景。虽然NIO模型基本上可以认为是CPU密集型任务,但是在现代多核处理器的情况下,使用多线程才能更好的发挥多核优势,提升服务的并发能力。但是多线程编程涉及到并发编程,对技术人员的要求会更高。

其次,Java原生的NIO类库使用起来都比较繁琐,而且需要理解和掌握众多概念,也会增加NIO网络编程的开发难度,因此很多小型应用依然使用BIO模型。

最后,网络编程还涉及到对网络数据流的处理,半包、粘包、网络拥塞、丢包等问题处理,因此要编写一个健壮性和性能都很好的NIO服务端程序,需要掌握和理解各种网络知识。

Netty是业界最流行的NIO框架之一,不但解决了以上的所有问题,而且还提供了各种开箱即用的网络协议解析类,http、websocket、TLS等都全部支持,并且对Java原生的NIO进行了封装,使用非常简单。 Netty框架会在后面以序列篇的形式单独分析。

5、总结

Java的IO模型从BIO逐步演进到NIO、AIO,理解这些IO模型首选要理解操作系统的IO模型,因为Java本身是基于操作系统提供的IO模型。

目前NIO已经在各个大型网站中成熟使用,因此,如果你的服务面临着高并发的问题,可以考虑使用NIO提升服务的性能。如果你不熟悉多线程编程,不熟悉传输层TCP协议、应用层的http、websocket、TLS等协议,那么也可以基于Netty框架来快速构建一个NIO模型的服务端。框架使用虽然简单方便,但是掌握其核心原理和底层实现对于我们更好的进行调优和定位问题有极大帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值