[譯]擴展 Node.js 應用

  • 原文地址:Scaling Node.js Applications
  • 原文作者:Samer Buna
  • 譯文出自:掘金翻譯計劃
  • 本文永久鏈接:github.com/xitu/gold-m…
  • 譯者:mnikn
  • 校對者:shawnchenxmu,reid3290

擴展 Node.js 應用

你應該知道的在 Node.js 內置模塊的應用於擴展的工具

來自 Pluralsight 課程中的截圖 - Node.js 進階

可擴展性在 Node.js 並不是事後添加的概念,這一概念在前期就已經體現出其核心地位。Node 之所以被命名為 Node 的原因就是強調一個想法:每一個 Node 應用應該由多個小型的分散 Node 應用相互聯繫來構成。

你曾經在你的 Node 應用上運行多個 Node 應用嗎?你曾經試過讓生產環境上的機器的每個 CPU 運行一個 Node 程序,並且對所有的請求進行負載均衡處理嗎?你知道 Node 有一個內置模塊能做上述事情嗎?

Node 的 cluster 模塊不只是提供一個黑箱的解決方案來充分利用機器中的 CPU,同時它也能幫助你提高 Node 應用的可用性,提供一個瞬時重啟整個應用的選項,這篇文章將闡述其中的所有好處。

這篇文章是 Pluralsight Node.js 課程 中的一部分,我從視頻中整理出了相關的內容。

實現擴展的策略

我們擴展一個應用的最主要的原因是應用的負載,但是不只是這一個原因。我們同時通過讓應用具備可擴展性來提高應用的可用性和容錯性。

我們可以通過三種主流的方式來拓展應用:

1?—?克隆

擴展一個大型應用最簡單的方法就是多次克隆它,並讓每一個克隆實例處理一部分工作(例如,使用負載均衡器)。這種做法不會佔用開發周期太多時間,並且真的很管用。想要在最低限度上實現擴展,你可以使用這種方法,Node.js 有個內置模塊 cluster 來讓你在一個單一的伺服器上更簡單地實現克隆方法。

2?—?分解

同時我們也可以通過 分解 來擴展一個應用,這種方法取決於應用的函數和服務。這意味著我們有多個不同的應用,各有著不同的基架代碼,有時還會有其獨自的資料庫和用戶介面。

這個策略一般和微服務聯繫在一起,其中的微是指每個服務應該越小越好,但實際上,服務的規模無關緊要,為的是強迫人們解耦和讓服務之間高內聚。實現這個策略並不容易,並有可能帶來一系列預想不到的問題,但是其益處也是很顯著的。

3?—?分離

我們同時也可以把應用分成多個實例,每個實例只負責應用的一部分數據。這個方法在資料庫領域內通常被稱為橫向分割碎片化。數據分割要求每一步操作前都需要查找當前在使用哪一個實例。例如,我們也許想要根據用戶所在的國家或者所用的語言進行分區,首先我們需要查找相關信息。

成功擴展一個大型應用最終應該實現這三個策略。Node.js 讓這一切變得簡單,因此這篇文章我將會把注意力集中在克隆策略上,看看 Node.js 有什麼可用的內置工具來實現這個策略。

請注意到在讀這篇文章前你需要理解好 Node.js 的子進程。如果你不太了解,我建議你可以先讀這篇文章:

cluster 模塊

想要在同一環境下多個 CPU 的情況開啟負載均衡,我們可以使用 cluster 模塊。這基於子進程模塊的 fork 方法,基本上它允許我們多次 fork 主應用並用在多個 CPU 上。然後它接管所有的子進程,並將所有對主進程的請求負載均衡到子進程中去。

Node.js 的 cluster 模塊幫助我們實現可拓展性克隆策略,但是這隻適用於在只有一台伺服器上的情況。如果你有一台可以儲存著大量的資源的伺服器,或者在一台伺服器上添加資源比增添新伺服器更容易和便宜時,採用 cluster 模塊來快速執行克隆策略是一個不錯的選擇。

即使是一個小型的伺服器通常也會有多個內核,甚至如果你不擔心 Node 伺服器負載過重的話,可以任意開啟 cluster 模塊來提高伺服器的可用性和容錯性。執行這一步操作很簡單,當你使用像 PM2 這樣的進程管理器,你要做的就只是簡單地給啟動命令提供一個參數而已!

