關於I/O模型的文章比較多,參考多篇後理解上仍然不太滿意,終需本身整理一次,也是編寫高吞吐量高性能網絡接口模塊的基礎。這裏所說的主要針對網絡I/O,近幾年面對愈來愈大的用戶請求量,如何優化這些步驟直接影響接口用戶體驗。css
1、前言html
I/O模型有幾個名詞的解釋 (比較容易混淆):編程
阻塞與非阻塞:區別在於調用函數時,是否當即返回仍是讓線程等待。阻塞模型須要等待操做完成,而非阻塞模型則是當即返回(未準備好則返回一個錯誤碼)。後端
同步與非同步:區別在於網絡數據從內核拷貝到用戶空間時是否須要用戶線程參與等待。緩存
UNIX 網絡I/O模型分類有幾種 (參考 UNIX Network Programming Volume.1.3rd.Ed):網絡
1. Blocking I/O
2. Nonblocking I/O
3. I/O multiplexing (select and poll)
4. Signal driven I/O (SIGIO)
5. Asynchronous I/O (the POSIX aio_functions)異步
它們有兩個獨特的階段差異 :socket
1. 等待數據準備好. (阻塞/非阻塞的差別)tcp
2. 數據從內核複製到用戶空間. (同步/非同步的差別)ide
理解上面2個階段,對後面的解釋就很容易明白。
2、I/O 模型詳解
2.1 Blocking I/O
下圖以UDP服務端調用 recvfrom爲例描述線程等待過程 (TCP稍微複雜):
主處理線程一直阻塞到有用戶數據,而且數據從內核拷貝到用戶空間,也就是說主處理線程同一時間只能處理一個用戶請求:
僞代碼相似以下:
while (1) { Socket clientSock = serverSock.accept(); processRequest(clientSock); }
void processRequest(Socket clientSock)
{
read(...);
write(...);
}
在複雜的網絡環境中,常常出現一個「慢速」客戶端。也就是說這個客戶端數據到達很慢,在TCP網絡編程中,若是以此方式等待足夠的數據(根據協議定義),則會嚴重影響到其餘客戶端的處理等待時間。
由此進行改善的模型就是使用線程池,僞代碼以下:
while (1) { Socket clientSock = serverSock.accept(); threadPool.execute(new Task(processRequest(clientSock)); } void processRequest(Socket clientSock) { read(...); write(...); }
線程池模式使得主線程能處理更多的客戶端,N個客戶端使用M個線程(N:M),主線程不會被一個慢客戶端阻塞,可是處理能力仍然是比較有限的。
2.2 Nonblocking I/O
將socket設置爲非阻塞模式,告訴內核若是操做不能當即完成則返回一個錯誤碼而不是等待,描述圖以下:
以下圖,主線程一直嘗試調用網絡函數,直到數據準備好,該模型的缺陷就是"忙等待",CPU空轉浪費系統資源。此模式極少使用,在此僅用於介紹。
2.3 I/O multiplexing (多路複用)
Linux 中提供select/poll系統調用實現,線程阻塞在此方法上,監測多個socket fd(file descriptor)是否就緒,一旦有事件發生則順序掃描具體是哪一個就緒,但這樣作會比較費時,模型以下:
在Linux kernel 2.6+提供了epoll實現,使用驅動方式替代順序掃描,哪一個fd有事件發生就返回哪個(避免了select/poll的只要有一個發生就掃描所有找出是哪一個,新的內核也可能對這模式實現作了一些優化)。
在ORACLE JDK有以下源碼對內核作判斷:
package sun.nio.ch;
import ...
public class DefaultSelectorProvider {
public static SelectorProvider create() {
......
if ("Linux".equals(str1)) {
String str2 = (String)AccessController.doPrivileged(new GetPropertyAction("os.version"));
String[] arrayOfString = str2.split("\\.", 0);
if (arrayOfString.length >= 2) {
try {
int i = Integer.parseInt(arrayOfString[0]);
int j = Integer.parseInt(arrayOfString[1]);
if ((i > 2) || ((i == 2) && (j >= 6))) {
return new EPollSelectorProvider();
}
} catch (NumberFormatException localNumberFormatException) {}
}
}
return new PollSelectorProvider();
}
}
關於epoll的實現有專門文章詳解,在此不作細說,另一個epoll使用mmap內存映射避免內存複製損耗。
2.4 Asynchronous I/O (異步)
異步I/O告訴內核開始某個操做,在內核完成後(包括數據從內核拷貝到用戶空間)通知咱們,以下圖:
在ORACLE JDK 1.7+提供異步I/O的實現AsynchronousChannel,相對於原Selector實現I/O複用模式簡單不少。
在這幾種模型中,只有異步I/O是用戶線程不參與數據從內核拷貝到用戶空間這個過程。
2.5 幾種I/O模型的對比
以下圖,它們在 [等待數據] 和 [從內核複製數據到用戶空間] 的差別:
從上圖看出,只有Asynchronous I/O不參與內核數據複製。
3、實例舉例說明
3.1 針對BIO拒絕服務攻擊
BIO中Boss線程處理請求後交給鏈接池Worker處理,但線程池有限,能輕易致使服務異常。好比針對Tomcat默認配置進行HTTP慢速攻擊:
聲明一個Content-Length爲300的POST包,開啓300個線程請求,每一個請求中每秒發送1 byte。默認狀況下Tomcat的200個線程將爆滿,在持續攻擊的這300秒內,都沒法正常處理其餘正經常使用戶請求,形成服務異常。
改善方法:將鏈接器使用NIO處理,修改默認配置protocol爲Http11NioProtocol,而且增大合適的線程數,對此類攻擊有必定的緩解做用。
3.2 對外接口服務應用
若是使用Java開發對外接口服務,在對響應時延和吞吐量有必定要求的話,一般能夠考慮集成Netty (NIO),若是遵循Servlet標準可考慮集成Jetty使用NIO鏈接器處理。在協議方面,內部服務可選擇高效的私有協議和高壓縮比的組件(如protobuf / kryo等),對外服務可選擇HTTP+JSON。
WEB服務可考慮動靜分離,html/css/js/image等文件使用Nginx (sendfile提供更高效的數據傳輸方式)處理,業務數據才透到後端服務中,後端服務加入緩存等方式減小響應時延。
3.3 關於JDK中一些名詞解析
在JDK提供的NIO API與這裏介紹的NIO(Nonblocking I/O)模型是不一樣的概念,JDK在1.7以前提供的Selector基於select/poll、epoll(Linux kernel > 2.6)實現I/O複用技術的非阻塞IO,JDK1.7之後提供的NIO2.0 API (如AsynchronousServerSocketChannel) 纔是真正的異步I/O。
參考資料:
1. http://www.madwizard.org/programming/tutorials/netcpp/5
2. http://www.importnew.com/22019.html
3. 《UNIX Network Programming Volume.1.3rd.Edition》
4.《Netty權威指南》