如何創(chuàng)建高性能、可擴(kuò)展的Node.js應(yīng)用?(如何創(chuàng)建高性能,可擴(kuò)展的node.js應(yīng)用程序)
作者|Virgafox
譯者|姚佳靈
出處丨前端之巔
說明:本文根據(jù)原文作者的系列文章編輯而成,略有刪改。
在這篇文章中,我們將介紹關(guān)于開發(fā) Node.js web 應(yīng)用程序的一些最佳實(shí)踐,重點(diǎn)關(guān)注效率和性能,以便用更少的資源獲得最佳結(jié)果。
提高 web 應(yīng)用程序吞吐量的一種方法是對(duì)其進(jìn)行擴(kuò)展,多次實(shí)例化其以平衡在多個(gè)實(shí)例之間的傳入連接,接來下我們要介紹的是如何在多個(gè)內(nèi)核上或多臺(tái)機(jī)器上對(duì) Node.js 應(yīng)用程序進(jìn)行水平擴(kuò)展。
在強(qiáng)制性規(guī)則中,有一些好的實(shí)踐可以用來解決這些問題,像拆分 API 和工作進(jìn)程、采用優(yōu)先級(jí)隊(duì)列、管理像 cron 進(jìn)程這樣的周期性作業(yè),在向上擴(kuò)展到 N 個(gè)進(jìn)程 / 機(jī)器時(shí),這不需要運(yùn)行 N 次。
水平擴(kuò)展 Node.js 應(yīng)用程序
水平擴(kuò)展是復(fù)制應(yīng)用程序?qū)嵗怨芾泶罅總魅脒B接。 此操作可以在單個(gè)多內(nèi)核機(jī)器上執(zhí)行,也可以在不同機(jī)器上執(zhí)行。
垂直擴(kuò)展是提高單機(jī)性能,它不涉及代碼方面的特定工作。
在同一臺(tái)機(jī)器上的多進(jìn)程
提高應(yīng)用程序吞吐量的一種常用方法是為機(jī)器的每個(gè)內(nèi)核生成一個(gè)進(jìn)程。 通過這種方式,Node.js 中請(qǐng)求的已經(jīng)有效的“并發(fā)”管理(請(qǐng)參見“事件驅(qū)動(dòng),非阻塞 I / O”)可以相乘和并行化。
產(chǎn)生大于內(nèi)核的數(shù)量的大量進(jìn)程可能并不好,因?yàn)樵谳^低級(jí)別,操作系統(tǒng)可能會(huì)平衡這些進(jìn)程之間的 CPU 時(shí)間。
擴(kuò)展單機(jī)有不同的策略,但常見的概念是,在同一端口上運(yùn)行多個(gè)進(jìn)程,并使用某種內(nèi)部負(fù)載平衡來分配所有進(jìn)程 / 核上的傳入連接。
下面所描述的策略是標(biāo)準(zhǔn)的 Node.js 集群模式以及自動(dòng)的,更高級(jí)別的 PM2 集群功能。
原生集群模式
原生 Node.js 群集模塊是在單機(jī)上擴(kuò)展 Node 應(yīng)用程序的基本方法(請(qǐng)參閱 https://Node.js.org/api/cluster.html)。 你的進(jìn)程的一個(gè)實(shí)例(稱為“master”)是負(fù)責(zé)生成其他子進(jìn)程(稱為“worker”)的實(shí)例,每個(gè)進(jìn)程對(duì)應(yīng)一個(gè)運(yùn)行你的應(yīng)用程序的核。 傳入連接按照循環(huán)策略分發(fā)到所有 worker 進(jìn)程,從而在同一端口上公開服務(wù)。
該方法的主要缺點(diǎn)是必須在代碼內(nèi)部管理 master 進(jìn)程和 worker 進(jìn)程之間的差異,通常使用經(jīng)典的 if-else 塊,不能夠輕易地修改進(jìn)動(dòng)態(tài)進(jìn)程數(shù)。
下面的例子來自官方文檔:
const cluster = require(‘cluster’);const http = require(‘http’);const numCPUs = require(‘os’).cpus().length;if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers. for (let i = 0; i < numCPUs; i ) { cluster.fork(); } cluster.on(‘exit’, (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); });} else { // Workers can share any TCP connection // In this case it is an HTTP server http.createServer((req, res) => { res.writeHead(200); res.end(‘hello worldn’); }).listen(8000); console.log(`Worker ${process.pid} started`);}
PM2 集群模式
如果你在使用 PM2 作為你的流程管理器(我也建議你這么做),那么有一個(gè)神奇的群集功能可以讓你跨所有內(nèi)核擴(kuò)展流程,而無需擔(dān)心集群模塊。 PM2 守護(hù)程序?qū)⒊袚?dān)“master”進(jìn)程的角色,它將生成你的應(yīng)用程序的 N 個(gè)進(jìn)程作為 worker 進(jìn)程, 并進(jìn)行循環(huán)平衡。
通過這個(gè)方法,只需要按你為單內(nèi)核用途一樣地編寫你的應(yīng)用程序(我們稍后再提其中的一些注意事項(xiàng)),而 PM2 將關(guān)注多內(nèi)核部分。
在集群模式下啟動(dòng)你的應(yīng)用程序后,你可以使用“pm2 scale”調(diào)整動(dòng)態(tài)實(shí)例數(shù),并執(zhí)行“0-second-downtime”重新加載,進(jìn)程重新串聯(lián),以便始終至少有一個(gè)在線進(jìn)程。
在生產(chǎn)中運(yùn)行節(jié)點(diǎn)時(shí),如果你的進(jìn)程像很多其他你應(yīng)該考慮的有用的東西一樣崩潰了,那么 PM2 作為進(jìn)程管理器將負(fù)責(zé)重新啟動(dòng)你的進(jìn)程。
如果你需要進(jìn)一步擴(kuò)展,那么你也許需要部署更多的機(jī)器。
具有網(wǎng)絡(luò)負(fù)載均衡的多臺(tái)機(jī)器
跨多臺(tái)機(jī)器進(jìn)行擴(kuò)展的主要概念類似于在多內(nèi)核上進(jìn)行擴(kuò)展,有多臺(tái)機(jī)器,每臺(tái)機(jī)器運(yùn)行一個(gè)或多個(gè)進(jìn)程,以及用于將流量重定向到每臺(tái)機(jī)器的均衡器。
一旦請(qǐng)求被發(fā)送到特定的節(jié)點(diǎn),剛才所提到的內(nèi)部均衡器發(fā)送該流量到特定的進(jìn)程。
可以以不同方式部署網(wǎng)絡(luò)平衡器。 如果使用 AWS 來配置你的基礎(chǔ)架構(gòu),那么一個(gè)不錯(cuò)的選擇是使用像 ELB(Elastic Load Balancer,彈性負(fù)載均衡器)這樣的托管負(fù)載均衡器,因?yàn)樗С肿詣?dòng)擴(kuò)展等有用功能,并且易于設(shè)置。
但是如果你想按傳統(tǒng)的方式來做,你可以自己部署一臺(tái)機(jī)器并用 NGINX 設(shè)置一個(gè)均衡器。 指向上游的反向代理的配置對(duì)于這個(gè)任務(wù)來說非常簡單。 下面是配置示例:
http { upstream myapp1 { server srv1.example.com; server srv2.example.com; server srv3.example.com; } server { listen 80; location / { proxy_pass http://myapp1; } }}
通過這種方式,負(fù)載均衡器將是你的應(yīng)用程序暴露給外部世界的唯一入口點(diǎn)。 如果擔(dān)心它成為基礎(chǔ)架構(gòu)的單點(diǎn)故障,可以部署多個(gè)指向相同服務(wù)器的負(fù)載均衡器。
為了在均衡器之間分配流量(每個(gè)均衡器都有自己的 IP 地址),可以向主域添加多個(gè) DNS“A”記錄,從而 DNS 解析器將在你的均衡器之間分配流量,每次都解析為不同的 IP 地址。通過這種方式,還可以在負(fù)載均衡器上實(shí)現(xiàn)冗余。
我們?cè)谶@里看到的是如何在不同級(jí)別擴(kuò)展 Node.js 應(yīng)用程序,以便從你的基礎(chǔ)架構(gòu)(從單節(jié)點(diǎn)到多節(jié)點(diǎn)和多均衡器)獲得盡可能高的性能,但要小心:如果想在多進(jìn)程環(huán)境中使用你的應(yīng)用程序,必須做好準(zhǔn)備,否則會(huì)遇到一些問題和不期望的行為。
在向上擴(kuò)展你的進(jìn)程時(shí),為了避免出現(xiàn)不期望的行為,現(xiàn)在我們來談?wù)劚仨毧紤]到的一些方面。
讓Node.js 應(yīng)用程序做好擴(kuò)展準(zhǔn)備
從 DB 中分離應(yīng)用程序?qū)嵗?/p>
首先不是代碼問題,而是你的基礎(chǔ)結(jié)構(gòu)。
如果希望你的應(yīng)用程序能夠跨不同主機(jī)進(jìn)行擴(kuò)展,則必須把你的數(shù)據(jù)庫部署在獨(dú)立的機(jī)器上,以便可以根據(jù)需要自由復(fù)制應(yīng)用程序機(jī)器。
在同一臺(tái)機(jī)器上部署用于開發(fā)目的的應(yīng)用程序和數(shù)據(jù)庫可能很便宜,但絕對(duì)不建議用于生產(chǎn)環(huán)境,其中的應(yīng)用程序和數(shù)據(jù)庫必須能夠獨(dú)立擴(kuò)展。 這同樣適用于像 Redis 這樣的內(nèi)存數(shù)據(jù)庫。
無狀態(tài)
如果生成你的應(yīng)用程序的多個(gè)實(shí)例,則每個(gè)進(jìn)程都有自己的內(nèi)存空間。 這意味著即使在一臺(tái)機(jī)器上運(yùn)行,當(dāng)你在全局變量中存儲(chǔ)某些值,或者更常見的是在內(nèi)存中存儲(chǔ)會(huì)話時(shí),如果均衡器在下一個(gè)請(qǐng)求期間將您重定向到另一個(gè)進(jìn)程,那么你將無法在那里找到它。
這適用于會(huì)話數(shù)據(jù)和內(nèi)部值,如任何類型的應(yīng)用程序范圍的設(shè)置。對(duì)于可在運(yùn)行時(shí)更改的設(shè)置或配置,解決方案是將它們存儲(chǔ)在外部數(shù)據(jù)庫(存儲(chǔ)或內(nèi)存中)上,以使所有進(jìn)程都可以訪問它們。
使用 JWT 進(jìn)行無狀態(tài)身份驗(yàn)證
身份驗(yàn)證是開發(fā)無狀態(tài)應(yīng)用程序時(shí)要考慮的首要主題之一。 如果將會(huì)話存儲(chǔ)在內(nèi)存中,它們將作用于這單個(gè)進(jìn)程。
為了正常工作,應(yīng)該將網(wǎng)絡(luò)負(fù)載均衡器配置為,始終將同一用戶重定向到同一臺(tái)機(jī)器,并將本地用戶重定向到同一用戶始終重定向到同一進(jìn)程(粘性會(huì)話)。
解決此問題的一個(gè)簡單方法是將會(huì)話的存儲(chǔ)策略設(shè)置為任何形式的持久性,例如,將它們存儲(chǔ)在 DB 而不是 RAM 中。 但是,如果你的應(yīng)用程序檢查每個(gè)請(qǐng)求的會(huì)話數(shù)據(jù),那么每次 API 調(diào)用都會(huì)進(jìn)行磁盤讀寫操作(I / O),從性能的角度來看,這絕對(duì)不是好事。
更好,更快的解決方案(如果你的身份驗(yàn)證框架支持)是將會(huì)話存儲(chǔ)在像 Redis 這樣的內(nèi)存數(shù)據(jù)庫中。 Redis 實(shí)例通常位于應(yīng)用程序?qū)嵗獠?,例?DB 實(shí)例,但在內(nèi)存中工作會(huì)使其更快。 無論如何,在 RAM 中存儲(chǔ)會(huì)話會(huì)在并發(fā)會(huì)話數(shù)增加時(shí)需要更多內(nèi)存。
如果想采用更有效的無狀態(tài)身份驗(yàn)證方法,可以看看 JSON Web Tokens。
JWT 背后的想法很簡單:當(dāng)用戶登錄時(shí),服務(wù)器生成一個(gè)令牌,該令牌本質(zhì)上是包含有效負(fù)載的 JSON 對(duì)象的 base64 編碼,加上簽名獲得的哈希,該負(fù)載具有服務(wù)器擁有的密鑰。 有效負(fù)載可以包含用于對(duì)用戶進(jìn)行身份驗(yàn)證和授權(quán)的數(shù)據(jù),例如 userID 及其關(guān)聯(lián)的 ACL 角色。 令牌被發(fā)送回客戶端并由其用于驗(yàn)證每個(gè) API 請(qǐng)求。
當(dāng)服務(wù)器處理傳入請(qǐng)求時(shí),它會(huì)獲取令牌的有效負(fù)載并使用其密鑰重新創(chuàng)建簽名。 如果兩個(gè)簽名匹配,則可以認(rèn)為有效載荷有效并且不被改變,并且可以識(shí)別用戶。
重要的是要記住 JWT 不提供任何形式的加密。 有效負(fù)載僅用 base64 編碼,并以明文形式發(fā)送,因此如果需要隱藏內(nèi)容,則必須使用 SSL。
被 jwt.io 借用的以下模式恢復(fù)了身份驗(yàn)證過程:
在認(rèn)證過程中,服務(wù)器不需要訪問存儲(chǔ)在某處的會(huì)話數(shù)據(jù),因此每個(gè)請(qǐng)求都可以由非常有效的方式由不同的進(jìn)程或機(jī)器處理。 RAM 中不保存數(shù)據(jù),也不需要執(zhí)行存儲(chǔ) I / O,因此在向上擴(kuò)展時(shí)這種方法非常有用。
S3 上的存儲(chǔ)
使用多臺(tái)機(jī)器時(shí),無法將用戶生成的資產(chǎn)直接保存在文件系統(tǒng)上,因?yàn)檫@些文件只能由該服務(wù)器本地的進(jìn)程訪問。 解決方案是,將所有內(nèi)容存儲(chǔ)在外部服務(wù)上,可以存儲(chǔ)在像 Amazon S3 這樣的專用服務(wù)上,并在你的數(shù)據(jù)庫中僅保存指向該資源的絕對(duì) URL。
然后,每個(gè)進(jìn)程 / 機(jī)器都可以以相同的方式訪問該資源。
使用 Node.js 的官方 AWS sdk 非常簡單,可以輕松地將服務(wù)集成到你的應(yīng)用程序中。 S3 非常便宜并且針對(duì)此目的進(jìn)行了優(yōu)化。即使你的應(yīng)用程序不是多進(jìn)程的,它也是一個(gè)不錯(cuò)的選擇。
正確配置 WebSockets
如果你的應(yīng)用程序使用 WebSockets 進(jìn)行客戶端之間或客戶端與服務(wù)器之間的實(shí)時(shí)交互,則需要鏈接后端實(shí)例,以便在連接到不同節(jié)點(diǎn)的客戶端之間正確傳播廣播消息或消息。
Socket.io 庫為此提供了一個(gè)特殊的適配器,稱為 socket.io-redis,它允許你使用 Redis pub-sub 功能鏈接服務(wù)器實(shí)例。
為了使用多節(jié)點(diǎn) socket.io 環(huán)境,還需要強(qiáng)制協(xié)議為“websockets”,因?yàn)殚L輪詢(long-polling)需要粘性會(huì)話才能工作。
以上這些對(duì)于單節(jié)點(diǎn)環(huán)境來說也是好的實(shí)例。
效率和性能的其他良好實(shí)踐
接下來,我們將介紹一些可以進(jìn)一步提高效率和性能的其他實(shí)踐。
Web 和 worker 進(jìn)程
你可能知道,Node.js 實(shí)際上是單線程的,因此該進(jìn)程的單個(gè)實(shí)例一次只能執(zhí)行一個(gè)操作。 在 Web 應(yīng)用程序的生命周期中,執(zhí)行許多不同的任務(wù):管理 API 調(diào)用,讀取 / 寫入 DB,與外部網(wǎng)絡(luò)服務(wù)通信,執(zhí)行某種不可避免的 CPU 密集型工作等。
雖然你使用異步編程,但將所有這些操作委派給響應(yīng) API 調(diào)用的同一進(jìn)程可能是一種非常低效的方法。
一種常見的模式是基于兩種不同類型的進(jìn)程之間的職責(zé)分離,這兩種類型的進(jìn)程組成了你的應(yīng)用程序,通常是 Web 進(jìn)程和 worker 進(jìn)程。
Web 進(jìn)程主要用于管理傳入的網(wǎng)絡(luò)呼叫,并盡快發(fā)送它們。 每當(dāng)需要執(zhí)行非阻塞任務(wù)時(shí),例如發(fā)送電子郵件 / 通知、編寫日志、執(zhí)行觸發(fā)操作,其結(jié)果是不需要響應(yīng) API 調(diào)用,web 進(jìn)程將操作委派給 worker 進(jìn)程。
Web 和 worker 進(jìn)程之間的通信可以用不同的方式實(shí)現(xiàn)。 一種常見且有效的解決方案是優(yōu)先級(jí)隊(duì)列,如下一段所描述的 Kue 中實(shí)現(xiàn)的優(yōu)先級(jí)隊(duì)列。
這種方法的一大勝利是,可以在相同或不同的機(jī)器上獨(dú)立擴(kuò)展 web 和 worker 進(jìn)程。
例如,如果你的應(yīng)用程序是高流量應(yīng)用程序,幾乎沒有生成的副作用,那么可以部署比 worker 進(jìn)程更多的 web 進(jìn)程,而如果很少有網(wǎng)絡(luò)請(qǐng)求為 worker 進(jìn)程生成大量作業(yè),則可以重新分發(fā)相應(yīng)的資源。
Kue
為了使 web 和 worker 進(jìn)程相互通信,隊(duì)列是一種靈活的方法,可以讓你不必?fù)?dān)心進(jìn)程間通信。
Kue 是基于 Redis 的 Node.js 的通用隊(duì)列庫,允許你以完全相同的方式放入在相同或不同機(jī)器上生成的通信進(jìn)程。
任何類型的進(jìn)程都可以創(chuàng)建作業(yè)并將其放入隊(duì)列,然后將 worker 進(jìn)程配置為選擇這些作業(yè)并執(zhí)行它們。 可以為每項(xiàng)工作提供許多選項(xiàng),如優(yōu)先級(jí)、TTL、延遲等。
你生成的 worker 進(jìn)程越多,執(zhí)行這些作業(yè)所需的并行吞吐量就越多。
Cron
應(yīng)用程序通常需要定期執(zhí)行某些任務(wù)。 通常,這種操作通過操作系統(tǒng)級(jí)別的 cron 作業(yè)進(jìn)行管理,從你的應(yīng)用程序外部調(diào)用單個(gè)腳本。
在新機(jī)器上部署你的應(yīng)用程序時(shí),用此方法就需要額外的工作,如果要自動(dòng)部署,這會(huì)使進(jìn)程感到不自在。
實(shí)現(xiàn)相同結(jié)果的更自在的方法是使用 NPM 上的可用 cron 模塊。 它允許你在 Node.js 代碼中定義 cron 作業(yè),使其獨(dú)立于 OS 配置。
根據(jù)上面描述的 web / worker 模式,worker 進(jìn)程可以創(chuàng)建 cron,它調(diào)用一個(gè)函數(shù),定期將新作業(yè)放入隊(duì)列。
使用隊(duì)列使其更加干凈,并可以利用 kue 提供的所有功能,如優(yōu)先級(jí),重試等。
當(dāng)你有多個(gè) worker 進(jìn)程時(shí)會(huì)出現(xiàn)問題,因?yàn)?cron 函數(shù)會(huì)同時(shí)喚醒每個(gè)進(jìn)程上的應(yīng)用程序,并將多次執(zhí)行的同一作業(yè)放入隊(duì)列副本中。
為了解決這個(gè)問題,有必要確定將執(zhí)行 cron 操作的單個(gè) worker 進(jìn)程。
領(lǐng)導(dǎo)者選舉(Leader election)和 cron-cluster(cron 集群)
這種問題被稱為“領(lǐng)導(dǎo)者選舉”,對(duì)于這個(gè)特定的場景,有一個(gè) NPM 包為我們做了一個(gè)叫做 cron-cluster 的技巧。
它暴露了為 cron 模塊提供動(dòng)力的相同 API,但在設(shè)置過程中,它需要一個(gè) redis 連接,用于與其他進(jìn)程通信并執(zhí)行領(lǐng)導(dǎo)者選舉算法。
使用 redis 作為單一事實(shí)來源,所有進(jìn)程都會(huì)同意誰將執(zhí)行 cron,并且只有一份作業(yè)副本將被放入隊(duì)列中。 之后,所有 worker 進(jìn)程都將有資格像往常一樣執(zhí)行作業(yè)。
緩存 API 調(diào)用
服務(wù)器端緩存是提高 API 調(diào)用的性能和反應(yīng)性的常用方法,但它是一個(gè)非常廣泛的主題,有很多可能的實(shí)現(xiàn)。
在像我們所描述的分布式環(huán)境中,使用 redis 來存儲(chǔ)緩存的值可能是使所有節(jié)點(diǎn)表現(xiàn)相同的最佳方法。
緩存需要考慮的最困難的方面是失效。 快速而簡陋的解決方案只考慮時(shí)間,因此緩存中的值在固定的 TTL 之后刷新,缺點(diǎn)是不得不等待下一次刷新以查看響應(yīng)中的更新。
如果你有更多的時(shí)間,最好在應(yīng)用程序級(jí)別實(shí)現(xiàn)失效,在 DB 上值更改時(shí)手動(dòng)刷新 redis 緩存上的記錄。
結(jié) 論
我們?cè)诒疚闹薪榻B了一些有關(guān)擴(kuò)展和性能的一些主題。 文中提供的建議可以作為指導(dǎo),可以根據(jù)你的項(xiàng)目的特定需求進(jìn)行定制。
英文原文:
- https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-1-3-bb06b6204197
- https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-2-3-2a68f875ce79
- https://medium.com/iquii/good-practices-for-high-performance-and-scalable-node-js-applications-part-3-3-c1a3381e1382
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。