服务端 I/O 性能大比拼:Node、PHP、Java、Go哪家强?
理解應(yīng)用程序的輸入/輸出(I/O)模型,意味著其在計劃處理負(fù)載與殘酷的實際使用場景之間的差異。若應(yīng)用程序比較小,也沒有服務(wù)于很高的負(fù)載,也許它影響甚微。但隨著應(yīng)用程序的負(fù)載逐漸上漲,采用錯誤的I/O模型有可能會讓你到處踩坑,傷痕累累。
正如大部分存在多種解決途徑的場景一樣,重點不在于哪一種途徑更好,而是在于理解如何進(jìn)行權(quán)衡。讓我們來參觀下I/O的景觀,看下可以從中竊取點什么。
在這篇文章,我們將會結(jié)合Apache分別比較Node,Java,Go,和PHP,討論這些不同的語言如何對他們的I/O進(jìn)行建模,各個模型的優(yōu)點和缺點,并得出一些初步基準(zhǔn)的結(jié)論。如果關(guān)心你下一個Web應(yīng)用的I/O性能,那你就找對文章了。
I/O基礎(chǔ)知識:快速回顧
為了理解與I/O密切相關(guān)的因素,必須先來回顧在操作系統(tǒng)底層的概念。雖然不會直接處理這些概念的大部分,但通過應(yīng)用程序的運行時環(huán)境你一直在間接地處理他們。而關(guān)鍵在于細(xì)節(jié)。推薦:詳解 Java 中 4 種 I/O 模型。
系統(tǒng)調(diào)用
首先,我們有系統(tǒng)調(diào)用,它可以描述成這樣:
-
你的程序(在“用戶區(qū)域”,正如他們所說的)必須讓操作系統(tǒng)內(nèi)核在它自身執(zhí)行I/O操作。
-
“系統(tǒng)調(diào)用”(syscall)意味著你的程序要求內(nèi)核做某事。不同的操作系統(tǒng),實現(xiàn)系統(tǒng)調(diào)用的細(xì)節(jié)有所不同,但基本的概念是一樣的。這將會有一些特定的指令,把控制權(quán)從你的程序轉(zhuǎn)交到內(nèi)核(類似函數(shù)調(diào)用但有一些專門用于處理這種場景的特殊sauce)。通常來說,系統(tǒng)調(diào)用是阻塞的,意味著你的程序需要等待內(nèi)核返回到你的代碼。
-
內(nèi)核在我們所說的物理設(shè)備(硬盤、網(wǎng)卡等)上執(zhí)行底層的I/O操作,并回復(fù)給系統(tǒng)調(diào)用。在現(xiàn)實世界中,內(nèi)核可能需要做很多事情才能完成你的請求,包括等待設(shè)備準(zhǔn)備就緒,更新它的內(nèi)部狀態(tài)等,但作為一名應(yīng)用程序開發(fā)人員,你可以不用關(guān)心這些。以下是內(nèi)核的工作情況。
阻塞調(diào)用與非阻塞調(diào)用
好了,我剛剛在上面說系統(tǒng)調(diào)用是阻塞的,通常來說這是對的。然而,有些調(diào)用被分類為“非阻塞”,意味著內(nèi)核接收了你的請求后,把它放進(jìn)了隊列或者緩沖的某個地方,然后立即返回而并沒有等待實際的I/O調(diào)用。所以它只是“阻塞”了一段非常短的時間,短到只是把你的請求入列而已。
這里有一些有助于解釋清楚的(Linux系統(tǒng)調(diào)用)例子:-read()是阻塞調(diào)用——你傳給它一個文件句柄和一個存放所讀到數(shù)據(jù)的緩沖,然后此調(diào)用會在當(dāng)數(shù)據(jù)好后返回。注意這種方式有著優(yōu)雅和簡單的優(yōu)點。
-epoll_create(),epoll_ctl(),和epoll_wait()這些調(diào)用分別是,讓你創(chuàng)建一組用于偵聽的句柄,從該組添加/刪除句柄,和然后直到有活動時才阻塞。這使得你可以通過一個線程有效地控制一系列I/O操作。如果需要這些功能,這非常棒,但也正如你所看到的,使用起來當(dāng)然也相當(dāng)復(fù)雜。
理解這里分時差異的數(shù)量級是很重要的。如果一個CPU內(nèi)核運行在3GHz,在沒有優(yōu)化的情況下,它每秒執(zhí)行30億次循環(huán)(或者每納秒3次循環(huán))。非阻塞系統(tǒng)調(diào)用可能需要10納秒這樣數(shù)量級的周期才能完成——或者“相對較少的納秒”。
對于正在通過網(wǎng)絡(luò)接收信息的阻塞調(diào)用可能需要更多的時間——例如200毫秒(0.2秒)。例如,假設(shè)非阻塞調(diào)用消耗了20納秒,那么阻塞調(diào)用消耗了200,000,000納秒。對于阻塞調(diào)用,你的程序多等待了1000萬倍的時間。
內(nèi)核提供了阻塞I/O(“從網(wǎng)絡(luò)連接中讀取并把數(shù)據(jù)給我”)和非阻塞I/O(“當(dāng)這些網(wǎng)絡(luò)連接有新數(shù)據(jù)時就告訴我”)這兩種方法。而使用何種機(jī)制,對應(yīng)調(diào)用過程的阻塞時間明顯長度不同。
調(diào)度
接下來第三件關(guān)鍵的事情是,當(dāng)有大量線程或進(jìn)程開始阻塞時怎么辦。
出于我們的目的,線程和進(jìn)程之間沒有太大的區(qū)別。實際上,最顯而易見的執(zhí)行相關(guān)的區(qū)別是,線程共享相同的內(nèi)存,而每個進(jìn)程則擁有他們獨自的內(nèi)存空間,使得分離的進(jìn)程往往占據(jù)了大量的內(nèi)存。
但當(dāng)我們討論調(diào)度時,它最終可歸結(jié)為一個事件清單(線程和進(jìn)程類似),其中每個事件需要在有效的CPU內(nèi)核上獲得一片執(zhí)行時間。如果你有300個線程正在運行并且運行在8核上,那么你得通過每個內(nèi)核運行一段很短的時間然后切換到下一個線程的方式,把這些時間劃分開來以便每個線程都能獲得它的分時。這是通過“上下文切換”來實現(xiàn)的,使得CPU可以從正在運行的某個線程/進(jìn)程切換到下一個。
這些上下文切換有一定的成本——它們消耗了一些時間。在快的時候,可能少于100納秒,但是根據(jù)實現(xiàn)的細(xì)節(jié),處理器速度/架構(gòu),CPU緩存等,消耗1000納秒甚至更長的時間也并不罕見。
線程(或者進(jìn)程)越多,上下文切換就越多。當(dāng)我們談?wù)摮汕先f的線程,并且每一次切換需要數(shù)百納秒時,速度將會變得非常慢。
然而,非阻塞調(diào)用本質(zhì)上是告訴內(nèi)核“當(dāng)你有一些新的數(shù)據(jù)或者這些連接中的任意一個有事件時才調(diào)用我”。這些非阻塞調(diào)用設(shè)計于高效地處理大量的I/O負(fù)載,以及減少上下文切換。
到目前為止你還在看這篇文章嗎?因為現(xiàn)在來到了有趣的部分:讓我們來看下一些流利的語言如何使用這些工具,并就在易用性和性能之間的權(quán)衡作出一些結(jié)論……以及其他有趣的點評。
請注意,雖然在這篇文章中展示的示例是瑣碎的(并且是不完整的,只是顯示了相關(guān)部分的代碼),但數(shù)據(jù)庫訪問,外部緩存系統(tǒng)(memcache等全部)和需要I/O的任何東西,都以執(zhí)行某些背后的I/O操作而結(jié)束,這些和展示的示例一樣有著同樣的影響。
同樣地,對于I/O被描述為“阻塞”(PHP,Java)這樣的情節(jié),HTTP請求與響應(yīng)的讀取與寫入本身是阻塞的調(diào)用:再一次,更多隱藏在系統(tǒng)中的I/O及其伴隨的性能問題需要考慮。
為項目選擇編程語言要考慮的因素有很多。當(dāng)你只考慮性能時,要考慮的因素甚至有更多。但是,如果你關(guān)注的是程序主要受限于I/O,如果I/O性能對于你的項目至關(guān)重要,那這些都是你需要了解的。
“保持簡單”的方法:PHP
回到90年代的時候,很多人穿著匡威鞋,用Perl寫著CGI腳本。隨后出現(xiàn)了PHP,很多人喜歡使用它,它使得制作動態(tài)網(wǎng)頁更為容易。PHP使用的模型相當(dāng)簡單。雖然有一些變化,但基本上PHP服務(wù)器看起來像:
HTTP請求來自用戶的瀏覽器,并且訪問了你的Apache網(wǎng)站服務(wù)器。Apache為每個請求創(chuàng)建一個單獨的進(jìn)程,通過一些優(yōu)化來重用它們,以便最大程度地減少其需要執(zhí)行的次數(shù)(創(chuàng)建進(jìn)程相對來說較慢)。Apache調(diào)用PHP并告訴它在磁盤上運行相應(yīng)的.php文件。PHP代碼執(zhí)行并做一些阻塞的I/O調(diào)用。若在PHP中調(diào)用了file_get_contents(),那在背后它會觸發(fā)read()系統(tǒng)調(diào)用并等待結(jié)果返回。
當(dāng)然,實際的代碼只是簡單地嵌在你的頁面中,并且操作是阻塞的:
// 阻塞的文件I/O $file_data = file_get_contents('/path/to/file.dat');// 阻塞的網(wǎng)絡(luò)I/O $curl = curl_init('http://example.com/example-microservice'); $result = curl_exec($curl);// 更多阻塞的網(wǎng)絡(luò)I/O $result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100');關(guān)于它如何與系統(tǒng)集成,就像這樣:
相當(dāng)簡單:一個請求,一個進(jìn)程。I/O是阻塞的。優(yōu)點是什么呢?簡單,可行。那缺點是什么呢?同時與20,000個客戶端連接,你的服務(wù)器就掛了。由于內(nèi)核提供的用于處理大容量I/O(epoll等)的工具沒有被使用,所以這種方法不能很好地擴(kuò)展。更糟糕的是,為每個請求運行一個單獨的進(jìn)程往往會使用大量的系統(tǒng)資源,尤其是內(nèi)存,這通常是在這樣的場景中遇到的第一件事情。
注意:Ruby使用的方法與PHP非常相似,在廣泛而普遍的方式下,我們可以將其視為是相同的。
多線程的方式:Java
所以就在你買了你的第一個域名的時候,Java來了,并且在一個句子之后隨便說一句“dot com”是很酷的。而Java具有語言內(nèi)置的多線程(特別是在創(chuàng)建時),這一點非常棒。
大多數(shù)Java網(wǎng)站服務(wù)器通過為每個進(jìn)來的請求啟動一個新的執(zhí)行線程,然后在該線程中最終調(diào)用作為應(yīng)用程序開發(fā)人員的你所編寫的函數(shù)。
在Java的Servlet中執(zhí)行I/O操作,往往看起來像是這樣:
public void doGet(HttpServletRequest request, ?HttpServletResponse response) throws ServletException, IOException {// 阻塞的文件I/OInputStream fileIs = new FileInputStream("/path/to/file");// 阻塞的網(wǎng)絡(luò)I/OURLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection();InputStream netIs = urlConnection.getInputStream();// 更多阻塞的網(wǎng)絡(luò)I/Oout.println("..."); }由于我們上面的doGet方法對應(yīng)于一個請求并且在自己的線程中運行,而不是每次請求都對應(yīng)需要有自己專屬內(nèi)存的單獨進(jìn)程,所以我們會有一個單獨的線程。這樣會有一些不錯的優(yōu)點,例如可以在線程之間共享狀態(tài)、共享緩存的數(shù)據(jù)等,因為它們可以相互訪問各自的內(nèi)存,但是它如何與調(diào)度進(jìn)行交互的影響,仍然與前面PHP例子中所做的內(nèi)容幾乎一模一樣。每個請求都會產(chǎn)生一個新的線程,而在這個線程中的各種I/O操作會一直阻塞,直到這個請求被完全處理為止。為了最小化創(chuàng)建和銷毀它們的成本,線程會被匯集在一起,但是依然,有成千上萬個連接就意味著成千上萬個線程,這對于調(diào)度器是不利的。
一個重要的里程碑是,在Java 1.4 版本(和再次顯著升級的1.7 版本)中,獲得了執(zhí)行非阻塞I/O調(diào)用的能力。大多數(shù)應(yīng)用程序,網(wǎng)站和其他程序,并沒有使用它,但至少它是可獲得的。一些Java網(wǎng)站服務(wù)器嘗試以各種方式利用這一點; 然而,絕大多數(shù)已經(jīng)部署的Java應(yīng)用程序仍然如上所述那樣工作。
Java讓我們更進(jìn)了一步,當(dāng)然對于I/O也有一些很好的“開箱即用”的功能,但它仍然沒有真正解決問題:當(dāng)你有一個嚴(yán)重I/O綁定的應(yīng)用程序正在被數(shù)千個阻塞線程狂拽著快要墜落至地面時怎么辦。
作為一等公民的非阻塞I/O:Node
當(dāng)談到更好的I/O時,Node.js無疑是新寵。任何曾經(jīng)對Node有過最簡單了解的人都被告知它是“非阻塞”的,并且它能有效地處理I/O。在一般意義上,這是正確的。但魔鬼藏在細(xì)節(jié)中,當(dāng)談及性能時這個巫術(shù)的實現(xiàn)方式至關(guān)重要。
本質(zhì)上,Node實現(xiàn)的范式不是基本上說“在這里編寫代碼來處理請求”,而是轉(zhuǎn)變成“在這里寫代碼開始處理請求”。每次你都需要做一些涉及I/O的事情,發(fā)出請求或者提供一個當(dāng)完成時Node會調(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);}); });可以看到,這里有兩個回調(diào)函數(shù)。第一個會在請求開始時被調(diào)用,而第二個會在文件數(shù)據(jù)可用時被調(diào)用。
這樣做的基本上給了Node一個在這些回調(diào)函數(shù)之間有效地處理I/O的機(jī)會。一個更加相關(guān)的場景是在Node中進(jìn)行數(shù)據(jù)庫調(diào)用,但我不想再列出這個煩人的例子,因為它是完全一樣的原則:啟動數(shù)據(jù)庫調(diào)用,并提供一個回調(diào)函數(shù)給Node,它使用非阻塞調(diào)用單獨執(zhí)行I/O操作,然后在你所要求的數(shù)據(jù)可用時調(diào)用回調(diào)函數(shù)。這種I/O調(diào)用隊列,讓Node來處理,然后獲取回調(diào)函數(shù)的機(jī)制稱為“事件循環(huán)”。它工作得非常好。
然而,這個模型中有一道關(guān)卡。在幕后,究其原因,更多是如何實現(xiàn)JavaScript V8 引擎(Chrome的JS引擎,用于Node)1,而不是其他任何事情。你所編寫的JS代碼全部都運行在一個線程中。
思考一下。這意味著當(dāng)使用有效的非阻塞技術(shù)執(zhí)行I/O時,正在進(jìn)行CPU綁定操作的JS可以在運行在單線程中,每個代碼塊阻塞下一個。 一個常見的例子是循環(huán)數(shù)據(jù)庫記錄,在輸出到客戶端前以某種方式處理它們。以下是一個例子,演示了它如何工作:
var handler = function(request, response) {connection.query('SELECT ...', function (err, rows) {if (err) { throw err };for (var i = 0; i < rows.length; i++) {// 對每一行紀(jì)錄進(jìn)行處理}response.end(...); // 輸出結(jié)果}) };雖然Node確實可以有效地處理I/O,但上面的例子中的for循環(huán)使用的是在你主線程中的CPU周期。這意味著,如果你有10,000個連接,該循環(huán)有可能會讓你整個應(yīng)用程序慢如蝸牛,具體取決于每次循環(huán)需要多長時間。每個請求必須分享在主線程中的一段時間,一次一個。
這個整體概念的前提是I/O操作是最慢的部分,因此最重要是有效地處理這些操作,即使意味著串行進(jìn)行其他處理。這在某些情況下是正確的,但不是全都正確。
另一點是,雖然這只是一個意見,但是寫一堆嵌套的回調(diào)可能會令人相當(dāng)討厭,有些人認(rèn)為它使得代碼明顯無章可循。在Node代碼的深處,看到嵌套四層、嵌套五層、甚至更多層級的嵌套并不罕見。
我們再次回到了權(quán)衡。如果你主要的性能問題在于I/O,那么Node模型能很好地工作。然而,它的阿喀琉斯之踵(譯者注:來自希臘神話,表示致命的弱點)是如果不小心的話,你可能會在某個函數(shù)里處理HTTP請求并放置CPU密集型代碼,最后使得每個連接慢得如蝸牛。
真正的非阻塞:Go
在進(jìn)入Go這一章節(jié)之前,我應(yīng)該披露我是一名Go粉絲。我已經(jīng)在許多項目中使用Go,是其生產(chǎn)力優(yōu)勢的公開支持者,并且在使用時我在工作中看到了他們。
也就是說,我們來看看它是如何處理I/O的。Go語言的一個關(guān)鍵特性是它包含自己的調(diào)度器。并不是每個線程的執(zhí)行對應(yīng)于一個單一的OS線程,Go采用的是“goroutines”這一概念。Go運行時可以將一個goroutine分配給一個OS線程并使其執(zhí)行,或者把它掛起而不與OS線程關(guān)聯(lián),這取決于goroutine做的是什么。來自Go的HTTP服務(wù)器的每個請求都在單獨的Goroutine中處理。
此調(diào)度器工作的示意圖,如下所示:
這是通過在Go運行時的各個點來實現(xiàn)的,通過將請求寫入/讀取/連接/等實現(xiàn)I/O調(diào)用,讓當(dāng)前的goroutine進(jìn)入睡眠狀態(tài),當(dāng)可采取進(jìn)一步行動時用信息把goroutine重新喚醒。
實際上,除了回調(diào)機(jī)制內(nèi)置到I/O調(diào)用的實現(xiàn)中并自動與調(diào)度器交互外,Go運行時做的事情與Node做的事情并沒有太多不同。它也不受必須把所有的處理程序代碼都運行在同一個線程中這一限制,Go將會根據(jù)其調(diào)度器的邏輯自動將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// 每個請求在它自己的goroutine中}w.Write(...) // 輸出響應(yīng)結(jié)果,也是非阻塞的}正如你在上面見到的,我們的基本代碼結(jié)構(gòu)像是更簡單的方式,并且在背后實現(xiàn)了非阻塞I/O。
在大多數(shù)情況下,這最終是“兩個世界中最好的”。非阻塞I/O用于全部重要的事情,但是你的代碼看起來像是阻塞,因此往往更容易理解和維護(hù)。Go調(diào)度器和OS調(diào)度器之間的交互處理了剩下的部分。
這不是完整的魔法,如果你建立的是一個大型的系統(tǒng),那么花更多的時間去理解它工作原理的更多細(xì)節(jié)是值得的; 但與此同時,“開箱即用”的環(huán)境可以很好地工作和很好地進(jìn)行擴(kuò)展。
Go可能有它的缺點,但一般來說,它處理I/O的方式不在其中。
謊言,詛咒的謊言和基準(zhǔn)
對這些各種模式的上下文切換進(jìn)行準(zhǔn)確的計時是很困難的。也可以說這對你來沒有太大作用。所以取而代之,我會給出一些比較這些服務(wù)器環(huán)境的HTTP服務(wù)器性能的基準(zhǔn)。請記住,整個端對端的HTTP請求/響應(yīng)路徑的性能與很多因素有關(guān),而這里我放在一起所提供的數(shù)據(jù)只是一些樣本,以便可以進(jìn)行基本的比較。
對于這些環(huán)境中的每一個,我編寫了適當(dāng)?shù)拇a以隨機(jī)字節(jié)讀取一個64k大小的文件,運行一個SHA-256哈希N次(N在URL的查詢字符串中指定,例如.../test.php?n=100),并以十六進(jìn)制形式打印生成的散列。我選擇了這個示例,是因為使用一些一致的I/O和一個受控的方式增加CPU使用率來運行相同的基準(zhǔn)測試是一個非常簡單的方式。
關(guān)于環(huán)境使用,更多細(xì)節(jié)請參考這些基準(zhǔn)要點。首先,來看一些低并發(fā)的例子。運行2000次迭代,并發(fā)300個請求,并且每次請求只做一次散列(N = 1),可以得到:
時間是在全部并發(fā)請求中完成請求的平均毫秒數(shù)。越低越好。很難從一個圖表就得出結(jié)論,但對于我來說,似乎與連接和計算量這些方面有關(guān),我們看到時間更多地與語言本身的一般執(zhí)行有關(guān),因此更多在于I/O。請注意,被認(rèn)為是“腳本語言”(輸入隨意,動態(tài)解釋)的語言執(zhí)行速度最慢。
但是如果將N增加到1000,仍然并發(fā)300個請求,會發(fā)生什么呢 —— 相同的負(fù)載,但是hash迭代是之前的1000倍(顯著增加了CPU負(fù)載):
時間是在全部并發(fā)請求中完成請求的平均毫秒數(shù)。越低越好。忽然之間,Node的性能顯著下降了,因為每個請求中的CPU密集型操作都相互阻塞了。有趣的是,在這個測試中,PHP的性能要好得多(相對于其他的語言),并且打敗了Java。(值得注意的是,在PHP中,SHA-256實現(xiàn)是用C編寫的,執(zhí)行路徑在這個循環(huán)中花費更多的時間,因為這次我們進(jìn)行了1000次哈希迭代)。
現(xiàn)在讓我們嘗試5000個并發(fā)連接(并且N = 1)—— 或者接近于此。不幸的是,對于這些環(huán)境的大多數(shù),失敗率并不明顯。對于這個圖表,我們會關(guān)注每秒的請求總數(shù)。越高越好:
每秒的請求總數(shù)。越高越好。這張圖看起來截然不同。這是一個猜測,但是看起來像是對于高連接量,每次連接的開銷與產(chǎn)生新進(jìn)程有關(guān),而與PHP + Apache相關(guān)聯(lián)的額外內(nèi)存似乎成為主要的因素并制約了PHP的性能。顯然,Go是這里的冠軍,其次是Java和Node,最后是PHP。
結(jié)論
綜上所述,很顯然,隨著語言的演進(jìn),處理大量I/O的大型應(yīng)用程序的解決方案也隨之不斷演進(jìn)。
為了公平起見,暫且拋開本文的描述,PHP和Java確實有可用于Web應(yīng)用程序的非阻塞I/O的實現(xiàn)。 但是這些方法并不像上述方法那么常見,并且需要考慮使用這種方法來維護(hù)服務(wù)器的伴隨的操作開銷。更不用說你的代碼必須以與這些環(huán)境相適應(yīng)的方式進(jìn)行結(jié)構(gòu)化; “正常”的PHP或Java Web應(yīng)用程序通常不會在這樣的環(huán)境中進(jìn)行重大改動。
作為比較,如果只考慮影響性能和易用性的幾個重要因素,可以得到:
| PHP | 進(jìn)程 | 否 | ? |
| Java | 線程 | 可用 | 需要回調(diào) |
| Node.js | 線程 | 是 | 需要回調(diào) |
| Go | 線程(Goroutine) | 是 | 不需要回調(diào) |
線程通常要比進(jìn)程有更高的內(nèi)存效率,因為它們共享相同的內(nèi)存空間,而進(jìn)程則沒有。結(jié)合與非阻塞I/O相關(guān)的因素,當(dāng)我們向下移動列表到一般的啟動時,因為它與改善I/O有關(guān),可以看到至少與上面考慮的因素一樣。如果我不得不在上面的比賽中選出一個冠軍,那肯定會是Go。
即便這樣,在實踐中,選擇構(gòu)建應(yīng)用程序的環(huán)境與你的團(tuán)隊對于所述環(huán)境的熟悉程度以及可以實現(xiàn)的總體生產(chǎn)力密切相關(guān)。因此,每個團(tuán)隊只是一味地扎進(jìn)去并開始用Node或Go開發(fā)Web應(yīng)用程序和服務(wù)可能沒有意義。事實上,尋找開發(fā)人員或內(nèi)部團(tuán)隊的熟悉度通常被認(rèn)為是不使用不同的語言和/或不同的環(huán)境的主要原因。也就是說,過去的十五年來,時代已經(jīng)發(fā)生了巨大的變化。
希望以上內(nèi)容可以幫助你更清楚地了解幕后所發(fā)生的事件,并就如何處理應(yīng)用程序現(xiàn)實世界中的可擴(kuò)展性為你提供的一些想法。快樂輸入,快樂輸出!
?
原文:https://www.toptal.com/back-end/server-side-io-performance-node-php-java-go
譯文:http://www.itran.cc/2017/05/17/server-side-io-performance-node-php-java-go/
總結(jié)
以上是生活随笔為你收集整理的服务端 I/O 性能大比拼:Node、PHP、Java、Go哪家强?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 应用中的日志
- 下一篇: 黑客是如何发现女朋友出轨的,痛心的经历!