755
技術社區[雲棲]
NIO vs. BIO
性能測試
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. 業務邏輯簡單,處理時間短,例如網絡聊天室,網絡遊戲等
參考:
最後更新:2017-04-03 08:26:24