接著讓我來跟你講講該如何使用原生的 cluster 模塊,並且我會解釋它是怎麼工作的。

cluster 模塊的結構很簡單,我們創建一個 master 進程,並且讓這個 master 進程 fork 多個 worker 進程並管理它們,每一個 worker 進程代表需要可拓展的應用的實例。所有請求都由 master 進程處理,這個進程會給每個 worker 進程分配其中一部分需要處理的請求。

Pluralsight 課程上的截圖?—?Node.js 進階

master 進程的工作很簡單,實際上它只是使用輪替演算法來挑選 worker 進程。除了 Windows 以外的操作系統都默認開啟了這個演算法,並且它能通過全局修改來讓操作系統本身來處理負載均衡。

輪替演算法讓負載輪流地均勻分布在可用進程。第一個請求會指向第一個 worker 進程,第二個請求指向列表上的下一個進程,以此類推。當列表已經遍歷完,演算法會從頭開始。

這是其中一種最簡易並且也是最常用的負載均衡演算法,但是並不是只有這一個。還有很多各具特色的演算法能分配優先順序和抽選負載最小或者響應速度最快的伺服器。

HTTP 伺服器上的負載均衡

讓我們克隆一個簡單的 HTTP 伺服器並通過 cluster 模塊實現負載均衡。這是一個簡單的 Node hello-word 例子,我們修改一下讓它模擬響應前的 CPU 工作。

// server.jsnnconst http = require(http);nconst pid = process.pid;nnhttp.createServer((req, res) => {n for (let i=0; i<1e7; i++); // simulate CPU workn res.end(`Handled by process ${pid}`);n}).listen(8080, () => {n console.log(`Started process ${pid}`);n});n

為了檢驗負載均衡器我們需要創建一些東西來讓它工作,我已經在 HTTP 響應中引進了程序 pid 來識別目前正在處理請求的應用的實例。

在我們使用 cluster 模塊把伺服器中的主進程克隆成多個 worker 進程之前,我們應該先調查下伺服器每秒能夠處理多少個請求。我們可以用 Apache 基準測試工具 來做這件事。在運行 server.js 之前,我們先執行 ab 命令:

ab -c200 -t10 http://localhost:8080/n

這個命令會在 10 秒內發起 200 個並發連接來測試伺服器的負載性能。

來自 Pluralsight 課程中的截圖?—?Node.js 進階

在我的伺服器上,單獨一個 node 伺服器每秒可以處理 51 個請求。當然,結果會隨著平台的不同而有所變化,這只是一個非常簡化的性能測試,並不能保證結果 100% 準確,但是它將會清晰地顯示 cluster 模塊給多核的應用環境所帶來的不同。

既然我們有了一個參照的基準,我們就可以通過 cluster 模塊來實現克隆策略,以此來拓展一個應用的規模。

在 server.js 的同級目錄上,我們可以創建一個名叫 cluster.js 的新文件,用來提供 master 進程:

// cluster.jsnnconst cluster = require(cluster);nconst os = require(os);nnif (cluster.isMaster) {n const cpus = os.cpus().length;nn console.log(`Forking for ${cpus} CPUs`);n for (let i = 0; i<cpus; i++) {n cluster.fork();n }n} else {n require(./server);n}n

在 cluster.js 文件里,我們首先引入 cluster 和 os 模塊,我們需要 os 模塊里的os.cpus() 方法來得到 CPU 的數量。

cluster 模塊給了我們一個便利的 Boolean 參數 isMaster 來確定 cluster.js 是否正在被 master 進程讀取。當我們第一次執行這個文件時,我們會執行在 master 進程上,因此 isMaster 為 true。在這種情況下,我們讓 master 進程多次 fork 我們的伺服器,直到 fork 的次數達到 CPU 的數量。

現在我們只是通過 os 模塊來讀取 CPU 的數量,然後對這個數字進行一個 for 循環,在循環內部調用 cluster.fork 方法。for 循環將會簡單地創建和 CPU 數量一樣多的 worker 進程,以此來充分利用伺服器可用的計算能力。

當 cluster.fork 這一行在 master 進程中被執行時,當前的 cluster.js 文件會再運行一次,但是這一次是在 worker 進程,其中的 isMaster 參數為 false。實際上在這種情況下,另外一個參數將為 true,這個參數是 isWorker 參數

