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。html

之因此能夠用Connection創建時間來計算TPS,而不考慮業務邏輯運行時間,是由於這裏的業務邏輯很簡單,只是Echo回從client傳過來的字符,所消耗時間能夠忽略不計。java


注意: 在現實場景中,業務邏輯會比較複雜,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

平均耗費時間大概是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

平均耗費時間大概是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. 業務邏輯簡單,處理時間短,例如網絡聊天室,網絡遊戲等

參考:


版權聲明:本文爲博主原創文章,未經博主容許不得轉載。併發

相關文章
相關標籤/搜索