閱讀905 返回首頁    go 微軟 go windows


《Java NIO文檔》非阻塞式服務器

即使你知道Java NIO 非阻塞的工作特性(如Selector,Channel,Buffer等組件),但是想要設計一個非阻塞的服務器仍然是一件很困難的事。非阻塞式服務器相較於阻塞式來說要多上許多挑戰。本文將會討論非阻塞式服務器的主要幾個難題,並針對這些難題給出一些可能的解決方案。

查找關於非阻塞式服務器設計方麵的資料實在不太容易,所以本文提供的解決方案都是基於本人工作和想法上的。如果各位有其他的替代方案或者更好的想法,我會很樂意聽取這些方案和想法!你可以在文章下方留下你的評論,或者發郵件給我(郵箱為:info@jenkov.com )。

本文的設計思路想法都是基於Java NIO的。但是我相信如果某些語言中也有像Selector之類的組件的話,文中的想法也能用於該語言。據我所知,類似的組件底層操作係統會提供,所以對你來說也可以根據其中的思想運用在其他語言上。

 

非阻塞式服務器– GitHub 倉庫

我已經創建了一些簡單的這些思想的概念驗證呈現在這篇教程中,並且為了讓你可以看到,我把源碼放到了github資源庫上了。這裏是GitHub資源庫地址:

https://github.com/jjenkov/java-nio-server

 

非阻塞式IO管道(Pipelines)

一個非阻塞式IO管道是由各個處理非阻塞式IO組件組成的鏈。其中包括讀/寫IO。下圖就是一個簡單的非阻塞式IO管道組成:

non-blocking-server-1

一個組件使用 Selector 監控 Channel 什麼時候有可讀數據。然後這個組件讀取輸入並且根據輸入生成相應的輸出。最後輸出將會再次寫入到一個Channel中。

一個非阻塞式IO管道不需要將讀數據和寫數據都包含,有一些管道可能隻會讀數據,另一些可能隻會寫數據。

上圖僅顯示了一個單一的組件。一個非阻塞式IO管道可能擁有超過一個以上的組件去處理輸入數據。一個非阻塞式管道的長度是由他的所要完成的任務決定。

一個非阻塞IO管道可能同時讀取多個Channel裏的數據。舉個例子:從多個SocketChannel管道讀取數據。

其實上圖的控製流程還是太簡單了。這裏是組件從Selector開始從Channel中讀取數據,而不是Channel將數據推送給Selector進入組件中,即便上圖畫的就是這樣。

 

非阻塞式vs. 阻塞式管道

非阻塞和阻塞IO管道兩者之間最大的區別在於他們如何從底層Channel(Socket或者file)讀取數據。

IO管道通常從流中讀取數據(來自socket或者file)並且將這些數據拆分為一係列連貫的消息。這和使用tokenizer(這裏估計是解析器之類的意思)將數據流解析為token(這裏應該是數據包的意思)類似。相反你隻是將數據流分解為更大的消息體。我將拆分數據流成消息這一組件稱為“消息讀取器”(Message Reader)下麵是Message Reader拆分流為消息的示意圖:

non-blocking-server-2

一個阻塞IO管道可以使用類似InputStream的接口每次一個字節地從底層Channel讀取數據,並且這個接口阻塞直到有數據可以讀取。這就是阻塞式Message Reader的實現過程。

使用阻塞式IO接口簡化了Message Reader的實現。阻塞式Message Reader從不用處理在流沒有數據可讀的情況,或者它隻讀取流中的部分數據並且對於消息的恢複也要延遲處理的情況。

同樣,阻塞式Message Writer(一個將數據寫入流中組件)也從不用處理隻有部分數據被寫入和寫入消息要延遲恢複的情況。

 

阻塞式IO管道的缺陷

雖然阻塞式Message Reader容易實現,但是也有一個不幸的缺點:每一個要分解成消息的流都需要一個獨立的線程。必須要這樣做的理由是每一個流的IO接口會阻塞,直到它有數據讀取。這就意味著一個單獨的線程是無法嚐試從一個沒有數據的流中讀取數據轉去讀另一個流。一旦一個線程嚐試從一個流中讀取數據,那麼這個線程將會阻塞直到有數據可以讀取。

