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