Java NIO成功的應用在了各類分佈式、即時通訊和中間件Java系統中。證實了基於NIO構建的通訊基礎,是一種高效,且擴展性很強的通訊架構。
基於Reactor模式的高可擴展性架構這個架構的基本思路在「基於高可用性NIO服務器架構」(http://today.java.net/pub/a/today/2007/02/13/architecture-of-highly-scalable-nio-server.html
)中有了清晰的論述。通過幾年實際運營的經驗,這種架構的靈活性獲得了很好的驗證。咱們注意幾點,
1,一個小的線程池負責dispatch NIO事件。
2,註冊事件,即操做selecter時,要使用一個同步鎖(即Architecture of a Highly Scalable NIO-Based Server一文中的guard對象),即對同一個selector的操做是互斥的。
3,這個小的線程池不處理邏輯業務,大小能夠是Runtime.getRuntime().availableProcessors() + 1,即你係統有效CPU個數+1。這是由於咱們假設有一個線程專門處理accept事件,
而其餘線程處理read/write操做。
4,用另外一個單獨的線程池處理邏輯業務html
在淘寶網團隊博客上分析Netty架構的時候也談到了這個思路,我決定說的比較好。這裏引用一段:java
那麼若是是咱們本身開發基於NIO實現高效和高可擴展服務,還有哪些構架方面的問題須要考慮呢?
NIO構架中比較須要經驗和比較複雜的主要是2點:1,)是基於提升的性能的線程池設計;2)基於網絡通信量的通信完整性校驗的構架。
1. 基於提升的性能的線程池設計
既然有一個單獨處理邏輯業務的線程池,這個線程池的大小應該由你的業務來決定。對於高效服務器來講,這個線程池大小會對你的服務性能產生很大的影響。設置多少合適呢?
這裏真的有不少狀況須要考慮,換句話說,這裏水很深。我只能根據本身的經驗舉幾個例子。真正到了運營系統上,一邊測試一邊調整一邊總結吧。
假設消息解析用時5毫秒,數據庫操做用時20毫秒,其餘邏輯處理用時20毫秒,那麼整個業務處理用時45毫秒。
由於數據庫操做主要是IO讀寫操做,爲使CPU獲得最大程度的利用,在一個16核的服務器上,應該設置 (45/ 25)
* 16 = 29 個線程便可。
假設不是全部的操做都是在平均時間內完成,好比數據庫操做,假設是在12~35毫秒區間內。即有線程會不斷的被某些操做block住,爲了充分利用CPU能力,因設置爲((35 + 25)/ 25)* 16 = 39個線程。
因此原則上,若是應用是一個偏重數據庫操做的應用,則線程數應高些;若是應用是一個高CPU應用,則線程數不用過高。
假設邏輯處理中,對共享資源的操做用時5毫秒。此時同時只能有一個線程對共享資源進行操做,那麼在一個16核的服務器上,應該設置 (37 / 5) * 1 = 8 個線程便可。
假設只有一部分操做對共享資源有寫,其餘只是讀。這樣採用樂觀鎖,使寫操做降爲全部操做的10%,那麼有90%的業務,其合適的線程數可爲39個線程。10%的業務應爲8個線程。平均則爲 35 + 1 = 36個線程。可見仔細的分析共享資源的使用,能很好的提升系統性能。
根據線程CPU佔用率和CPU個數來設置線程數的假設前提是全部線程都要要運行。但實際系統中線程處理要處理不一樣時間達到的請求。
場景:假設線程處理不是同時進行的
假設有一個消息服務器,每秒處理500個消息,即認爲平均每2ms接受一個新請求。假設處理一個請求須要100ms,那麼當接收到第51個請求時,第一個線程就已經空閒。這個請求能夠由第一個線程處理,而不須要新線程。這樣,須要50個線程。若是每一個消息請求CPU空閒時間爲10ms,那麼爲對於每一個線程,併發的數量爲 100/90 = 1.1;所以合適的線程爲 50 * 1.1 * 核數。
跑一個小測試程序,code見附件
執行一個task耗時1000ms,其中50%CPU佔滿。每100毫秒處理一個task。CPU4核。
這樣計算 (1000/100) * 2 * 4 = 40
測試結果,設置不一樣的線程數執行100個task,結果
線程數 | 所有執行使用時間
100 | 14484
80 | 14097
40 | 14407
20 | 16016
10 | 16548
在線程數達到40以後,再增長線程,由於CPU已經被充分使用,所以處理速度沒有獲得響應增長。反而有線程開銷有可能降低。所以在CPU佔用率和處理task間隔恆定的狀況下,使用以上公式計算適合的線程數量能夠獲得較優結果。
2. 基於網絡通信量的通信完整性校驗
先看看READ事件的觸發條件:
If the selector detects that the corresponding channel is ready for reading, has reached end-of-stream, has been remotely shut down for further reading,
or has an error pending, then it will add OP_READ to the key's ready-operation set and add the key to its selected-key set.
就是說,NIO構架中不能保證每次READ事件發生時從channel中讀出的數據就是完整。例如,在通信數據量較大時,網絡層write buffer很容易被寫滿。此時讀到的數據就是不完整的。
從構架角度,應根據應用場景設計三種不一樣的處理方式。
基本上有三種類型的應用,
1. 較低的通訊量應用。這類應用的特色是全部的通訊量不是很大,並且數據包小。全部數據都能在一次網絡層buffer flush中所有寫出。好比ZooKeeper client對cluster的操做。這種通訊模式是徹底不須要進行數據包校驗的。
2. 基於RPC模式的應用。好比Hadoop,每次NameNode和DataNode之間的通信都是經過RPC框架封裝,轉變成client對server的調用。全部的操做都是經過Java反射機制反射成方法調用,這樣操做的特色是每次讀到的數據都是能夠經過ObjectInputStream(new ByteArrayInputStream(bytes)).readObject()操做的。這樣的應用,應該在第一種應用的架構基礎上增長對ObjectInputStream的校驗。若是校驗失敗,則說明此次通訊沒有完成,應和下次read到數據合併在一塊兒處理。
3. 基於大量數據通訊的應用。這種應用的特色是基於一種大數據量通訊協議,好比RTSP。數據包是否完整須要通過通訊協議約定的校驗符進行校驗。這樣就必須實現一個校驗類。若是校驗失敗,則說明此次通訊沒有完成,應和下次read到數據合併在一塊兒處理。數據庫