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