性能测试
BIO -- Blocking IO 即阻塞式IO
NIO -- Non-Blocking IO, 即非阻塞式IO或异步IO
性能 -- 所谓的性能是指服务器响应客户端的能力,对于服务器我们通常用并发客户连接数+系统响应时间来衡量服务器性能,例如,我们说这个服务器在10000个并发下响应时间是100ms,就是高性能,而另一个服务器在10个并发下响应时间是500ms,性能一般。所以提升性能就是提升服务器的并发处理能力,和缩短系统的响应时间。
测试方法
用同一个Java Socket Client 分别调用用BIO和NIO实现的Socket Server, 观察其建立一个Socket (TCP Connection)所需要的时间,从而计算出Server吞吐量TPS。
之所以可以用Connection建立时间来计算TPS,而不考虑业务逻辑运行时间,是因为这里的业务逻辑很简单,只是Echo回从client传过来的字符,所消耗时间可以忽略不计。
注意: 在现实场景中,业务逻辑会比较复杂,TPS的计算必须综合考虑IO时间+业务逻辑执行时间+多线程并行运行情况 等因素的影响。
测试类
1. Java Socket Client
public class PlainEchoClient { public static void main(String args[]) throws Exception { for (int i = 0; i < 20; i++) { startClientThread(); } } private static void startClientThread() throws UnknownHostException, IOException { Thread t = new Thread(new Runnable() { @Override public void run() { try { startClient(); } catch (Exception e) { e.printStackTrace(); } } }); t.start(); } private static void startClient() throws UnknownHostException, IOException { long beforeTime = System.nanoTime(); String host = "127.0.0.1"; int port = 8086; Socket client = new Socket(host, port); // 建立连接后就可以往服务端写数据了 Writer writer = new OutputStreamWriter(client.getOutputStream()); writer.write("Hello Server."); writer.flush(); // 写完以后进行读操作 Reader reader = new InputStreamReader(client.getInputStream()); char chars[] = new char[64];// 假设所接收字符不超过64位,just for demo int len = reader.read(chars); StringBuffer sb = new StringBuffer(); sb.append(new String(chars, 0, len)); System.out.println("From server: " + sb); writer.close(); reader.close(); client.close(); System.out.println("Client use time: " + (System.nanoTime() - beforeTime) + " ns"); } }
2. IO Socket Server
这个Socket Server模拟的是我们经常使用的thread-per-connection模式, Tomcat,JBoss等Web Container都是这种方式。
public class PlainEchoServer { private static final ExecutorService executorPool = Executors.newFixedThreadPool(5); private static class Handler implements Runnable{ private Socket clientSocket; public Handler(Socket clientSocket){ this.clientSocket = clientSocket; } @Override public void run() { try { BufferedReader reader = new BufferedReader( new InputStreamReader( clientSocket.getInputStream())); PrintWriter writer = new PrintWriter( clientSocket.getOutputStream(), true); char chars[] = new char[64]; int len = reader.read(chars); StringBuffer sb = new StringBuffer(); sb.append(new String(chars, 0, len)); System.out.println("From client: " + sb); writer.write(sb.toString()); writer.flush(); } catch (IOException e) { e.printStackTrace(); try { clientSocket.close(); } catch (IOException ex) { // ignore on close } } } } public void serve(int port) throws IOException { final ServerSocket socket = new ServerSocket(port); try { while (true) { long beforeTime = System.nanoTime(); final Socket clientSocket = socket.accept(); System.out.println("Establish connection time: "+ (System.nanoTime()-beforeTime)+" ns"); executorPool.execute(new Handler(clientSocket)); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException{ PlainEchoServer server = new PlainEchoServer(); server.serve(8086); } }
3. NIO Socket Server
public class PlainNioEchoServer { public void serve(int port) throws IOException { ServerSocketChannel serverChannel = ServerSocketChannel.open(); ServerSocket ss = serverChannel.socket(); InetSocketAddress address = new InetSocketAddress(port); ss.bind(address); // #1 serverChannel.configureBlocking(false); Selector selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); // #2 while (true) { try { selector.select(); // #3 } catch (IOException ex) { ex.printStackTrace(); // handle in a proper way break; } Set readyKeys = selector.selectedKeys(); // #4 Iterator iterator = readyKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = (SelectionKey) iterator.next(); try { if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key .channel(); long beforeTime = System.nanoTime(); SocketChannel client = server.accept(); // #6 System.out.println("Accept connection time: "+ (System.nanoTime()-beforeTime)+" ns"); if (client == null){//Check if socketChannel has been created, it could be null, because it's non-blocking continue; } client.configureBlocking(false); client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, ByteBuffer.allocate(100)); } if (key.isReadable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer output = (ByteBuffer) key.attachment(); client.read(output); } if (key.isWritable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer output = (ByteBuffer) key.attachment(); output.flip(); client.write(output); output.compact(); } } catch (IOException ex) { key.cancel(); try { key.channel().close(); } catch (IOException cex) { } } iterator.remove(); // #5 } } } public static void main(String[] args) throws IOException{ PlainNioEchoServer server = new PlainNioEchoServer(); server.serve(8086); } }
测试结果
Socket Client <——> IO Socket Server
Establish connection time: 2183277 ns
Establish connection time: 1523264 ns
Establish connection time: 1430883 ns
Establish connection time: 1523264 ns
Establish connection time: 1430883 ns
平均耗费时间大概是1.5 ms,TPS 大概是600
Socket Client <——> NIO Socket Server
Accept connection time: 138059 ns
Accept connection time: 132927 ns
Accept connection time: 132413 ns
Accept connection time: 132927 ns
Accept connection time: 132413 ns
平均耗费时间大概是0.15 ms,TPS 大概是6000
从测试结果可以看出,NIO的接受请求的速率大概是IO的十倍。
NIO还是BIO
在探讨在什么场景下使用BIO,什么场景下使用NIO之前,让我们先看一下在两种不同IO模型下,实现的服务器有什么不同。
BIO Server
通常采用的是request-per-thread模式,用一个Acceptor线程负责接收TCP连接请求,并建立链路(这是一个典型的网络IO,是非常耗时的操作),然后将请求dispatch给负责业务逻辑处理的线程池,业务逻辑线程从inputStream中读取数据,进行业务处理,最后将处理结果写入outputStream,自此,一个Transaction完成。
Acceptor线程是服务的入口,任何发生在其上面的堵塞操作,都将严重影响Server性能,假设建立一个TCP连接需要4ms,无论你后面的业务处理有多快,因为Acceptor的堵塞,这个Server最多每秒钟只能接受250个请求。而NIO则是另外一番风景,因为所有的IO操作都是非堵塞的,毫无疑问,Acceptor可以接受更大的并发量,并能最大限度的利用CPU和硬件资源处理这些请求。
BIO通信模型图
BIO序列图
NIO Server
如下图所示,在NIO Server中,所有的IO操作都是异步非堵塞的,Acceptor的工作变的非常轻量,即将IO操作分派给IO线程池,在收到IO操作完成的消息通知时,指派业务逻辑线程池去完成业务逻辑处理,因为所有的耗时工作都是异步的,使得Acceptor可以以非常快的速度接收请求,10W每秒是完全有可能的。
10W/S可能是没有考虑业务处理时间,考虑到业务时间,现实场景中,普通服务器可能很难做到10W TPS,为什么这么说呢?试想下,假设一个业务处理需要500ms,而业务线程池中只有50个线程,假设其它耗时忽略不计,50个线程满负载运行,在50个并发下,大家都很happy,所有的Client都能在500ms后获得响应. 在100个并发下,因为只有50个线程,当50个请求被处理时,另50个请求只能处在等待状态直到有可用线程为止。也就是说,理想情况下50个请求会在500ms返回,另50个可能会在1000ms返回。以此类推,若是10000个并发,最慢的50个请求需要100S才能返回。
以上做法是为线程池预设50个线程,这是相对保守的一种做法,其好处是不管有多少个并发请求,系统只有这么多资源(50个线程)提供服务,是一种时间换空间的做法,也许有的客户会等很长时间,甚至超时,但是服务器的运行是平稳的。 还有一种比较激进的线程池模型是类似Netty里推荐的弹性线程池,就是没有给线程池制定一个线程上线,而是根据需要,弹性的增减线程数量,这种做法的好处是,并发量加大时,系统会创建更多的线程以缩短响应时间,缺点是到达一个极限时,系统可能会因为资源耗尽(CPU 100%或者Out of Memory)而down机。
所以可以这样说,NIO极大的提升了服务器接受并发请求的能力,而服务器性能还是要取决于业务处理时间和业务线程池模型。
NIO序列图
什么时候使用BIO?
1. 低负载、低并发的应用程序可以选择同步阻塞IO以降低编程复杂度。
2. 业务逻辑耗时过长,使得NIO节省的时间显得微不足道。
什么时候使用NIO?
1. 对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。
2. 业务逻辑简单,处理时间短,例如网络聊天室,网络游戏等
参考: