- 原文地址:Scaling Node.js Applications
- 原文做者:Samer Buna
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:mnikn
- 校對者:shawnchenxmu,reid3290
來自 Pluralsight 課程中的截圖 - Node.js 進階html
可擴展性在 Node.js 並非過後添加的概念,這一律念在前期就已經體現出其核心地位。Node 之因此被命名爲 Node 的緣由就是強調一個想法:每個 Node 應用應該由多個小型的分散 Node 應用相互聯繫來構成。前端
你曾經在你的 Node 應用上運行多個 Node 應用嗎?你曾經試過讓生產環境上的機器的每一個 CPU 運行一個 Node 程序,而且對全部的請求進行負載均衡處理嗎?你知道 Node 有一個內置模塊能作上述事情嗎?node
Node 的 cluster 模塊不僅是提供一個黑箱的解決方案來充分利用機器中的 CPU,同時它也能幫助你提升 Node 應用的可用性,提供一個瞬時重啓整個應用的選項,這篇文章將闡述其中的全部好處。react
這篇文章是 Pluralsight Node.js 課程 中的一部分,我從視頻中整理出了相關的內容。android
咱們擴展一個應用的最主要的緣由是應用的負載,可是不僅是這一個緣由。咱們同時經過讓應用具有可擴展性來提升應用的可用性和容錯性。ios
咱們能夠經過三種主流的方式來拓展應用:git
擴展一個大型應用最簡單的方法就是屢次克隆它,並讓每個克隆實例處理一部分工做(例如,使用負載均衡器)。這種作法不會佔用開發週期太多時間,而且真的很管用。想要在最低限度上實現擴展,你可使用這種方法,Node.js 有個內置模塊 cluster
來讓你在一個單一的服務器上更簡單地實現克隆方法。github
同時咱們也能夠經過 分解 來擴展一個應用,這種方法取決於應用的函數和服務。這意味着咱們有多個不一樣的應用,各有着不一樣的基架代碼,有時還會有其獨自的數據庫和用戶接口。web
這個策略通常和微服務聯繫在一塊兒,其中的微是指每一個服務應該越小越好,但實際上,服務的規模可有可無,爲的是強迫人們解耦和讓服務之間高內聚。實現這個策略並不容易,並有可能帶來一系列預想不到的問題,可是其益處也是很顯著的。算法
咱們同時也能夠把應用分紅多個實例,每一個實例只負責應用的一部分數據。這個方法在數據庫領域內一般被稱爲橫向分割或碎片化。數據分割要求每一步操做前都須要查找當前在使用哪個實例。例如,咱們也許想要根據用戶所在的國家或者所用的語言進行分區,首先咱們須要查找相關信息。
成功擴展一個大型應用最終應該實現這三個策略。Node.js 讓這一切變得簡單,所以這篇文章我將會把注意力集中在克隆策略上,看看 Node.js 有什麼可用的內置工具來實現這個策略。
請注意到在讀這篇文章前你須要理解好 Node.js 的子進程。若是你不太瞭解,我建議你能夠先讀這篇文章:
想要在同一環境下多個 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 服務器並經過 cluster 模塊實現負載均衡。這是一個簡單的 Node hello-word 例子,咱們修改一下讓它模擬響應前的 CPU 工做。
// server.js
const http = require('http');
const pid = process.pid;
http.createServer((req, res) => {
for (let i=0; i<1e7; i++); // simulate CPU work
res.end(`Handled by process ${pid}`);
}).listen(8080, () => {
console.log(`Started process ${pid}`);
});複製代碼
爲了檢驗負載均衡器咱們須要建立一些東西來讓它工做,我已經在 HTTP 響應中引進了程序 pid
來識別目前正在處理請求的應用的實例。
在咱們使用 cluster 模塊把服務器中的主進程克隆成多個 worker 進程以前,咱們應該先調查下服務器每秒可以處理多少個請求。咱們能夠用 Apache 基準測試工具 來作這件事。在運行 server.js
以前,咱們先執行 ab
命令:
ab -c200 -t10 http://localhost:8080/複製代碼
這個命令會在 10 秒內發起 200 個併發鏈接來測試服務器的負載性能。
來自 Pluralsight 課程中的截圖 — Node.js 進階
在個人服務器上,單獨一個 node 服務器每秒能夠處理 51 個請求。固然,結果會隨着平臺的不一樣而有所變化,這只是一個很是簡化的性能測試,並不能保證結果 100% 準確,可是它將會清晰地顯示 cluster 模塊給多核的應用環境所帶來的不一樣。
既然咱們有了一個參照的基準,咱們就能夠經過 cluster 模塊來實現克隆策略,以此來拓展一個應用的規模。
在 server.js
的同級目錄上,咱們能夠建立一個名叫 cluster.js
的新文件,用來提供 master 進程:
// cluster.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const cpus = os.cpus().length;
console.log(`Forking for ${cpus} CPUs`);
for (let i = 0; i<cpus; i++) {
cluster.fork();
}
} else {
require('./server');
}複製代碼
在 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 倍。
master 進程與 worker 進程之間可以簡單地進行通訊,由於 cluster 模塊有個 child_process.fork
的 api,這意味着 master 進程與每一個 worker 進程之間進行通訊是可能的。
基於 server.js
/cluster.js
的例子,咱們能夠用 cluster.workers
獲取一個包含全部 worker 對象的列表,該列表持有全部 worker 的引用,並能夠經過這個引用來讀取 worker 的信息。有了讓 master 進程和 worker 進程通訊的方法後,想要廣播每一個 worker 進程,咱們只須要簡單地遍歷全部的 worker。例如:
Object.values(cluster.workers).forEach(worker => {
worker.send(`Hello Worker ${worker.id}`);
});複製代碼
經過 Object.values
能夠從 cluster.workers
對象裏簡單地來獲取一個包含全部 worker 的數組。而後對於每一個 worker,咱們使用 send
函數來傳遞任意咱們要傳的值。
在一個 worker 文件裏,在咱們的例子中 server.js
要讀取從 master 進程中收到的消息,咱們能夠在全局 process
對象中給 message
事件註冊一個 handler。
process.on('message', msg => {
console.log(`Message from master: ${msg}`);
});複製代碼
當我在 cluster/server 上測試這兩項新加的東西時所看到:
來自 Pluralsight 課程中的截圖 — Node.js 進階
每一個 worker 都收到了來自 master 進程的消息。注意到 worker 的啓動是亂序的。
此次咱們讓通訊的內容變得更實際一點。此次咱們想要服務器返回數據庫中用戶的數量。咱們將會建立一個 mock 函數來返回數據庫中用戶的數量,而且每次當它被調用時對這個值進行平方處理(理想狀況下的增加):
// **** 模擬 DB 調用
const numberOfUsersInDB = function() {
this.count = this.count || 5;
this.count = this.count * this.count;
return this.count;
}
// ****複製代碼
每次 numberOfUsersInDB
被調用,咱們會假設已經鏈接數據庫。咱們想要避免屢次數據庫的請求,所以咱們會根據必定時間對調用進行緩存,例如每 10 秒緩存一次。然而,咱們仍然不想讓 8 個 forked worker 使用獨自的數據庫鏈接和每 10 秒關閉 8 個數據庫鏈接。咱們可讓 master 進程只請求一次數據庫鏈接,而後經過通訊接口告訴這 8 個 worker 用戶數量的最新值。
例如,在 master 進程模式中,咱們一樣能夠遍歷全部 worker 來廣播用戶數量的值:
// 在 isMaster=true 的狀態下進行 fork 循環後
const updateWorkers = () => {
const usersCount = numberOfUsersInDB();
Object.values(cluster.workers).forEach(worker => {
worker.send({ usersCount });
});
};
updateWorkers();
setInterval(updateWorkers, 10000);複製代碼
這裏第一次咱們調用了 updateWorkers
,而後經過 setInterval
每 10 秒調用這個方法。這樣的話,每 10 秒全部的 worker 會以通訊的形式收到用戶數量的值,而且咱們只須要建立一次數據庫鏈接。
在服務端的代碼,咱們能夠從一樣的 message
事件 handler 中拿到 usersCount
的值。咱們簡單地用一個模塊全局變量緩存這個值,這樣咱們在任何地方都能使用它。
例如:
const http = require('http');
const pid = process.pid;
let usersCount;
http.createServer((req, res) => {
for (let i=0; i<1e7; i++); // simulate CPU work
res.write(`Handled by process ${pid}\n`);
res.end(`Users: ${usersCount}`);
}).listen(8080, () => {
console.log(`Started process ${pid}`);
});
process.on('message', msg => {
usersCount = msg.usersCount;
});複製代碼
上面的代碼讓 worker 的 web 服務器用緩存的 usersCount
進行響應。若是你如今測試 cluster 的代碼,前 10 秒你會從全部的 worker 裏獲得用戶數量爲 「25」(同時只建立了一個數據庫鏈接)。而後 10 秒事後,全部的 worker 開始報告當前的用戶數量,625(一樣只建立了一個數據庫鏈接)。
得力於 master 進程和 worker 之間通訊的方法的存在,咱們可以作到這一切。
咱們在運行單獨一個 Node 應用的實例時有一個問題,就是當這個實例崩潰時,咱們必須重啓整個應用。這意味着崩潰後的重啓之間會存在一個時間差,即便咱們讓這項操做自動執行也是同樣的。
同理當服務器想要部署新代碼就必須重啓。只有一個實例,爲此所形成的時間差會影響系統的可用性。
而若是咱們有多個實例的話,只需添加寥寥數行代碼就能夠提升系統的可用性。
爲了在服務器中模擬隨機崩潰,咱們經過一個 timer 來調用 process.exit
,讓它隨機執行。
// 在 server.js 文件
setTimeout(() => {
process.exit(1) // 隨時退出進程
}, Math.random() * 10000);複製代碼
當一個 worker 進程因崩潰而退出,cluster
對象裏的 exit
事件會通知 master 進程。咱們能夠給這個事件註冊一個 handler,而且當其餘 worker 進程還存在時讓它 fork 一個新的 worker 進程。
例如:
// 在 isMaster=true 的狀態下進行 fork 循環後
cluster.on('exit', (worker, code, signal) => {
if (code !== 0 && !worker.exitedAfterDisconnect) {
console.log(`Worker ${worker.id} crashed. ` +
'Starting a new worker...');
cluster.fork();
}
});複製代碼
這裏咱們添加一個 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 裏面
process.on('SIGUSR2', () => { ... });
// 觸發信號
$ kill -SIGUSR2 PID複製代碼
這樣,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);複製代碼
而後,咱們建立 restartWorker
函數來接受要重啓的 worker 的 index。這樣當下一個 worker 能夠重啓時,咱們讓函數調用當前 worker,直到最後重啓整個序列裏的 worker。這是須要調用的 restartWorker
函數(解釋在後面):
const restartWorker = (workerIndex) => {
const worker = workers[workerIndex];
if (!worker) return;
worker.on('exit', () => {
if (!worker.exitedAfterDisconnect) return;
console.log(`Exited process ${worker.process.pid}`);
cluster.fork().on('listening', () => {
restartWorker(workerIndex + 1);
});
});
worker.disconnect();
};
restartWorker(0);複製代碼
在 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}`);複製代碼
開啓 cluster,複製 master 進程的 id,而後用 kill -SIGUSR2 PID
命令重啓 cluster。一樣你能夠在重啓 cluster 時用 ab
命令來看看重啓時的可用性。劇透一下,沒有請求失敗:
來自 Pluralsight 課程中的截圖 — Node.js 進階
像 PM2 這樣的進程監控器,我我的把它用在生產環境上,它讓咱們實現上述工做變得異常簡單,同時它還有許多功能來監控 Node.js 應用的健壯度。例如,用 PM2,想要在任意應用上啓動 cluster,你只須要用 -i
參數:
pm2 start server.js -i max複製代碼
想要瞬時重啓你只須要使用這個神奇的命令:
pm2 reload all複製代碼
然而,我以爲在使用這些命令以前先理解其背後的實現是有幫助的。
好東西老是須要付出代價。當咱們對一個 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。若是你想要找一位導師,能夠 發郵件給我。如對這篇文章或者其餘我寫的文章有任何疑問,請在 這個 slack 用戶 上找到我而且在 #question 空間上提問。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。