BIO的改進方案
為了改進BIO一線程一連接模型,演進出了一種通過線程池或者消息隊列實現1個或者多個線程處理N個客戶端的模型,由于它的底層通信機制依然使用同步阻塞I/O,所以被稱為“偽異步”。
后端通過一個線程池來處理多個的請求,形成請求輸M:線程池最大線程數N的比例關系,其中M可以遠遠大于N。通過線程池可以靈活地調配線程資源,設置線程的最大值,防止由于海量并發接入導致線程耗盡。
當有新的客戶端接入時,將客戶端的Socket封裝成一個Task(該任務實現java.lang. Runnable
接口)投遞到后端的線程池中進行處理,JDK的線程池維護一個消息隊列和N個活躍線程,對消息隊列中的任務進行處理。由于線程池可以設置消息隊列的大小和最大線程數,因此,它的資源占用是可控的,無論多少個客戶端并發訪問,都不會導致資源的耗盡和宕機。
偽異步的模型圖
服務端代碼
線程池代碼
優勢
由于線程池和消息隊列都是有界的,因此,無論客戶端并發連接數多大,它都不會導致線程個數過于膨脹或者內存溢出,相比于傳統的一連接一線程模型,是一種改良。
但是由于它底層的通信依然采用同步阻塞模型,因此無法從根本上解決問題。
偽異步I/O弊端分析
當對Socket的輸入流進行讀取操作的時候,它會一直阻塞下去,直到發生如下三種事件。
有數據可讀;
可用數據已經讀取完畢;
發生空指針或者I/O異常。
這意味著當對方發送請求或者應答消息比較緩慢,或者網絡傳輸較慢時,讀取輸入流一方的通信線程將被長時間阻塞,如果對方要60s才能夠將數據發送完成,讀取一方的I/O線程也將會被同步阻塞60s,在此期間,其他接入消息只能在消息隊列中排隊。
當調用OutputStream的write方法寫輸出流的時候,它將會被阻塞,直到所有要發送的字節全部寫入完畢,或者發生異常。當消息的接收方處理緩慢的時候,將不能及時地從TCP緩沖區讀取數據,這將會導致發送方的TCP window size不斷減小,直到為0,雙方處于Keep-Alive狀態,消息發送方將不能再向TCP緩沖區寫入消息,這時如果采用的是同步阻塞I/O,write操作將會被無限期阻塞,直到TCP window size大于0或者發生I/O異常。
讀和寫操作都是同步阻塞的,阻塞的時間取決于對方I/O線程的處理速度和網絡I/O的傳輸速度。
本質上來講,我們無法保證生產環境的網絡狀況和對端的應用程序能足夠快,如果我們的應用程序依賴對方的處理速度,它的可靠性就非常差。也許在實驗室進行的性能測試結果令人滿意,但是一旦上線運行,面對惡劣的網絡環境和良莠不齊的第三方系統,問題就會如火山一樣噴發。
偽異步I/O實際上僅僅是對之前I/O線程模型的一個簡單優化,它無法從根本上解決同步I/O導致的通信線程阻塞問題。下面我們就簡單分析下通信對方返回應答時間過長會引起的級聯故障。
(1)服務端處理緩慢,返回應答消息耗費60s,平時只需要10ms。
(2)采用偽異步I/O的線程正在讀取故障服務節點的響應,由于讀取輸入流是阻塞的,它將會被同步阻塞60s。
(3)假如所有的可用線程都被故障服務器阻塞,那后續所有的I/O消息都將在隊列中排隊。
(4)由于線程池采用阻塞隊列實現,當隊列積滿之后,后續入隊列的操作將被阻塞。(5)由于前端只有一個Accptor線程接收客戶端接入,它被阻塞在線程池的同步阻塞隊列之后,新的客戶端請求消息將被拒絕,客戶端會發生大量的連接超時。
(6)由于幾乎所有的連接都超時,調用者會認為系統已經崩潰,無法接收新的請求消息。