溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Node、PHP、Java和Go服務端I/O性能對比

發(fā)布時間:2021-07-22 09:35:41 來源:億速云 閱讀:307 作者:chen 欄目:編程語言

本篇內(nèi)容主要講解“Node、PHP、Java和Go服務端I/O性能對比”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Node、PHP、Java和Go服務端I/O性能對比”吧!

了解應用程序的輸入/輸出(I/O)模型能夠更好的理解它在處理負載時理想情況與實際情況下的差異。也許你的應用程序很小,也無需支撐太高的負載,所以這方面需要考慮的東西還比較少。但是,隨著應用程序流量負載的增加,使用錯誤的I/O模型可能會導致非常嚴重的后果。

在本文中,我們將把Node、Java、Go和PHP與Apache配套進行比較,討論不同語言如何對I/O進行建模、每個模型的優(yōu)缺點,以及一些基本的性能評測。如果你比較關心自己下一個Web應用程序的I/O性能,本文將為你提供幫助。

I/O基礎:快速回顧一下

要了解與I/O相關的因素,我們必須首先在操作系統(tǒng)層面上了解這些概念。雖然不太可能一上來就直接接觸到太多的概念,但在應用的運行過程中,不管是直接還是間接,總會遇到它們。細節(jié)很重要。

系統(tǒng)調(diào)用

首先,我們來認識下系統(tǒng)調(diào)用,具體描述如下:

  • 應用程序請求操作系統(tǒng)內(nèi)核為其執(zhí)行I/O操作。

  • “系統(tǒng)調(diào)用”是指程序請求內(nèi)核執(zhí)行某些操作。其實現(xiàn)細節(jié)因操作系統(tǒng)而異,但基本概念是相同的。在執(zhí)行“系統(tǒng)調(diào)用”時,將會有一些控制程序的特定指令轉移到內(nèi)核中去。一般來說,系統(tǒng)調(diào)用是阻塞的,這意味著程序會一直等待直到內(nèi)核返回結果。

  • 內(nèi)核在物理設備(磁盤、網(wǎng)卡等)上執(zhí)行底層I/O操作并回復系統(tǒng)調(diào)用。在現(xiàn)實世界中,內(nèi)核可能需要做很多事情來滿足你的請求,包括等待設備準備就緒、更新其內(nèi)部狀態(tài)等等,但作為一名應用程序開發(fā)人員,你無需關心這些,這是內(nèi)核的事情。

阻塞調(diào)用與非阻塞調(diào)用

我在上面說過,系統(tǒng)調(diào)用一般來說是阻塞的。但是,有些調(diào)用卻屬于“非阻塞”的,這意味著內(nèi)核會將請求放入隊列或緩沖區(qū)中,然后立即返回而不等待實際I/O的發(fā)生。所以,它只會“阻塞”很短的時間,但排隊需要一定的時間。

為了說明這一點,下面給出幾個例子(Linux系統(tǒng)調(diào)用):

  • read()是一個阻塞調(diào)用。我們需要傳遞一個文件句柄和用于保存數(shù)據(jù)的緩沖區(qū)給它,當數(shù)據(jù)保存到緩沖區(qū)之后返回。它的優(yōu)點是優(yōu)雅而又簡單。

  • epoll_create()、epoll_ctl()和epoll_wait()可用于創(chuàng)建一組句柄進行監(jiān)聽,添加/刪除這個組中的句柄、阻塞程序直到句柄有任何的活動。這些系統(tǒng)調(diào)用能讓你只用單個線程就能高效地控制大量的I/O操作。這些功能雖然非常有用,但使用起來相當復雜。

了解這里的時間差的數(shù)量級非常重要。如果一個沒有優(yōu)化過的CPU內(nèi)核以3GHz的頻率運行,那么它可以每秒執(zhí)行30億個周期(即每納秒3個周期)。一個非阻塞的系統(tǒng)調(diào)用可能需要大約10多個周期,或者說幾個納秒。對從網(wǎng)絡接收信息的調(diào)用進行阻塞可能需要更長的時間,比如說200毫秒(1/5秒)。

比方說,非阻塞調(diào)用花了20納秒,阻塞調(diào)用花了200,000,000納秒。這樣,進程為了阻塞調(diào)用可能就要等待1000萬個周期。

內(nèi)核提供了阻塞I/O(“從網(wǎng)絡讀取數(shù)據(jù)”)和非阻塞I/O(“告訴我網(wǎng)絡連接上什么時候有新數(shù)據(jù)”)這兩種方法,并且兩種機制阻塞調(diào)用進程的時間長短完全不同。

調(diào)度

第三個非常關鍵的事情是當有很多線程或進程開始出現(xiàn)阻塞時會發(fā)生什么問題。