如果IO管道是必須要處理大量並發鏈接服務器的一部分的話,那麼服務器就需要為每一個鏈接維護一個線程。對於任何時間都隻有幾百條並發鏈接的服務器這確實不是什麼問題。但是如果服務器擁有百萬級別的並發鏈接量,這種設計方式就沒有良好收放。每個線程都會占用棧32bit-64bit的內存。所以一百萬個線程占用的內存將會達到1TB!不過在此之前服務器將會把所有的內存用以處理傳經來的消息(例如:分配給消息處理期間使用對象的內存)

為了將線程數量降下來,許多服務器使用了服務器維持線程池(例如:常用線程為100)的設計,從而一次一個地從入站鏈接(inbound connections)地讀取。入站鏈接保存在一個隊列中,線程按照進入隊列的順序處理入站鏈接。這一設計如下圖所示:(譯者注:Tomcat就是這樣的)

non-blocking-server-3

然而,這一設計需要入站鏈接合理地發送數據。如果入站鏈接長時間不活躍,那麼大量的不活躍鏈接實際上就造成了線程池中所有線程阻塞。這意味著服務器響應變慢甚至是沒有反應。

一些服務器嚐試通過彈性控製線程池的核心線程數量這一設計減輕這一問題。例如,如果線程池線程不足時,線程池可能開啟更多的線程處理請求。這一方案意味著需要大量的長時鏈接才能使服務器不響應。但是記住,對於並發線程數任然是有一個上限的。因此,這一方案仍然無法很好地解決一百萬個長時鏈接。

基礎非阻塞式IO管道設計

一個非阻塞式IO管道可以使用一個單獨的線程向多個流讀取數據。這需要流可以被切換到非阻塞模式。在非阻塞模式下,當你讀取流信息時可能會返回0個字節或更多字節的信息。如果流中沒有數據可讀就返回0字節,如果流中有數據可讀就返回1+字節。

為了避免檢查沒有可讀數據的流我們可以使用 Java NIO Selector. 一個或多個SelectableChannel 實例可以同時被一個Selector注冊.。當你調用Selectorselect()或者 selectNow() 方法它隻會返回有數據讀取的SelectableChannel的實例. 下圖是該設計的示意圖:

non-blocking-server-4

讀取部分消息

當我們從一個SelectableChannel讀取一個數據包時,我們不知道這個數據包相比於源文件是否有丟失或者重複數據(原文是:When we read a block of data from a SelectableChannel we do not know if that data block contains less or more than a message)。一個數據包可能的情況有:缺失數據(比原有消息的數據少)、與原有一致、比原來的消息的數據更多(例如:是原來的1.5或者2.5倍)。數據包可能出現的情況如下圖所示:

non-blocking-server-5

在處理類似上麵這樣部分信息時,有兩個問題:

  1. 判斷你是否能在數據包中獲取完整的消息。
  2. 在其餘消息到達之前如何處理已到達的部分消息。

判斷消息的完整性需要消息讀取器(Message Reader)在數據包中尋找是否存在至少一個完整消息體的數據。如果一個數據包包含一個或多個完整消息體,這些消息就能夠被發送到管道進行處理。尋找完整消息體這一處理可能會重複多次,因此這一操作應該盡可能的快。

判斷消息完整性和存儲部分消息都是消息讀取器(Message Reader)的責任。為了避免混合來自不同Channel的消息,我們將對每一個Channel使用一個Message Reader。設計如下圖所示:

non-blocking-server-6

在從Selector得到可從中讀取數據的Channel實例之後, 與該Channel相關聯的Message Reader讀取數據並嚐試將他們分解為消息。這樣讀出的任何完整消息可以被傳到讀取通道(read pipeline)任何需要處理這些消息的組件中。

一個Message Reader一定滿足特定的協議。Message Reader需要知道它嚐試讀取的消息的消息格式。如果我們的服務器可以通過協議來複用,那它需要有能夠插入Message Reader實現的功能 – 可能通過接收一個Message Reader工廠作為配置參數。

 轉載自 並發編程網 - ifeve.com

最後更新:2017-05-18 20:37:03

  上一篇:go  分布式消息中間件Metaq發布1.4.3
  下一篇:go  Emacs + Clojure配置的幾個Tip