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