node和java性能_服务端I/O性能大比拼:Node、PHP、Java和Go(二)
服務(wù)端I/O性能大比拼:Node、PHP、Java和Go(二)
服務(wù)端I/O性能大比拼:Node、PHP、Java和Go(二)
### 多線程的方式:Java
所以就在你買了你的第一個(gè)域名的時(shí)候,Java來(lái)了,并且在一個(gè)句子之后隨便說(shuō)一句“dot com”是很酷的。而Java具有語(yǔ)言內(nèi)置的多線程(特別是在創(chuàng)建時(shí)),這一點(diǎn)非常棒。
大多數(shù)Java網(wǎng)站服務(wù)器通過(guò)為每個(gè)進(jìn)來(lái)的請(qǐng)求啟動(dòng)一個(gè)新的執(zhí)行線程,然后在該線程中最終調(diào)用作為應(yīng)用程序開(kāi)發(fā)人員的你所編寫的函數(shù)。
在Java的Servlet中執(zhí)行I/O操作,往往看起來(lái)像是這樣:
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
// 阻塞的文件I/O
InputStream fileIs = new FileInputStream("/path/to/file");
// 阻塞的網(wǎng)絡(luò)I/O
URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection();
InputStream netIs = urlConnection.getInputStream();
// 更多阻塞的網(wǎng)絡(luò)I/O
out.println("...");
}
由于我們上面的doGet方法對(duì)應(yīng)于一個(gè)請(qǐng)求并且在自己的線程中運(yùn)行,而不是每次請(qǐng)求都對(duì)應(yīng)需要有自己專屬內(nèi)存的單獨(dú)進(jìn)程,所以我們會(huì)有一個(gè)單獨(dú)的線程。
這樣會(huì)有一些不錯(cuò)的優(yōu)點(diǎn),例如可以在線程之間共享狀態(tài)、共享緩存的數(shù)據(jù)等,因?yàn)樗鼈兛梢韵嗷ピL問(wèn)各自的內(nèi)存,但是它如何與調(diào)度進(jìn)行交互的影響,仍然與前面PHP例子中所做的內(nèi)容幾乎一模一樣。
每個(gè)請(qǐng)求都會(huì)產(chǎn)生一個(gè)新的線程,而在這個(gè)線程中的各種I/O操作會(huì)一直阻塞,直到這個(gè)請(qǐng)求被完全處理為止。
為了最小化創(chuàng)建和銷毀它們的成本,線程會(huì)被匯集在一起,但是依然,有成千上萬(wàn)個(gè)連接就意味著成千上萬(wàn)個(gè)線程,這對(duì)于調(diào)度器是不利的。
一個(gè)重要的里程碑是,在Java 1.4 版本(和再次顯著升級(jí)的1.7 版本)中,獲得了執(zhí)行非阻塞I/O調(diào)用的能力。大多數(shù)應(yīng)用程序,網(wǎng)站和其他程序,并沒(méi)有使用它,但至少它是可獲得的。一些Java網(wǎng)站服務(wù)器嘗試以各種方式利用這一點(diǎn); 然而,絕大多數(shù)已經(jīng)部署的Java應(yīng)用程序仍然如上所述那樣工作。