對我們而言,線程和進程之間并沒有太大的區(qū)別。而在現(xiàn)實中,與性能相關的最顯著的區(qū)別是,由于線程共享相同的內(nèi)存,并且每個進程都有自己的內(nèi)存空間,所以單個進程往往會占用更多的內(nèi)存。但是,在我們談論調(diào)度的時候,實際上講的是完成一系列的事情,并且每個事情都需要在可用的CPU內(nèi)核上獲得一定的執(zhí)行時間。

如果你有8個內(nèi)核來運行300個線程,那么你必須把時間分片,這樣,每個線程才能獲得屬于它的時間片,每一個內(nèi)核運行很短的時間,然后切換到下一個線程。這是通過“上下文切換”完成的,可以讓CPU從一個線程/進程切換到下一個線程/進程。

這種上下文切換有一定的成本,即需要一定的時間??斓臅r候可能會小于100納秒,但如果實現(xiàn)細節(jié)、處理器速度/架構、CPU緩存等軟硬件的不同,花個1000納秒或更長的時間也很正常。

線程(或進程)數(shù)量越多,則上下文切換的次數(shù)也越多。如果存在成千上萬的線程,每個線程都要耗費幾百納秒的切換時間的時候,系統(tǒng)就會變得非常慢。

然而,非阻塞調(diào)用實質(zhì)上告訴內(nèi)核“只有在這些連接上有新的數(shù)據(jù)或事件到來時才調(diào)用我”。這些非阻塞調(diào)用可有效地處理大I/O負載并減少上下文切換。

值得注意的是,雖然本文舉得例子很小,但數(shù)據(jù)庫訪問、外部緩存系統(tǒng)(memcache之類的)以及任何需要I/O的東西最終都會執(zhí)行某種類型的I/O調(diào)用,這跟示例的原理是一樣的。

影響項目中編程語言選擇的因素有很多,即使你只考慮性能方面,也存在很多的因素。但是,如果你擔心自己的程序主要受I/O的限制,并且性能是決定項目成功或者失敗的重要因素,那么,下文提到的幾點建議就是你需要重點考慮的。

“保持簡單”:PHP

早在上世紀90年代,有很多人穿著Converse鞋子使用Perl編寫CGI腳本。然后,PHP來了,很多人都喜歡它,它使得動態(tài)網(wǎng)頁的制作更加容易。

PHP使用的模型非常簡單。雖然不可能完全相同,但一般的PHP服務器原理是這樣的:

用戶瀏覽器發(fā)出一個HTTP請求,請求進入到Apache web服務器中。 Apache為每個請求創(chuàng)建一個單獨的進程,并通過一些優(yōu)化手段對這些進程進行重用,從而最大限度地減少原本需要執(zhí)行的操作(創(chuàng)建進程相對而言是比較慢的)。

Apache調(diào)用PHP并告訴它運行磁盤上的某個.php文件。

PHP代碼開始執(zhí)行,并阻塞I/O調(diào)用。你在PHP中調(diào)用的file_get_contents(),在底層實際上是調(diào)用了read()系統(tǒng)調(diào)用并等待返回的結果。

<?php// blocking file I/O$file_data = file_get_contents(‘/path/to/file.dat’);

// blocking network I/O$curl = curl_init('http://example.com/example-microservice');
$result = curl_exec($curl);

// some more blocking network I/O$result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100');

?>

很簡單:每個請求一個進程。 I/O調(diào)用是阻塞的。那么優(yōu)點呢?簡單而又有效。缺點呢?如果有20000個客戶端并發(fā),服務器將會癱瘓。這種方法擴展起來比較難,因為內(nèi)核提供的用于處理大量I/O(epoll等)的工具并沒有充分利用起來。更糟糕的是,為每個請求運行一個單獨的進程往往會占用大量的系統(tǒng)資源,尤其是內(nèi)存,這通常是第一個耗盡的。

*注意:在這一點上,Ruby的情況與PHP非常相似。

多線程:Java

所以,Java就出現(xiàn)了。而且Java在語言中內(nèi)置了多線程,特別是在創(chuàng)建線程時非常得棒。

大多數(shù)的Java Web服務器都會為每個請求啟動一個新的執(zhí)行線程,然后在這個線程中調(diào)用開發(fā)人員編寫的函數(shù)。

在Java Servlet中執(zhí)行I/O往往是這樣的:

publicvoiddoGet(HttpServletRequest request,
    HttpServletResponse response) throws ServletException, IOException
{

    // blocking file I/O
    InputStream fileIs = new FileInputStream("/path/to/file");

    // blocking network I/O
    URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection();
    InputStream netIs = urlConnection.getInputStream();

    // some more blocking network I/O
out.println("...");
}

