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

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

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

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