Java NIO成功的應用在了各種分布式、即時(shí)通信和中間件Java系統中。證明了基于NIO構建的通信基礎,是一種高效,且擴展性很強的通信架構。
基于Reactor模式的高可擴展性架構這個(gè)架構的基本思路在“基于高可用性NIO服務(wù)器架構”(http://today.java.net/pub/a/today/2007/02/13/architecture-of-highly-scalable-nio-server.html
)中有了清晰的論述。經(jīng)過(guò)幾年實(shí)際運營(yíng)的經(jīng)驗,這種架構的靈活性得到了很好的驗證。我們注意幾點(diǎn),
1,一個(gè)小的線(xiàn)程池負責dispatch NIO事件。
2,注冊事件,即操作selecter時(shí),要使用一個(gè)同步鎖(即Architecture of a Highly Scalable NIO-Based Server一文中的guard對象),即對同一個(gè)selector的操作是互斥的。
3,這個(gè)小的線(xiàn)程池不處理邏輯業(yè)務(wù),大小可以是Runtime.getRuntime().availableProcessors() + 1,即你系統有效CPU個(gè)數+1。這是因為我們假設有一個(gè)線(xiàn)程專(zhuān)門(mén)處理accept事件,
而其他線(xiàn)程處理read/write操作。
4,用另一個(gè)單獨的線(xiàn)程池處理邏輯業(yè)務(wù)
在淘寶網(wǎng)團隊博客上分析Netty架構的時(shí)候也談到了這個(gè)思路,我決定說(shuō)的比較好。這里引用一段:
那么如果是我們自己開(kāi)發(fā)基于NIO實(shí)現高效和高可擴展服務(wù),還有哪些構架方面的問(wèn)題需要考慮呢?
NIO構架中比較需要經(jīng)驗和比較復雜的主要是2點(diǎn):1,)是基于提高的性能的線(xiàn)程池設計;2)基于網(wǎng)絡(luò )通訊量的通訊完整性校驗的構架。
1. 基于提高的性能的線(xiàn)程池設計
既然有一個(gè)單獨處理邏輯業(yè)務(wù)的線(xiàn)程池,這個(gè)線(xiàn)程池的大小應該由你的業(yè)務(wù)來(lái)決定。對于高效服務(wù)器來(lái)說(shuō),這個(gè)線(xiàn)程池大小會(huì )對你的服務(wù)性能產(chǎn)生很大的影響。設置多少合適呢?
這里真的有很多情況需要考慮,換句話(huà)說(shuō),這里水很深。我只能根據自己的經(jīng)驗舉幾個(gè)例子。真正到了運營(yíng)系統上,一邊測試一邊調整一邊總結吧。
假設消息解析用時(shí)5毫秒,數據庫操作用時(shí)20毫秒,其他邏輯處理用時(shí)20毫秒,那么整個(gè)業(yè)務(wù)處理用時(shí)45毫秒。
因為數據庫操作主要是IO讀寫(xiě)操作,為使CPU得到最大程度的利用,在一個(gè)16核的服務(wù)器上,應該設置 (45/ 25)
* 16 = 29 個(gè)線(xiàn)程即可。
假設不是所有的操作都是在平均時(shí)間內完成,比如數據庫操作,假設是在12~35毫秒區間內。即有線(xiàn)程會(huì )不斷的被某些操作block住,為了充分利用CPU能力,因設置為((35 + 25)/ 25)* 16 = 39個(gè)線(xiàn)程。
所以原則上,如果應用是一個(gè)偏重數據庫操作的應用,則線(xiàn)程數應高些;如果應用是一個(gè)高CPU應用,則線(xiàn)程數不用太高。
假設邏輯處理中,對共享資源的操作用時(shí)5毫秒。此時(shí)同時(shí)只能有一個(gè)線(xiàn)程對共享資源進(jìn)行操作,那么在一個(gè)16核的服務(wù)器上,應該設置 (37 / 5) * 1 = 8 個(gè)線(xiàn)程即可。
假設只有一部分操作對共享資源有寫(xiě),其他只是讀。這樣采用樂(lè )觀(guān)鎖,使寫(xiě)操作降為所有操作的10%,那么有90%的業(yè)務(wù),其合適的線(xiàn)程數可為39個(gè)線(xiàn)程。10%的業(yè)務(wù)應為8個(gè)線(xiàn)程。平均則為 35 + 1 = 36個(gè)線(xiàn)程??梢?jiàn)仔細的分析共享資源的使用,能很好的提高系統性能。
根據線(xiàn)程CPU占用率和CPU個(gè)數來(lái)設置線(xiàn)程數的假設前提是所有線(xiàn)程都要要運行。但實(shí)際系統中線(xiàn)程處理要處理不同時(shí)間達到的請求。
場(chǎng)景:假設線(xiàn)程處理不是同時(shí)進(jìn)行的
假設有一個(gè)消息服務(wù)器,每秒處理500個(gè)消息,即認為平均每2ms接受一個(gè)新請求。假設處理一個(gè)請求需要100ms,那么當接收到第51個(gè)請求時(shí),第一個(gè)線(xiàn)程就已經(jīng)空閑。這個(gè)請求可以由第一個(gè)線(xiàn)程處理,而不需要新線(xiàn)程。這樣,需要50個(gè)線(xiàn)程。如果每個(gè)消息請求CPU空閑時(shí)間為10ms,那么為對于每個(gè)線(xiàn)程,并發(fā)的數量為 100/90 = 1.1;因此合適的線(xiàn)程為 50 * 1.1 * 核數。
跑一個(gè)小測試程序,code見(jiàn)附件
執行一個(gè)task耗時(shí)1000ms,其中50%CPU占滿(mǎn)。每100毫秒處理一個(gè)task。CPU4核。
這樣計算 (1000/100) * 2 * 4 = 40
測試結果,設置不同的線(xiàn)程數執行100個(gè)task,結果
線(xiàn)程數 | 全部執行使用時(shí)間
100 | 14484
80 | 14097
40 | 14407
20 | 16016
10 | 16548
在線(xiàn)程數達到40之后,再增加線(xiàn)程,因為CPU已經(jīng)被充分使用,因此處理速度沒(méi)有得到響應增加。反而有線(xiàn)程開(kāi)銷(xiāo)有可能下降。因此在CPU占用率和處理task間隔恒定的情況下,使用以上公式計算適合的線(xiàn)程數量可以得到較優(yōu)結果。
2. 基于網(wǎng)絡(luò )通訊量的通訊完整性校驗
先看看READ事件的觸發(fā)條件:
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.
就是說(shuō),NIO構架中不能保證每次READ事件發(fā)生時(shí)從channel中讀出的數據就是完整。例如,在通訊數據量較大時(shí),網(wǎng)絡(luò )層write buffer很容易被寫(xiě)滿(mǎn)。此時(shí)讀到的數據就是不完整的。
從構架角度,應根據應用場(chǎng)景設計三種不同的處理方式。
基本上有三種類(lèi)型的應用,
1. 較低的通信量應用。這類(lèi)應用的特點(diǎn)是所有的通信量不是很大,而且數據包小。所有數據都能在一次網(wǎng)絡(luò )層buffer flush中全部寫(xiě)出。比如ZooKeeper client對cluster的操作。這種通信模式是完全不需要進(jìn)行數據包校驗的。
2. 基于RPC模式的應用。比如Hadoop,每次NameNode和DataNode之間的通訊都是通過(guò)RPC框架封裝,轉變成client對server的調用。所有的操作都是通過(guò)Java反射機制反射成方法調用,這樣操作的特點(diǎn)是每次讀到的數據都是可以通過(guò)ObjectInputStream(new ByteArrayInputStream(bytes)).readObject()操作的。這樣的應用,應該在第一種應用的架構基礎上增加對ObjectInputStream的校驗。如果校驗失敗,則說(shuō)明這次通信沒(méi)有完成,應和下次read到數據合并在一起處理。
3. 基于大量數據通信的應用。這種應用的特點(diǎn)是基于一種大數據量通信協(xié)議,比如RTSP。數據包是否完整需要經(jīng)過(guò)通信協(xié)議約定的校驗符進(jìn)行校驗。這樣就必須實(shí)現一個(gè)校驗類(lèi)。如果校驗失敗,則說(shuō)明這次通信沒(méi)有完成,應和下次read到數據合并在一起處理。
本文純屬原創(chuàng ),歡迎轉載,請注明原網(wǎng)址;
聯(lián)系客服