當應用運行在 worker 進程上,它開始做實際的工作。我們就在這裡定義伺服器的業務邏輯,例如,我們可以通過請求已經有的 server.js 文件來實現業務邏輯。

基本就是這樣了。這樣就能簡單地充分利用伺服器的計算能力。想要測試 cluster,運行 cluster.js 文件:

來自 Pluralsight 課程中的截圖?—?Node.js 進階

我的伺服器有 8 核因此我要開啟 8 個進程。其中重要的是要理解它們和 Node.js 里的進程完全不同。每個 worker 進程有其獨自的事件循環和內存空間。

當我們多次請求網路伺服器,這些請求將會由不同的 worker 進程處理,worker 進程的 id 也各不相同。序列里的 worker 進程不會準確地進行輪換,因為 cluster 模塊在挑選下一個處理請求的 worker 進程時進行了一些優化,負載會分布在不同的 worker 進程中。

我們同樣可以使用先前的 ab 命令來測試 cluster 中的進程的負載:

來自 Pluralsight 課程中的截圖?—?Node.js 進階

同樣是單獨的 node 伺服器,創建 cluster 後伺服器每秒能夠處理 181 個請求,沒用 cluster 模塊之前每秒只能處理 51 個請求。我們只是增加了幾行代碼,應用的性能就提高了 3 倍。

廣播所有 Worker 進程

master 進程與 worker 進程之間能夠簡單地進行通信,因為 cluster 模塊有個 child_process.fork 的 api,這意味著 master 進程與每個 worker 進程之間進行通信是可能的。

基於 server.js/cluster.js 的例子,我們可以用 cluster.workers 獲取一個包含所有 worker 對象的列表,該列表持有所t有 worker 的引用,並可以通過這個引用來讀取 worker 的信息。有了讓 master 進程和 worker 進程通信的方法後,想要廣播每個 worker 進程,我們只需要簡單地遍歷所有的 worker。例如:

Object.values(cluster.workers).forEach(worker => {n worker.send(`Hello Worker ${worker.id}`);n});n

通過 Object.values 可以從 cluster.workers 對象里簡單地來獲取一個包含所有 worker 的數組。然後對於每個 worker,我們使用 send 函數來傳遞任意我們要傳的值。

在一個 worker 文件里,在我們的例子中 server.js 要讀取從 master 進程中收到的消息,我們可以在全局 process 對象中給 message 事件註冊一個 handler。

process.on(message, msg => {n console.log(`Message from master: ${msg}`);n});n

當我在 cluster/server 上測試這兩項新加的東西時所看到:

來自 Pluralsight 課程中的截圖?—?Node.js 進階

每個 worker 都收到了來自 master 進程的消息。注意到 worker 的啟動是亂序的。

這次我們讓通信的內容變得更實際一點。這次我們想要伺服器返回資料庫中用戶的數量。我們將會創建一個 mock 函數來返回資料庫中用戶的數量,並且每次當它被調用時對這個值進行平方處理(理想情況下的增長):

// **** 模擬 DB 調用nconst numberOfUsersInDB = function() {n this.count = this.count || 5;n this.count = this.count * this.count;n return this.count;n}n// ****n

每次 numberOfUsersInDB 被調用,我們會假設已經連接資料庫。我們想要避免多次資料庫的請求,因此我們會根據一定時間對調用進行緩存,例如每 10 秒緩存一次。然而,我們仍然不想讓 8 個 forked worker 使用獨自的資料庫連接和每 10 秒關閉 8 個資料庫連接。我們可以讓 master 進程只請求一次資料庫連接,然後通過通信介面告訴這 8 個 worker 用戶數量的最新值。

例如,在 master 進程模式中,我們同樣可以遍歷所有 worker 來廣播用戶數量的值:

// 在 isMaster=true 的狀態下進行 fork 循環後nnconst updateWorkers = () => {n const usersCount = numberOfUsersInDB();n Object.values(cluster.workers).forEach(worker => {n worker.send({ usersCount });n });n};nnupdateWorkers();nsetInterval(updateWorkers, 10000);n

這裡第一次我們調用了 updateWorkers,然後通過 setInterval 每 10 秒調用這個方法。這樣的話,每 10 秒所有的 worker 會以通信的形式收到用戶數量的值,並且我們只需要創建一次資料庫連接。