由于上面的doGet方法對應于一個請求,并且在自己的線程中運行,而不是在需要有獨立內(nèi)存的單獨進程中運行,所以我們將創(chuàng)建一個單獨的線程。每個請求都會得到一個新的線程,并在該線程內(nèi)部阻塞各種I/O操作,直到請求處理完成。應用會創(chuàng)建一個線程池以最小化創(chuàng)建和銷毀線程的成本,但是,成千上萬的連接意味著有成千上萬的線程,這對于調(diào)度器來說并不件好事情。

值得注意的是,1.4版本的Java(1.7版本中又重新做了升級)增加了非阻塞I/O調(diào)用的能力。雖然大多數(shù)的應用程序都沒有使用這個特性,但它至少是可用的。一些Java Web服務器正在嘗試使用這個特性,但絕大部分已經(jīng)部署的Java應用程序仍然按照上面所述的原理進行工作。

Java提供了很多在I/O方面開箱即用的功能,但如果遇到創(chuàng)建大量阻塞線程執(zhí)行大量I/O操作的情況時,Java也沒有太好的解決方案。

把非阻塞I/O作為頭等大事:Node

在I/O方面表現(xiàn)比較好的、比較受用戶歡迎的是Node.js。任何一個對Node有簡單了解的人都知道,它是“非阻塞”的,并且能夠高效地處理I/O。這在一般意義上是正確的。但是細節(jié)和實現(xiàn)的方式至關重要。

在需要做一些涉及I/O的操作的時候,你需要發(fā)出請求,并給出一個回調(diào)函數(shù),Node會在處理完請求之后調(diào)用這個函數(shù)。

在請求中執(zhí)行I/O操作的典型代碼如下所示:

http.createServer(function(request, response) {
    fs.readFile('/path/to/file', 'utf8', function(err, data) {
        response.end(data);
    });
});

如上所示,這里有兩個回調(diào)函數(shù)。當請求開始時,第一個函數(shù)會被調(diào)用,而第二個函數(shù)是在文件數(shù)據(jù)可用時被調(diào)用。

這樣,Node就能更有效地處理這些回調(diào)函數(shù)的I/O。有一個更能說明問題的例子:在Node中調(diào)用數(shù)據(jù)庫操作。首先,你的程序開始調(diào)用數(shù)據(jù)庫操作,并給Node一個回調(diào)函數(shù),Node會使用非阻塞調(diào)用來單獨執(zhí)行I/O操作,然后在請求的數(shù)據(jù)可用時調(diào)用你的回調(diào)函數(shù)。這種對I/O調(diào)用進行排隊并讓Node處理I/O調(diào)用然后得到一個回調(diào)的機制稱為“事件循環(huán)”。這個機制非常不錯。

然而,這個模型有一個問題。在底層,這個問題出現(xiàn)的原因跟V8 JavaScript引擎(Node使用的是Chrome的JS引擎)的實現(xiàn)有關,即:你寫的JS代碼都運行在一個線程中。請思考一下。這意味著,盡管使用高效的非阻塞技術來執(zhí)行I/O,但是JS代碼在單個線程操作中運行基于CPU的操作,每個代碼塊都會阻塞下一個代碼塊的運行。有一個常見的例子:在數(shù)據(jù)庫記錄上循環(huán),以某種方式處理記錄,然后將它們輸出到客戶端。下面這段代碼展示了這個例子的原理:

var handler = function(request, response) {

    connection.query('SELECT ...', function(err, rows) {if (err) { throw err };

        for (var i = 0; i < rows.length; i++) {
            // do processing on each row
        }

        response.end(...); // write out the results

    })

};

雖然Node處理I/O的效率很高,但是上面例子中的for循環(huán)在一個主線程中使用了CPU周期。這意味著如果你有10000個連接,那么這個循環(huán)就可能會占用整個應用程序的時間。每個請求都必須要在主線程中占用一小段時間。

這整個概念的前提是I/O操作是最慢的部分,因此,即使串行處理是不得已的,但對它們進行有效處理也是非常重要的。這在某些情況下是成立的,但并非一成不變。

另一點觀點是,寫一堆嵌套的回調(diào)很麻煩,有些人認為這樣的代碼很丑陋。在Node代碼中嵌入四個、五個甚至更多層的回調(diào)并不罕見。

又到了權衡利弊的時候了。如果你的主要性能問題是I/O的話,那么這個Node模型能幫到你。但是,它的缺點在于,如果你在一個處理HTTP請求的函數(shù)中放入了CPU處理密集型代碼的話,一不小心就會讓每個連接都出現(xiàn)擁堵。

原生無阻塞:Go

在介紹Go之前,我透露一下,我是一個Go的粉絲。我已經(jīng)在許多項目中使用了Go。

讓我們看看它是如何處理I/O的吧。 Go語言的一個關鍵特性是它包含了自己的調(diào)度器。它并不會為每個執(zhí)行線程對應一個操作系統(tǒng)線程,而是使用了“goroutines”這個概念。Go運行時會為一個goroutine分配一個操作系統(tǒng)線程,并控制它執(zhí)行或暫停。Go HTTP服務器的每個請求都在一個單獨的Goroutine中進行處理。

