新聞中心
了解應(yīng)用程序的輸入/輸出(I/O)模型意味著理解應(yīng)用程序處理其數(shù)據(jù)的載入差異,并揭示其在真實(shí)環(huán)境中表現(xiàn)?;蛟S你的應(yīng)用程序很小,在不承受很大的負(fù)載時(shí),這并不是個(gè)嚴(yán)重的問(wèn)題;但隨著應(yīng)用程序的流量負(fù)載增加,可能因?yàn)槭褂昧说托У?I/O 模型導(dǎo)致承受不了而崩潰。

和大多數(shù)情況一樣,處理這種問(wèn)題的方法有多種方式,這不僅僅是一個(gè)擇優(yōu)的問(wèn)題,而是對(duì)權(quán)衡的理解問(wèn)題。 接下來(lái)我們來(lái)看看 I/O 到底是什么。
在本文中,我們將對(duì) Node、Java、Go 和 PHP + Apache 進(jìn)行對(duì)比,討論不同語(yǔ)言如何構(gòu)造其 I/O ,每個(gè)模型的優(yōu)缺點(diǎn),并總結(jié)一些基本的規(guī)律。如果你擔(dān)心你的下一個(gè) Web 應(yīng)用程序的 I/O 性能,本文將給你***的解答。
I/O 基礎(chǔ)知識(shí): 快速?gòu)?fù)習(xí)
要了解 I/O 所涉及的因素,我們首先深入到操作系統(tǒng)層面復(fù)習(xí)這些概念。雖然看起來(lái)并不與這些概念直接打交道,但你會(huì)一直通過(guò)應(yīng)用程序的運(yùn)行時(shí)環(huán)境與它們間接接觸。了解細(xì)節(jié)很重要。
系統(tǒng)調(diào)用
首先是系統(tǒng)調(diào)用,其被描述如下:
- 程序(所謂“用戶端user land”)必須請(qǐng)求操作系統(tǒng)內(nèi)核代表它執(zhí)行 I/O 操作。
- “系統(tǒng)調(diào)用syscall”是你的程序要求內(nèi)核執(zhí)行某些操作的方法。這些實(shí)現(xiàn)的細(xì)節(jié)在操作系統(tǒng)之間有所不同,但基本概念是相同的。有一些具體的指令會(huì)將控制權(quán)從你的程序轉(zhuǎn)移到內(nèi)核(類(lèi)似函數(shù)調(diào)用,但是使用專(zhuān)門(mén)用于處理這種情況的專(zhuān)用方式)。一般來(lái)說(shuō),系統(tǒng)調(diào)用會(huì)被阻塞,這意味著你的程序會(huì)等待內(nèi)核返回(控制權(quán)到)你的代碼。
- 內(nèi)核在所需的物理設(shè)備( 磁盤(pán)、網(wǎng)卡等 )上執(zhí)行底層 I/O 操作,并回應(yīng)系統(tǒng)調(diào)用。在實(shí)際情況中,內(nèi)核可能需要做許多事情來(lái)滿足你的要求,包括等待設(shè)備準(zhǔn)備就緒、更新其內(nèi)部狀態(tài)等,但作為應(yīng)用程序開(kāi)發(fā)人員,你不需要關(guān)心這些。這是內(nèi)核的工作。
Syscalls Diagram
阻塞與非阻塞
上面我們提到過(guò),系統(tǒng)調(diào)用是阻塞的,一般來(lái)說(shuō)是這樣的。然而,一些調(diào)用被歸類(lèi)為“非阻塞”,這意味著內(nèi)核會(huì)接收你的請(qǐng)求,將其放在隊(duì)列或緩沖區(qū)之類(lèi)的地方,然后立即返回而不等待實(shí)際的 I/O 發(fā)生。所以它只是在很短的時(shí)間內(nèi)“阻塞”,只需要排隊(duì)你的請(qǐng)求即可。
舉一些 Linux 系統(tǒng)調(diào)用的例子可能有助于理解:
- read() 是一個(gè)阻塞調(diào)用 - 你傳遞一個(gè)句柄,指出哪個(gè)文件和緩沖區(qū)在哪里傳送它所讀取的數(shù)據(jù),當(dāng)數(shù)據(jù)就緒時(shí),該調(diào)用返回。這種方式的優(yōu)點(diǎn)是簡(jiǎn)單友好。
- 分別調(diào)用 epoll_create()、epoll_ctl() 和 epoll_wait() ,你可以創(chuàng)建一組句柄來(lái)偵聽(tīng)、添加/刪除該組中的處理程序、然后阻塞直到有任何事件發(fā)生。這允許你通過(guò)單個(gè)線程有效地控制大量的 I/O 操作,但是現(xiàn)在談這個(gè)還太早。如果你需要這個(gè)功能當(dāng)然好,但須知道它使用起來(lái)是比較復(fù)雜的。
了解這里的時(shí)間差異的數(shù)量級(jí)是很重要的。假設(shè) CPU 內(nèi)核運(yùn)行在 3GHz,在沒(méi)有進(jìn)行 CPU 優(yōu)化的情況下,那么它每秒執(zhí)行 30 億次周期cycle(即每納秒 3 個(gè)周期)。非阻塞系統(tǒng)調(diào)用可能需要幾十個(gè)周期來(lái)完成,或者說(shuō) “相對(duì)少的納秒” 時(shí)間完成。而一個(gè)被跨網(wǎng)絡(luò)接收信息所阻塞的系統(tǒng)調(diào)用可能需要更長(zhǎng)的時(shí)間 - 例如 200 毫秒(1/5 秒)。這就是說(shuō),如果非阻塞調(diào)用需要 20 納秒,阻塞調(diào)用需要 2 億納秒。你的進(jìn)程因阻塞調(diào)用而等待了 1000 萬(wàn)倍的時(shí)長(zhǎng)!
Blocking vs. Non-blocking Syscalls
內(nèi)核既提供了阻塞 I/O (“從網(wǎng)絡(luò)連接讀取并給出數(shù)據(jù)”),也提供了非阻塞 I/O (“告知我何時(shí)這些網(wǎng)絡(luò)連接具有新數(shù)據(jù)”)的方法。使用的是哪種機(jī)制對(duì)調(diào)用進(jìn)程的阻塞時(shí)長(zhǎng)有截然不同的影響。
調(diào)度
關(guān)鍵的第三件事是當(dāng)你有很多線程或進(jìn)程開(kāi)始阻塞時(shí)會(huì)發(fā)生什么。
根據(jù)我們的理解,線程和進(jìn)程之間沒(méi)有很大的區(qū)別。在現(xiàn)實(shí)生活中,最顯著的性能相關(guān)的差異在于,由于線程共享相同的內(nèi)存,而進(jìn)程每個(gè)都有自己的內(nèi)存空間,使得單獨(dú)的進(jìn)程往往占用更多的內(nèi)存。但是當(dāng)我們談?wù)撜{(diào)度Scheduling時(shí),它真正歸結(jié)為一類(lèi)事情(線程和進(jìn)程類(lèi)同),每個(gè)都需要在可用的 CPU 內(nèi)核上獲得一段執(zhí)行時(shí)間。如果你有 300 個(gè)線程運(yùn)行在 8 個(gè)內(nèi)核上,則必須將時(shí)間分成幾份,以便每個(gè)線程和進(jìn)程都能分享它,每個(gè)運(yùn)行一段時(shí)間,然后交給下一個(gè)。這是通過(guò) “上下文切換context switch” 完成的,可以使 CPU 從運(yùn)行到一個(gè)線程/進(jìn)程到切換下一個(gè)。
這些上下文切換也有相關(guān)的成本 - 它們需要一些時(shí)間。在某些快速的情況下,它可能小于 100 納秒,但根據(jù)實(shí)際情況、處理器速度/體系結(jié)構(gòu)、CPU 緩存等,偶見(jiàn)花費(fèi) 1000 納秒或更長(zhǎng)時(shí)間。
而線程(或進(jìn)程)越多,上下文切換就越多。當(dāng)我們涉及數(shù)以千計(jì)的線程時(shí),每個(gè)線程花費(fèi)數(shù)百納秒,就會(huì)變得很慢。
然而,非阻塞調(diào)用實(shí)質(zhì)上是告訴內(nèi)核“僅在這些連接之一有新的數(shù)據(jù)或事件時(shí)再叫我”。這些非阻塞調(diào)用旨在有效地處理大量 I/O 負(fù)載并減少上下文交換。
這些你明白了么?現(xiàn)在來(lái)到了真正有趣的部分:我們來(lái)看看一些流行的語(yǔ)言對(duì)那些工具的使用,并得出關(guān)于易用性和性能之間權(quán)衡的結(jié)論,以及一些其他有趣小東西。
聲明,本文中顯示的示例是零碎的(片面的,只能體現(xiàn)相關(guān)的信息); 數(shù)據(jù)庫(kù)訪問(wèn)、外部緩存系統(tǒng)( memcache 等等)以及任何需要 I/O 的東西都將執(zhí)行某種類(lèi)型的 I/O 調(diào)用,其實(shí)質(zhì)與上面所示的簡(jiǎn)單示例效果相同。此外,對(duì)于將 I/O 描述為“阻塞”( PHP、Java )的情況,HTTP 請(qǐng)求和響應(yīng)讀取和寫(xiě)入本身就是阻塞調(diào)用:系統(tǒng)中隱藏著更多 I/O 及其伴生的性能問(wèn)題需要考慮。
為一個(gè)項(xiàng)目選擇編程語(yǔ)言要考慮很多因素。甚至當(dāng)你只考慮效率時(shí),也有很多因素。但是,如果你擔(dān)心你的程序?qū)⒅饕艿?I/O 的限制,如果 I/O 性能影響到項(xiàng)目的成敗,那么這些是你需要了解的。
“保持簡(jiǎn)單”方法:PHP
早在 90 年代,很多人都穿著 Converse 鞋,用 Perl 寫(xiě)著 CGI 腳本。然后 PHP 來(lái)了,就像一些人喜歡咒罵的一樣,它使得動(dòng)態(tài)網(wǎng)頁(yè)更容易。
PHP 使用的模型相當(dāng)簡(jiǎn)單。雖有一些出入,但你的 PHP 服務(wù)器基本上是這樣:
HTTP 請(qǐng)求來(lái)自用戶的瀏覽器,并訪問(wèn)你的 Apache Web 服務(wù)器。Apache 為每個(gè)請(qǐng)求創(chuàng)建一個(gè)單獨(dú)的進(jìn)程,有一些優(yōu)化方式可以重新使用它們,以***限度地減少創(chuàng)建次數(shù)( 相對(duì)而言,創(chuàng)建進(jìn)程較慢 )。Apache 調(diào)用 PHP 并告訴它運(yùn)行磁盤(pán)上合適的 .php 文件。PHP 代碼執(zhí)行并阻塞 I/O 調(diào)用。你在 PHP 中調(diào)用 file_get_contents() ,其底層會(huì)調(diào)用 read() 系統(tǒng)調(diào)用并等待結(jié)果。
當(dāng)然,實(shí)際的代碼是直接嵌入到你的頁(yè)面,并且該操作被阻塞:
- // 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');
- ?>
關(guān)于如何與系統(tǒng)集成,就像這樣:
I/O Model PHP
很簡(jiǎn)單:每個(gè)請(qǐng)求一個(gè)進(jìn)程。 I/O 調(diào)用就阻塞。優(yōu)點(diǎn)是簡(jiǎn)單可工作,缺點(diǎn)是,同時(shí)與 20,000 個(gè)客戶端連接,你的服務(wù)器將會(huì)崩潰。這種方法不能很好地?cái)U(kuò)展,因?yàn)閮?nèi)核提供的用于處理大容量 I/O (epoll 等) 的工具沒(méi)有被使用。 雪上加霜的是,為每個(gè)請(qǐng)求運(yùn)行一個(gè)單獨(dú)的進(jìn)程往往會(huì)使用大量的系統(tǒng)資源,特別是內(nèi)存,這通常是你在這樣的場(chǎng)景中遇到的***個(gè)問(wèn)題。
注意:Ruby 使用的方法與 PHP 非常相似,在大致的方面上,它們可以被認(rèn)為是相同的。
多線程方法: Java
就在你購(gòu)買(mǎi)你的***個(gè)域名,在某個(gè)句子后很酷地隨機(jī)說(shuō)出 “dot com” 的那個(gè)時(shí)候,Java 來(lái)了。而 Java 具有內(nèi)置于該語(yǔ)言中的多線程功能,它非常棒(特別是在創(chuàng)建時(shí))。
大多數(shù) Java Web 服務(wù)器通過(guò)為每個(gè)請(qǐng)求啟動(dòng)一個(gè)新的執(zhí)行線程,然后在該線程中最終調(diào)用你(作為應(yīng)用程序開(kāi)發(fā)人員)編寫(xiě)的函數(shù)。
在 Java Servlet 中執(zhí)行 I/O 往往看起來(lái)像:
- public void doGet(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 方法對(duì)應(yīng)于一個(gè)請(qǐng)求,并且在其自己的線程中運(yùn)行,而不是每個(gè)請(qǐng)求一個(gè)單獨(dú)的進(jìn)程,申請(qǐng)自己的內(nèi)存。這樣有一些好處,比如在線程之間共享狀態(tài)、緩存數(shù)據(jù)等,因?yàn)樗鼈兛梢栽L問(wèn)彼此的內(nèi)存,但是它與調(diào)度的交互影響與之前的 PHP 的例子幾乎相同。每個(gè)請(qǐng)求獲得一個(gè)新線程,該線程內(nèi)的各種 I/O 操作阻塞在線程內(nèi),直到請(qǐng)求被完全處理為止。線程被池化以最小化創(chuàng)建和銷(xiāo)毀它們的成本,但是數(shù)千個(gè)連接仍然意味著數(shù)千個(gè)線程,這對(duì)調(diào)度程序是不利的。
重要的里程碑出現(xiàn)在 Java 1.4 版本(以及 1.7 的重要升級(jí))中,它獲得了執(zhí)行非阻塞 I/O 調(diào)用的能力。大多數(shù)應(yīng)用程序、web 應(yīng)用和其它用途不會(huì)使用它,但至少它是可用的。一些 Java Web 服務(wù)器嘗試以各種方式利用這一點(diǎn);然而,絕大多數(shù)部署的 Java 應(yīng)用程序仍然如上所述工作。
I/O Model Java
肯定有一些很好的開(kāi)箱即用的 I/O 功能,Java 讓我們更接近,但它仍然沒(méi)有真正解決當(dāng)你有一個(gè)大量的 I/O 綁定的應(yīng)用程序被數(shù)千個(gè)阻塞線程所壓垮的問(wèn)題。
無(wú)阻塞 I/O 作為一等公民: Node
當(dāng)更好的 I/O 模式來(lái)到 Node.js,阻塞才真正被解決。任何一個(gè)曾聽(tīng)過(guò) Node 簡(jiǎn)單介紹的人都被告知這是“非阻塞”,可以有效地處理 I/O。這在一般意義上是正確的。但在細(xì)節(jié)中則不盡然,而且當(dāng)在進(jìn)行性能工程時(shí),這種巫術(shù)遇到了問(wèn)題。
Node 實(shí)現(xiàn)的范例基本上不是說(shuō) “在這里寫(xiě)代碼來(lái)處理請(qǐng)求”,而是說(shuō) “在這里寫(xiě)代碼來(lái)開(kāi)始處理請(qǐng)求”。每次你需要做一些涉及到 I/O 的操作,你會(huì)創(chuàng)建一個(gè)請(qǐng)求并給出一個(gè)回調(diào)函數(shù),Node 將在完成之后調(diào)用該函數(shù)。
在請(qǐng)求中執(zhí)行 I/O 操作的典型 Node 代碼如下所示:
- http.createServer(function(request, response) {
- fs.readFile('/path/to/file', 'utf8', function(err, data) {
- response.end(data);
- });
- });
你可以看到,這里有兩個(gè)回調(diào)函數(shù)。當(dāng)請(qǐng)求開(kāi)始時(shí),***個(gè)被調(diào)用,當(dāng)文件數(shù)據(jù)可用時(shí),第二個(gè)被調(diào)用。
這樣做的基本原理是讓 Node 有機(jī)會(huì)有效地處理這些回調(diào)之間的 I/O 。一個(gè)更加密切相關(guān)的場(chǎng)景是在 Node 中進(jìn)行數(shù)據(jù)庫(kù)調(diào)用,但是我不會(huì)在這個(gè)例子中啰嗦,因?yàn)樗裱耆嗤脑瓌t:?jiǎn)?dòng)數(shù)據(jù)庫(kù)調(diào)用,并給 Node 一個(gè)回調(diào)函數(shù),它使用非阻塞調(diào)用單獨(dú)執(zhí)行 I/O 操作,然后在你要求的數(shù)據(jù)可用時(shí)調(diào)用回調(diào)函數(shù)。排隊(duì) I/O 調(diào)用和讓 Node 處理它然后獲取回調(diào)的機(jī)制稱(chēng)為“事件循環(huán)”。它工作的很好。
I/O Model Node.js
然而,這個(gè)模型有一個(gè)陷阱,究其原因,很多是與 V8 JavaScript 引擎(Node 用的是 Chrome 瀏覽器的 JS 引擎)如何實(shí)現(xiàn)的有關(guān)注1 。你編寫(xiě)的所有 JS 代碼都運(yùn)行在單個(gè)線程中。你可以想想,這意味著當(dāng)使用高效的非阻塞技術(shù)執(zhí)行 I/O 時(shí),你的 JS 可以在單個(gè)線程中運(yùn)行計(jì)算密集型的操作,每個(gè)代碼塊都會(huì)阻塞下一個(gè)??赡艹霈F(xiàn)這種情況的一個(gè)常見(jià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++) {
- // do processing on each row
- }
- response.end(...); // write out the results
- })
- };
雖然 Node 確實(shí)有效地處理了 I/O ,但是上面的例子中 for 循環(huán)是在你的唯一的一個(gè)主線程中占用 CPU 周期。這意味著如果你有 10,000 個(gè)連接,則該循環(huán)可能會(huì)使你的整個(gè)應(yīng)用程序像爬行般緩慢,具體取決于其會(huì)持續(xù)多久。每個(gè)請(qǐng)求必須在主線程中分享一段時(shí)間,一次一段。
這整個(gè)概念的前提是 I/O 操作是最慢的部分,因此最重要的是要有效地處理這些操作,即使這意味著要連續(xù)進(jìn)行其他處理。這在某些情況下是正確的,但不是全部。
另一點(diǎn)是,雖然這只是一個(gè)觀點(diǎn),但是寫(xiě)一堆嵌套的回調(diào)可能是相當(dāng)令人討厭的,有些則認(rèn)為它使代碼更難以追蹤。在 Node 代碼中看到回調(diào)嵌套 4 層、5 層甚至更多層并不罕見(jiàn)。
我們?cè)俅蝸?lái)權(quán)衡一下。如果你的主要性能問(wèn)題是 I/O,則 Node 模型工作正常。然而,它的關(guān)鍵是,你可以在一個(gè)處理 HTTP 請(qǐng)求的函數(shù)里面放置 CPU 密集型的代碼,而且不小心的話會(huì)導(dǎo)致每個(gè)連接都很慢。
最自然的非阻塞:Go
在我進(jìn)入 Go 部分之前,我應(yīng)該披露我是一個(gè) Go 的粉絲。我已經(jīng)在許多項(xiàng)目中使用過(guò)它,我是一個(gè)其生產(chǎn)力優(yōu)勢(shì)的公開(kāi)支持者,我在我的工作中使用它。
那么,讓我們來(lái)看看它是如何處理 I/O 的。Go 語(yǔ)言的一個(gè)關(guān)鍵特征是它包含自己的調(diào)度程序。在 Go 中,不是每個(gè)執(zhí)行線程對(duì)應(yīng)于一個(gè)單一的 OS 線程,其通過(guò)一種叫做 “協(xié)程goroutine” 的概念來(lái)工作。而 Go 的運(yùn)行時(shí)可以將一個(gè)協(xié)程分配給一個(gè) OS 線程,使其執(zhí)行或暫停它,并且它不與一個(gè) OS 線程相關(guān)聯(lián)——這要基于那個(gè)協(xié)程正在做什么。來(lái)自 Go 的 HTTP 服務(wù)器的每個(gè)請(qǐng)求都在單獨(dú)的協(xié)程中處理。
調(diào)度程序的工作原理如圖所示:
I/O Model Go
在底層,這是通過(guò) Go 運(yùn)行時(shí)中的各個(gè)部分實(shí)現(xiàn)的,它通過(guò)對(duì)請(qǐng)求的寫(xiě)入/讀取/連接等操作來(lái)實(shí)現(xiàn) I/O 調(diào)用,將當(dāng)前協(xié)程休眠,并當(dāng)采取進(jìn)一步動(dòng)作時(shí)喚醒該協(xié)程。
從效果上看,Go 運(yùn)行時(shí)做的一些事情與 Node 做的沒(méi)有太大不同,除了回調(diào)機(jī)制是內(nèi)置到 I/O 調(diào)用的實(shí)現(xiàn)中,并自動(dòng)與調(diào)度程序交互。它也不會(huì)受到必須讓所有處理程序代碼在同一個(gè)線程中運(yùn)行的限制,Go 將根據(jù)其調(diào)度程序中的邏輯自動(dòng)將協(xié)程映射到其認(rèn)為適當(dāng)?shù)?OS 線程。結(jié)果是這樣的代碼:
- 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
- }
如上所述,我們重構(gòu)基本的代碼結(jié)構(gòu)為更簡(jiǎn)化的方式,并在底層仍然實(shí)現(xiàn)了非阻塞 I/O。
在大多數(shù)情況下,最終是“兩全其美”的。非阻塞 I/O 用于所有重要的事情,但是你的代碼看起來(lái)像是阻塞,因此更容易理解和維護(hù)。Go 調(diào)度程序和 OS 調(diào)度程序之間的交互處理其余部分。這不是完整的魔法,如果你建立一個(gè)大型系統(tǒng),那么值得我們來(lái)看看有關(guān)它的工作原理的更多細(xì)節(jié);但與此同時(shí),你獲得的“開(kāi)箱即用”的環(huán)境可以很好地工作和擴(kuò)展。
Go 可能有其缺點(diǎn),但一般來(lái)說(shuō),它處理 I/O 的方式不在其中。
謊言,可惡的謊言和基準(zhǔn)
對(duì)這些各種模式的上下文切換進(jìn)行準(zhǔn)確的定時(shí)是很困難的。我也可以認(rèn)為這對(duì)你來(lái)說(shuō)不太有用。相反,我會(huì)給出一些比較這些服務(wù)器環(huán)境的整個(gè) HTTP 服務(wù)器性能的基本基準(zhǔn)。請(qǐng)記住,影響整個(gè)端到端 HTTP 請(qǐng)求/響應(yīng)路徑的性能有很多因素,這里提供的數(shù)字只是我將一些樣本放在一起進(jìn)行基本比較的結(jié)果。
對(duì)于這些環(huán)境中的每一個(gè),我寫(xiě)了適當(dāng)?shù)拇a在一個(gè) 64k 文件中讀取隨機(jī)字節(jié),在其上運(yùn)行了一個(gè) SHA-256 哈希 N 次( N 在 URL 的查詢(xún)字符串中指定,例如 .../test.php?n=100),并打印出結(jié)果十六進(jìn)制散列。我選擇這樣做,是因?yàn)槭褂靡恍┮恢碌?I/O 和受控的方式來(lái)運(yùn)行相同的基準(zhǔn)測(cè)試是一個(gè)增加 CPU 使用率的非常簡(jiǎn)單的方法。
有關(guān)使用的環(huán)境的更多細(xì)節(jié),請(qǐng)參閱 基準(zhǔn)說(shuō)明 。
首先,我們來(lái)看一些低并發(fā)的例子。運(yùn)行 2000 次迭代,300 個(gè)并發(fā)請(qǐng)求,每個(gè)請(qǐng)求只有一個(gè)散列(N = 1),結(jié)果如下:
時(shí)間是在所有并發(fā)請(qǐng)求中完成請(qǐng)求的平均毫秒數(shù)。越低越好。
僅從一張圖很難得出結(jié)論,但是對(duì)我來(lái)說(shuō),似乎在大量的連接和計(jì)算量上,我們看到時(shí)間更多地與語(yǔ)言本身的一般執(zhí)行有關(guān),對(duì)于 I/O 更是如此。請(qǐng)注意,那些被視為“腳本語(yǔ)言”的語(yǔ)言(松散類(lèi)型,動(dòng)態(tài)解釋)執(zhí)行速度最慢。
但是,如果我們將 N 增加到 1000,仍然有 300 個(gè)并發(fā)請(qǐng)求,相同的任務(wù),但是哈希迭代是 1000 倍(顯著增加了 CPU 負(fù)載):
時(shí)間是在所有并發(fā)請(qǐng)求中完成請(qǐng)求的平均毫秒數(shù)。越低越好。
突然間, Node 性能顯著下降,因?yàn)槊總€(gè)請(qǐng)求中的 CPU 密集型操作都相互阻塞。有趣的是,在這個(gè)測(cè)試中,PHP 的性能要好得多(相對(duì)于其他的),并且打敗了 Java。(值得注意的是,在 PHP 中,SHA-256 實(shí)現(xiàn)是用 C 編寫(xiě)的,在那個(gè)循環(huán)中執(zhí)行路徑花費(fèi)了更多的時(shí)間,因?yàn)楝F(xiàn)在我們正在進(jìn)行 1000 個(gè)哈希迭代)。
現(xiàn)在讓我們嘗試 5000 個(gè)并發(fā)連接(N = 1) - 或者是我可以發(fā)起的***連接。不幸的是,對(duì)于大多數(shù)這些環(huán)境,故障率并不顯著。對(duì)于這個(gè)圖表,我們來(lái)看每秒的請(qǐng)求總數(shù)。 越高越好 :
每秒請(qǐng)求數(shù)。越高越好。
這個(gè)圖看起來(lái)有很大的不同。我猜測(cè),但是看起來(lái)像在高連接量時(shí),產(chǎn)生新進(jìn)程所涉及的每連接開(kāi)銷(xiāo)以及與 PHP + Apache 相關(guān)聯(lián)的附加內(nèi)存似乎成為主要因素,并阻止了 PHP 的性能。顯然,Go 是這里的贏家,其次是 Java,Node,***是 PHP。
雖然與你的整體吞吐量相關(guān)的因素很多,并且在應(yīng)用程序之間也有很大的差異,但是你對(duì)底層發(fā)生什么的事情以及所涉及的權(quán)衡了解更多,你將會(huì)得到更好的結(jié)果。
總結(jié)
以上所有這一切,很顯然,隨著語(yǔ)言的發(fā)展,處理大量 I/O 的大型應(yīng)用程序的解決方案也隨之發(fā)展。
為了公平起見(jiàn),PHP 和 Java,盡管這篇文章中的描述,確實(shí) 實(shí)現(xiàn)了 在 web 應(yīng)用程序 中 可使用的 非阻塞 I/O 。但是這些方法并不像上述方法那么常見(jiàn),并且需要考慮使用這種方法來(lái)維護(hù)服務(wù)器的隨之而來(lái)的操作開(kāi)銷(xiāo)。更不用說(shuō)你的代碼必須以與這些環(huán)境相適應(yīng)的方式進(jìn)行結(jié)構(gòu)化;你的 “正常” PHP 或 Java Web 應(yīng)用程序通常不會(huì)在這樣的環(huán)境中進(jìn)行重大修改。
作為比較,如果我們考慮影響性能和易用性的幾個(gè)重要因素,我們得出以下結(jié)論:
< 如顯示不全,請(qǐng)左右滑動(dòng) >
| 語(yǔ)言 | 線程與進(jìn)程 | 非阻塞 I/O | 使用便捷性 |
|---|---|---|---|
| PHP | 進(jìn)程 | 否 | |
| Java | 線程 | 可用 | 需要回調(diào) |
| Node.js | 線程 | 是 | 需要回調(diào) |
| Go | 線程 (協(xié)程) | 是 | 不需要回調(diào) |
線程通常要比進(jìn)程有更高的內(nèi)存效率,因?yàn)樗鼈児蚕硐嗤膬?nèi)存空間,而進(jìn)程則沒(méi)有。結(jié)合與非阻塞 I/O 相關(guān)的因素,我們可以看到,至少考慮到上述因素,當(dāng)我們從列表往下看時(shí),與 I/O 相關(guān)的一般設(shè)置得到改善。所以如果我不得不在上面的比賽中選擇一個(gè)贏家,那肯定會(huì)是 Go。
即使如此,在實(shí)踐中,選擇構(gòu)建應(yīng)用程序的環(huán)境與你的團(tuán)隊(duì)對(duì)所述環(huán)境的熟悉程度以及你可以實(shí)現(xiàn)的總體生產(chǎn)力密切相關(guān)。因此,每個(gè)團(tuán)隊(duì)都深入并開(kāi)始在 Node 或 Go 中開(kāi)發(fā) Web 應(yīng)用程序和服務(wù)可能就沒(méi)有意義。事實(shí)上,尋找開(kāi)發(fā)人員或你內(nèi)部團(tuán)隊(duì)的熟悉度通常被認(rèn)為是不使用不同語(yǔ)言和/或環(huán)境的主要原因。也就是說(shuō),過(guò)去十五年來(lái),時(shí)代已經(jīng)發(fā)生了變化。
希望以上內(nèi)容可以幫助你更清楚地了解底層發(fā)生的情況,并為你提供如何處理應(yīng)用程序的現(xiàn)實(shí)可擴(kuò)展性的一些想法。
網(wǎng)站題目:服務(wù)端I/O性能:Node、PHP、Java、Go的對(duì)比
鏈接地址:http://m.5511xx.com/article/dhpecic.html


咨詢(xún)
建站咨詢(xún)