在服務端的代碼,我們可以從同樣的 message 事件 handler 中拿到 usersCount 的值。我們簡單地用一個模塊全局變數緩存這個值,這樣我們在任何地方都能使用它。

例如:

const http = require(http);nconst pid = process.pid;nnlet usersCount;nnhttp.createServer((req, res) => {n for (let i=0; i<1e7; i++); // simulate CPU workn res.write(`Handled by process ${pid}n`);n res.end(`Users: ${usersCount}`);n}).listen(8080, () => {n console.log(`Started process ${pid}`);n});nnprocess.on(message, msg => {n usersCount = msg.usersCount;n});n

上面的代碼讓 worker 的 web 伺服器用緩存的 usersCount 進行響應。如果你現在測試 cluster 的代碼,前 10 秒你會從所有的 worker 里得到用戶數量為 「25」(同時只創建了一個資料庫連接)。然後 10 秒過後,所有的 worker 開始報告當前的用戶數量,625(同樣只創建了一個資料庫連接)。

得力於 master 進程和 worker 之間通信的方法的存在,我們能夠做到這一切。

提高伺服器的可用性

我們在運行單獨一個 Node 應用的實例時有一個問題,就是當這個實例崩潰時,我們必須重啟整個應用。這意味著崩潰後的重啟之間會存在一個時間差,即使我們讓這項操作自動執行也是一樣的。

同理當伺服器想要部署新代碼就必須重啟。只有一個實例,為此所造成的時間差會影響系統的可用性。

而如果我們有多個實例的話,只需添加寥寥數行代碼就可以提高系統的可用性。

為了在伺服器中模擬隨機崩潰,我們通過一個 timer 來調用 process.exit,讓它隨機執行。

// 在 server.js 文件nnsetTimeout(() => {n process.exit(1) // 隨時退出進程n}, Math.random() * 10000);n

當一個 worker 進程因崩潰而退出,cluster 對象里的 exit 事件會通知 master 進程。我們可以給這個事件註冊一個 handler,並且當其他 worker 進程還存在時讓它 fork 一個新的 worker 進程。

例如:

// 在 isMaster=true 的狀態下進行 fork 循環後nncluster.on(exit, (worker, code, signal) => {n if (code !== 0 && !worker.exitedAfterDisconnect) {n console.log(`Worker ${worker.id} crashed. ` +n Starting a new worker...);n cluster.fork();n }n});n

這裡我們添加一個 if 條件來保證 worker 進程真的崩潰了而不是手動斷開連接或者被 master 進程殺死了。例如,我們使用了太多的資源超出了負載的上限,因此 master 進程決定殺死一部分 worker。因此我們調用 disconnect 方法給任意 worker,這樣 exitedAfterDisconnect flag 就會設為 true。if 語句會保證不會因此而 fork 新的 worker。

如果我們帶著上面的 handler 運行 cluster(同時 server.js 里有隨機的崩潰的代碼),在隨機數秒過後,worker 會開始崩潰,master 進程會立刻 fork 新的 worker 來提高系統的可用性。你同樣可以用 ab 命令來衡量可用性,看看伺服器有多少的請求沒有處理(因為有一些請求會不走運地遇到無法避免的崩潰)。

當我測試這段代碼,10 秒內請求 1800 次,其中有 200 次並發請求,最後只有 17 次請求失敗。

來自 Pluralsight 課程中的截圖?—?Node.js 進階

這有 99% 以上的可用性。只是添加數行代碼,現在我們不再擔心進程崩潰了。master 守護將會替我們關注這些進程的情況。

瞬時重啟

那當我們想要部署新代碼,而不得不重啟所有的 worker 進程時該怎麼辦呢?

我們有多個實例在運行,所以與其讓它們一起重啟,不如每次只重啟一個,這樣的話即使重啟也能保證其他的 worker 進程能夠繼續處理請求。

用 cluster 模塊能簡單地實現這一想法。當 master 進程開始運行之後我們就不想重啟它,我們需要想辦法傳遞重啟 worker 的指令給 master 進程。在 Linux 系統上這樣做很容易因為我們能監聽一個進程的信號像 SIGUSR2,當 kill 命令裡面帶有進程 id 和信號時這個監聽事件將會觸發:

// 在 Node 裡面nprocess.on(SIGUSR2, () => { ... });nn// 觸發信號n$ kill -SIGUSR2 PIDn

這樣,master 進程不會被殺死,我們就能夠在裡面進行一系列操作了。SIGUSR2 信號適合這種情況,因為我們要執行用戶指令。如果你想知道為什麼不用 SIGUSR1,那是因為這個信號用在 Node 的調試器上,我們為了避免衝突所以不用它。

不幸的是,在 Windows 裡面的進程不支持這個信號,我們要找其他方法讓 master 進程做這件事。有幾種代替方案。例如,我們可以用標準輸入或者 socket 輸入。或者我們可以監控 process.id 文件的刪除事件。但是為了讓這個教程更容易,我們還是假定伺服器運行在 Linux 平台上。

在 Windows 上 Node 運行良好,但是我認為讓作為產品的 Node 應用在 Linux 平台上運行會更安全。這和 Node 本身無關,只是因為在 Linux 上有更多穩定的生產工具。這只是我的個人見解,最好還是根據自己的情況選擇平台。

順帶一提,在最近的 Windows 版本里,實際上你可以在裡面使用 Linux 子系統。我自己測試過了,沒有什麼特別明顯的缺點。如果你在 Windows 上開發 Node 應用,可以看看 [Bash on Windows](msdn.microsoft.com/en-us/comma…) 並嘗試一下。

在我們的例子中,當 master 進程收到 SIGUSR2 信號,就意味著是時候重啟 worker 了,但是我們想要每次只重啟一個 worker。因此 master 進程應該等到當前的 worker 已經重啟完後再重啟下一個 worker。

我們需要用 cluster.workers 對象來得到當前所有 worker 的引用,然後我們簡單地把它存進一個數組中:

const workers = Object.values(cluster.workers);n

然後,我們創建 restartWorker 函數來接受要重啟的 worker 的 index。這樣當下一個 worker 可以重啟時,我們讓函數調用當前 worker,直到最後重啟整個序列里的 worker。這是需要調用的 restartWorker 函數(解釋在後面):

const restartWorker = (workerIndex) => {n const worker = workers[workerIndex];n if (!worker) return;nn worker.on(exit, () => {n if (!worker.exitedAfterDisconnect) return;n console.log(`Exited process ${worker.process.pid}`);nn cluster.fork().on(listening, () => {n restartWorker(workerIndex + 1);n });n });nn worker.disconnect();n};nnrestartWorker(0);n

在 restartWorker 函數裡面,我們得到了要重啟的 worker 的引用,然後我們會根據序列遞歸調用這個函數,我們需要一個結束遞歸的條件。當沒有 worker 需要重啟,我們就直接 return。基本上我們想讓這個 worker 斷開連接(使用 worker.disconnect),但是在重啟下一個 worker 之前,我們需要 fork 一個新的 worker 來代替當前斷開連接的 worker。

當目前要斷開連接的 worker 還存在時,我們可以用 worker 本身的 exit 事件來 fork 一個新的 worker,但是我們要確保在平常的斷開連接調用後 exit 動作就會被觸發。我們可以用 exitedAfetrDisconnect flag,如果 flag 不為 true,那麼是因為其他原因而導致的 exit,這種情況下我們什麼都不做就直接 return。但是如果 flag 為 true,我們就繼續執行下去,fork 一個新的 worker 來代替當前要斷開連接的那個。

當新的 fork worker 進程準備好了,我們就要重啟下一個。然而,記住 fork 的過程不是同步的,所以我們不能在調用完 fork 後就直接重啟下個 worker。我們要在新的 fork worker 上監聽 listening 事件,這個事件告訴我們這個 worker 已經連接並準備好了。當我們觸發這個事件,我們就可以安全地重啟下個在序列里 worker 了。

這就是我們為了實現瞬時重啟要做的東西。要測試它,你要知道需要發送 SIGUSR2 信號的 master 進程的 id:

console.log(`Master PID: ${process.pid}`);n

開啟 cluster,複製 master 進程的 id,然後用 kill -SIGUSR2 PID 命令重啟 cluster。同樣你可以在重啟 cluster 時用 ab 命令來看看重啟時的可用性。劇透一下,沒有請求失敗:

來自 Pluralsight 課程中的截圖?—?Node.js 進階

像 PM2 這樣的進程監控器,我個人把它用在生產環境上,它讓我們實現上述工作變得異常簡單,同時它還有許多功能來監控 Node.js 應用的健壯度。例如,用 PM2,想要在任意應用上啟動 cluster,你只需要用 -i 參數:

pm2 start server.js -i maxn

想要瞬時重啟你只需要使用這個神奇的命令:

pm2 reload alln

然而,我覺得在使用這些命令之前先理解其背後的實現是有幫助的。

共享狀態和粘性負載均衡

好東西總是需要付出代價。當我們對一個 Node 應用進行負載均衡,我們也失去了一些只能在單進程適用的功能。這個問題在其他語言上被稱為線程安全,它和在線程之間共享數據有關。在我們的案例中,問題則在於如何在 worker 進程之間共享數據。

例如,設立了 cluster 後,我們就不能在內存上緩存東西了,因為每個 worker 有其獨立的內存空間,如果我們在其中一個 worker 的內存里緩存東西,其他的 worker 就沒辦法拿到它。

如果我們需要在 cluster 里緩存東西,我們要從所有 worker 那裡分離實體和讀取/寫入實體的 API。實體要存放在資料庫伺服器,或者如果你想用內存來緩存,你可以使用像 Redis 這樣的伺服器,或者創建一個專註於讀取/寫入 API 的 Node 進程供所有 worker 使用。

來自 Pluralsight 課程中的截圖?—?Node.js 進階

這個做法有個好處,當你的應用為了緩存而分離了實體,實際上這是分解的一部分,能讓你的應用更具可拓展性。即使你運行在一個單核伺服器,你也應該這樣做。

除了緩存外,當我們運行 cluster,總體來說狀態之間的交流成為了一個問題。我們不能確保交流發生在同一個 worker 上,因此不能在任何一個 worker 上創建一個狀態相關的交流通道。

一個最常見的例子是用戶認證。

來自 Pluralsight 課程中的截圖?—?Node.js 進階

用 cluster,驗證的請求分配到 master 進程,而這個進程把請求分配給一個 worker,假定分配給 A。

來自 Pluralsight 課程中的截圖?—?Node.js 進階

現在 Worker A 認出了用戶的狀態。但是,當同樣的用戶進行另外一個請求,最終負載均衡器會把它分配給其他 worker,而這些 worker 還沒有驗證這個用戶。在單獨一個實例的內存上持有驗證用戶的引用並不管用。

有很多方法處理這個問題。通過在共享資料庫或者 Redis node 上對會話信息進行排序,我們可以在 worker 之間共享狀態。然而,實現這個策略需要改變一些代碼,這不是最好的方法。

如果你不想修改代碼就實現一個會話的共享存儲倉庫,有個入侵性低但效率不高的策略。你可以用粘性負載均衡。和讓普通的負載均衡器實現上述策略相比,它更為簡單。想法很簡單,當 worker 的實例要驗證用戶,我們在負載均衡器上記錄相關的關係。

來自 Pluralsight 課程中的截圖?—?Node.js 進階

然後,當同樣的用戶發送新的請求,我們就檢查記錄,發現伺服器里已經有驗證的會話,然後把這個會話發送給伺服器,而不是執行普通的驗證操作。用這個方法不需要改變伺服器里的代碼,但同時我們不會得到用負載均衡器來驗證用戶的好處,所以只有別無選擇時才用粘性負載均衡。

實際上 cluster 模塊並不支持粘性負載均衡,但是大多數負載均衡器可以默認設置為粘性負載均衡。

感謝閱讀。如果你覺得這篇文章對你有幫助,請點擊下面的 ??。關注我來得到更多有關 Node.js 和 JavaScript 的文章。

我為 Pluralsight 和 Lynda 創建了網路課程。最近我的課程是 Advanced React.js,Advanced Node.js 和 Learning Full-stack JavaScript。

同時我也為 JavaScript,Node.js,React.js,和 GraphQL 的水平在初級與進階之間的人們創建了在線 training。如果你想要找一位導師,可以 發郵件給我。如對這篇文章或者其他我寫的文章有任何疑問,請在 href="link.juejin.im/?">這個 slack 用戶 上找到我並且在 #question 空間上提問。

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。

推薦閱讀:

Node.js的線程和進程詳解
eggjs-feed-04
前端周刊第57期:《戰爭與和平版》的 CSS-IN-JS 黑歷史
node.js教程3--文件操作

TAG:Nodejs | 后端技术 |