Java讓我們更進(jìn)了一步,當(dāng)然對(duì)于I/O也有一些很好的“開(kāi)箱即用”的功能,但它仍然沒(méi)有真正解決問(wèn)題:當(dāng)你有一個(gè)嚴(yán)重I/O綁定的應(yīng)用程序正在被數(shù)千個(gè)阻塞線程狂拽著快要墜落至地面時(shí)怎么辦。
### 作為一等公民的非阻塞I/O:Node
當(dāng)談到更好的I/O時(shí),Node.js無(wú)疑是新寵。任何曾經(jīng)對(duì)Node有過(guò)最簡(jiǎn)單了解的人都被告知它是“非阻塞”的,并且它能有效地處理I/O。在一般意義上,這是正確的。但魔鬼藏在細(xì)節(jié)中,當(dāng)談及性能時(shí)這個(gè)巫術(shù)的實(shí)現(xiàn)方式至關(guān)重要。
本質(zhì)上,Node實(shí)現(xiàn)的范式不是基本上說(shuō)“在這里編寫代碼來(lái)處理請(qǐng)求”,而是轉(zhuǎn)變成“在這里寫代碼開(kāi)始處理請(qǐng)求”。每次你都需要做一些涉及I/O的事情,發(fā)出請(qǐng)求或者提供一個(gè)當(dāng)完成時(shí)Node會(huì)調(diào)用的回調(diào)函數(shù)。
在求中進(jìn)行I/O操作的典型Node代碼,如下所示:
http.createServer(function(request, response) {
fs.readFile('/path/to/file', 'utf8', function(err, data) {
response.end(data);
});
});
可以看到,這里有兩個(gè)回調(diào)函數(shù)。第一個(gè)會(huì)在請(qǐng)求開(kāi)始時(shí)被調(diào)用,而第二個(gè)會(huì)在文件數(shù)據(jù)可用時(shí)被調(diào)用。
這樣做的基本上給了Node一個(gè)在這些回調(diào)函數(shù)之間有效地處理I/O的機(jī)會(huì)。一個(gè)更加相關(guān)的場(chǎng)景是在Node中進(jìn)行數(shù)據(jù)庫(kù)調(diào)用,但我不想再列出這個(gè)煩人的例子,因?yàn)樗峭耆粯拥脑瓌t:啟動(dòng)數(shù)據(jù)庫(kù)調(diào)用,并提供一個(gè)回調(diào)函數(shù)給Node,它使用非阻塞調(diào)用單獨(dú)執(zhí)行I/O操作,然后在你所要求的數(shù)據(jù)可用時(shí)調(diào)用回調(diào)函數(shù)。這種I/O調(diào)用隊(duì)列,讓Node來(lái)處理,然后獲取回調(diào)函數(shù)的機(jī)制稱為“事件循環(huán)”。它工作得非常好。

然而,這個(gè)模型中有一道關(guān)卡。在幕后,究其原因,更多是如何實(shí)現(xiàn)JavaScript V8 引擎(Chrome的JS引擎,用于Node)1,而不是其他任何事情。
你所編寫的JS代碼全部都運(yùn)行在一個(gè)線程中。思考一下。這意味著當(dāng)使用有效的非阻塞技術(shù)執(zhí)行I/O時(shí),正在進(jìn)行CPU綁定操作的JS可以在運(yùn)行在單線程中,每個(gè)代碼塊阻塞下一個(gè)。
一個(gè)常見(jiàn)的例子是循環(huán)數(shù)據(jù)庫(kù)記錄,在輸出到客戶端前以某種方式處理它們。以下是一個(gè)例子,演示了它如何工作:
var handler = function(request, response) {
connection.query('SELECT ...', function (err, rows) {
if (err) { throw err };
for (var i = 0; i < rows.length; i++) {
// 對(duì)每一行紀(jì)錄進(jìn)行處理
}
response.end(...); // 輸出結(jié)果
})
};
雖然Node確實(shí)可以有效地處理I/O,但上面的例子中的for循環(huán)使用的是在你主線程中的CPU周期。這意味著,如果你有10,000個(gè)連接,該循環(huán)有可能會(huì)讓你整個(gè)應(yīng)用程序慢如蝸牛,具體取決于每次循環(huán)需要多長(zhǎng)時(shí)間。每個(gè)請(qǐng)求必須分享在主線程中的一段時(shí)間,一次一個(gè)。
這個(gè)整體概念的前提是I/O操作是最慢的部分,因此最重要是有效地處理這些操作,即使意味著串行進(jìn)行其他處理。這在某些情況下是正確的,但不是全都正確。
另一點(diǎn)是,雖然這只是一個(gè)意見(jiàn),但是寫一堆嵌套的回調(diào)可能會(huì)令人相當(dāng)討厭,有些人認(rèn)為它使得代碼明顯無(wú)章可循。在Node代碼的深處,看到嵌套四層、嵌套五層、甚至更多層級(jí)的嵌套并不罕見(jiàn)。
我們?cè)俅位氐搅藱?quán)衡。如果你主要的性能問(wèn)題在于I/O,那么Node模型能很好地工作。然而,它的阿喀琉斯之踵(譯者注:來(lái)自希臘神話,表示致命的弱點(diǎn))是如果不小心的話,你可能會(huì)在某個(gè)函數(shù)里處理HTTP請(qǐng)求并放置CPU密集型代碼,最后使得每個(gè)連接慢得如蝸牛。
### 真正的非阻塞:Go
在進(jìn)入Go這一章節(jié)之前,我應(yīng)該披露我是一名Go粉絲。我已經(jīng)在許多項(xiàng)目中使用Go,是其生產(chǎn)力優(yōu)勢(shì)的公開(kāi)支持者,并且在使用時(shí)我在工作中看到了他們。
也就是說(shuō),我們來(lái)看看它是如何處理I/O的。Go語(yǔ)言的一個(gè)關(guān)鍵特性是它包含自己的調(diào)度器。并不是每個(gè)線程的執(zhí)行對(duì)應(yīng)于一個(gè)單一的OS線程,Go采用的是“goroutines”這一概念。Go運(yùn)行時(shí)可以將一個(gè)goroutine分配給一個(gè)OS線程并使其執(zhí)行,或者把它掛起而不與OS線程關(guān)聯(lián),這取決于goroutine做的是什么。來(lái)自Go的HTTP服務(wù)器的每個(gè)請(qǐng)求都在單獨(dú)的Goroutine中處理。
此調(diào)度器工作的示意圖,如下所示:

這是通過(guò)在Go運(yùn)行時(shí)的各個(gè)點(diǎn)來(lái)實(shí)現(xiàn)的,通過(guò)將請(qǐng)求寫入/讀取/連接/等實(shí)現(xiàn)I/O調(diào)用,讓當(dāng)前的goroutine進(jìn)入睡眠狀態(tài),當(dāng)可采取進(jìn)一步行動(dòng)時(shí)用信息把goroutine重新喚醒。
實(shí)際上,除了回調(diào)機(jī)制內(nèi)置到I/O調(diào)用的實(shí)現(xiàn)中并自動(dòng)與調(diào)度器交互外,Go運(yùn)行時(shí)做的事情與Node做的事情并沒(méi)有太多不同。它也不受必須把所有的處理程序代碼都運(yùn)行在同一個(gè)線程中這一限制,Go將會(huì)根據(jù)其調(diào)度器的邏輯自動(dòng)將Goroutine映射到其認(rèn)為合適的OS線程上。最后代碼類似這樣:
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 這里底層的網(wǎng)絡(luò)調(diào)用是非阻塞的
rows, err := db.Query("SELECT ...")
for _, row := range rows {
// 處理rows
// 每個(gè)請(qǐng)求在它自己的goroutine中
}
w.Write(...) // 輸出響應(yīng)結(jié)果,也是非阻塞的
}
正如你在上面見(jiàn)到的,我們的基本代碼結(jié)構(gòu)像是更簡(jiǎn)單的方式,并且在背后實(shí)現(xiàn)了非阻塞I/O。
在大多數(shù)情況下,這最終是“兩個(gè)世界中最好的”。非阻塞I/O用于全部重要的事情,但是你的代碼看起來(lái)像是阻塞,因此往往更容易理解和維護(hù)。Go調(diào)度器和OS調(diào)度器之間的交互處理了剩下的部分。這不是完整的魔法,如果你建立的是一個(gè)大型的系統(tǒng),那么花更多的時(shí)間去理解它工作原理的更多細(xì)節(jié)是值得的; 但與此同時(shí),“開(kāi)箱即用”的環(huán)境可以很好地工作和很好地進(jìn)行擴(kuò)展。
Go可能有它的缺點(diǎn),但一般來(lái)說(shuō),它處理I/O的方式不在其中。
總結(jié)
以上是生活随笔為你收集整理的node和java性能_服务端I/O性能大比拼:Node、PHP、Java和Go(二)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: java大会主题曲_网易未来大会主题曲发
- 下一篇: java闭包lambda,闭包在groo