實際上,除了回調(diào)機制被內(nèi)置到I/O調(diào)用的實現(xiàn)中并自動與調(diào)度器交互之外,Go運行時正在做的事情與Node不同。它也不會受到必須讓所有的處理代碼在同一個線程中運行的限制,Go會根據(jù)其調(diào)度程序中的邏輯自動將你的Goroutine映射到它認為合適的操作系統(tǒng)線程中。因此,它的代碼是這樣的:

func ServeHTTP(w http.ResponseWriter, r *http.Request) {

    // the underlying network call here is non-blocking
    rows, err := db.Query("SELECT ...")

    for _, row := range rows {
        // do something with the rows,// each request in its own goroutine
    }

    w.Write(...) // write the response, also non-blocking

}

如上所示,這樣的基本代碼結構更為簡單,而且還實現(xiàn)了非阻塞I/O。

在大多數(shù)情況下,這真正做到了“兩全其美”。非阻塞I/O可用于所有重要的事情,但是代碼卻看起來像是阻塞的,因此這樣往往更容易理解和維護。 剩下的就是Go調(diào)度程序和OS調(diào)度程序之間的交互處理了。這并不是魔法,如果你正在建立一個大型系統(tǒng),那么還是值得花時間去了解它的工作原理的。同時,“開箱即用”的特點使它能夠更好地工作和擴展。

Go可能也有不少缺點,但總的來說,它處理I/O的方式并沒有明顯的缺點。

性能評測

對于這些不同模型的上下文切換,很難進行準確的計時。當然,我也可以說這對你并沒有多大的用處。這里,我將對這些服務器環(huán)境下的HTTP服務進行基本的性能評測比較。請記住,端到端的HTTP請求/響應性能涉及到的因素有很多。

我針對每一個環(huán)境都寫了一段代碼來讀取64k文件中的隨機字節(jié),然后對其運行N次SHA-256散列(在URL的查詢字符串中指定N,例如.../test.php?n=100)并以十六進制打印結果。我之所以選擇這個,是因為它可以很容易運行一些持續(xù)的I/O操作,并且可以通過受控的方式來增加CPU使用率。

在這種存在大量連接和計算的情況下,我們看到的結果更多的是與語言本身的執(zhí)行有關。請注意,“腳本語言”的執(zhí)行速度最慢。

突然之間,由于每個請求中的CPU密集型操作相互阻塞,Node的性能顯著下降。有趣的是,在這個測試中,PHP的性能變得更好了(相對于其他),甚至優(yōu)于Java。 (值得注意的是,在PHP中,SHA-256的實現(xiàn)是用C語言編寫的,但執(zhí)行路徑在這個循環(huán)中花費了更多的時間,因為我們這次做了1000次哈希迭代)。

我猜測,在較高的連接數(shù)量下,PHP + Apache中產(chǎn)生新進程和內(nèi)存的申請似乎成為了影響PHP性能的主要因素。 很顯然,Go是這次的贏家,其次是Java,Node,最后是PHP。

雖然涉及到整體吞吐量的因素很多,而且應用程序和應用程序之間也存在著很大的差異,但是,越是了解底層的原理和所涉及的權衡問題,應用程序的表現(xiàn)就會越好。

總結

綜上所述,隨著語言的發(fā)展,處理大量I/O大型應用程序的解決方案也隨之發(fā)展。

公平地說,PHP和Java在web應用方面都有可用的非阻塞I/O的實現(xiàn)。但是這些實現(xiàn)并不像上面描述的方法那么使用廣泛,并且還需要考慮維護上的開銷。更不用說應用程序的代碼必須以適合這種環(huán)境的方式來構建。

我們來比較一下幾個影響性能和易用性的重要因素:

語言線程與進程非阻塞I/O易于使用
PHP進程-
Java線程有效需要回調(diào)
Node.js線程需要回調(diào)
Go線程 (Goroutines)無需回調(diào)

因為線程會共享相同的內(nèi)存空間,而進程不會,所以線程通常要比進程的內(nèi)存效率高得多。在上面的列表中,從上往下看,與I/O相關的因素一個比一個好。所以,如果我不得不在上面的比較中選擇一個贏家,那肯定選Go。

即便如此,在實踐中,選擇構建應用程序的環(huán)境與你團隊對環(huán)境的熟悉程度以及團隊可以實現(xiàn)的整體生產(chǎn)力密切相關。所以,對于團隊來說,使用Node或Go來開發(fā)Web應用程序和服務可能并不是最好的選擇。

到此,相信大家對“Node、PHP、Java和Go服務端I/O性能對比”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關內(nèi)容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI