1. 程式人生 > >如何建立高效能、可擴充套件的Node.js應用?

如何建立高效能、可擴充套件的Node.js應用?

在這篇文章中,我們將介紹關於開發 Node.js web 應用程式的一些最佳實踐,重點關注效率和效能,以便用更少的資源獲得最佳結果。

提高 web 應用程式吞吐量的一種方法是對其進行擴充套件,多次例項化其以平衡在多個例項之間的傳入連線,接來下我們要介紹的是如何在多個核心上或多臺機器上對 Node.js 應用程式進行水平擴充套件。

在強制性規則中,有一些好的實踐可以用來解決這些問題,像拆分 API 和工作程序、採用優先順序佇列、管理像 cron 程序這樣的週期性作業,在向上擴充套件到 N 個程序 / 機器時,這不需要執行 N 次。

水平擴充套件 Node.js 應用程式

水平擴充套件是複製應用程式例項以管理大量傳入連線。 此操作可以在單個多核心機器上執行,也可以在不同機器上執行。

垂直擴充套件是提高單機效能,它不涉及程式碼方面的特定工作。

在同一臺機器上的多程序

提高應用程式吞吐量的一種常用方法是為機器的每個核心生成一個程序。 通過這種方式,Node.js 中請求的已經有效的“併發”管理(請參見“事件驅動,非阻塞 I / O”)可以相乘和並行化。

產生大於核心的數量的大量程序可能並不好,因為在較低級別,作業系統可能會平衡這些程序之間的 CPU 時間。

擴充套件單機有不同的策略,但常見的概念是,在同一埠上執行多個程序,並使用某種內部負載平衡來分配所有程序 / 核上的傳入連線。

下面所描述的策略是標準的 Node.js 叢集模式以及自動的,更高級別的 PM2 叢集功能。

原生叢集模式

原生 Node.js 群集模組是在單機上擴充套件 Node 應用程式的基本方法(請參閱 https://Node.js.org/api/cluster.html)。 你的程序的一個例項(稱為“master”)是負責生成其他子程序(稱為“worker”)的例項,每個程序對應一個執行你的應用程式的核。 傳入連線按照迴圈策略分發到所有 worker 程序,從而在同一埠上公開服務。

該方法的主要缺點是必須在程式碼內部管理 master 程序和 worker 程序之間的差異,通常使用經典的 if-else 塊,不能夠輕易地修改進動態程序數。

下面的例子來自官方文件:

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 world\n’);
 }).listen(8000);

 console.log(`Worker ${process.pid} started`);

}

PM2 叢集模式

如果你在使用 PM2 作為你的流程管理器(我也建議你這麼做),那麼有一個神奇的群集功能可以讓你跨所有核心擴充套件流程,而無需擔心叢集模組。 PM2 守護程式將承擔“master”程序的角色,它將生成你的應用程式的 N 個程序作為 worker 程序, 並進行迴圈平衡。

通過這個方法,只需要按你為單核心用途一樣地編寫你的應用程式(我們稍後再提其中的一些注意事項),而 PM2 將關注多核心部分。

在叢集模式下啟動你的應用程式後,你可以使用“pm2 scale”調整動態例項數,並執行“0-second-downtime”重新載入,程序重新串聯,以便始終至少有一個線上程序。

在生產中執行節點時,如果你的程序像很多其他你應該考慮的有用的東西一樣崩潰了,那麼 PM2 作為程序管理器將負責重新啟動你的程序。

如果你需要進一步擴充套件,那麼你也許需要部署更多的機器。

具有網路負載均衡的多臺機器

跨多臺機器進行擴充套件的主要概念類似於在多核心上進行擴充套件,有多臺機器,每臺機器執行一個或多個程序,以及用於將流量重定向到每臺機器的均衡器。

一旦請求被髮送到特定的節點,剛才所提到的內部均衡器傳送該流量到特定的程序。

可以以不同方式部署網路平衡器。 如果使用 AWS 來配置你的基礎架構,那麼一個不錯的選擇是使用像 ELB(Elastic Load Balancer,彈性負載均衡器)這樣的託管負載均衡器,因為它支援自動擴充套件等有用功能,並且易於設定。

但是如果你想按傳統的方式來做,你可以自己部署一臺機器並用 NGINX 設定一個均衡器。 指向上游的反向代理的配置對於這個任務來說非常簡單。 下面是配置示例:

http {

 upstream myapp1 {
   server srv1.example.com;
   server srv2.example.com;
   server srv3.example.com;
 }

 server {
   listen 80;
   location / {
    proxy_pass http://myapp1;
   }
 }

}

通過這種方式,負載均衡器將是你的應用程式暴露給外部世界的唯一入口點。 如果擔心它成為基礎架構的單點故障,可以部署多個指向相同伺服器的負載均衡器。

為了在均衡器之間分配流量(每個均衡器都有自己的 IP 地址),可以向主域新增多個 DNS“A”記錄,從而 DNS 解析器將在你的均衡器之間分配流量,每次都解析為不同的 IP 地址。通過這種方式,還可以在負載均衡器上實現冗餘。

我們在這裡看到的是如何在不同級別擴充套件 Node.js 應用程式,以便從你的基礎架構(從單節點到多節點和多均衡器)獲得儘可能高的效能,但要小心:如果想在多程序環境中使用你的應用程式,必須做好準備,否則會遇到一些問題和不期望的行為。

在向上擴充套件你的程序時,為了避免出現不期望的行為,現在我們來談談必須考慮到的一些方面。

讓Node.js 應用程式做好擴充套件準備

從 DB 中分離應用程式例項

首先不是程式碼問題,而是你的基礎結構。

如果希望你的應用程式能夠跨不同主機進行擴充套件,則必須把你的資料庫部署在獨立的機器上,以便可以根據需要自由複製應用程式機器。

在同一臺機器上部署用於開發目的的應用程式和資料庫可能很便宜,但絕對不建議用於生產環境,其中的應用程式和資料庫必須能夠獨立擴充套件。 這同樣適用於像 Redis 這樣的記憶體資料庫。

無狀態

如果生成你的應用程式的多個例項,則每個程序都有自己的記憶體空間。 這意味著即使在一臺機器上執行,當你在全域性變數中儲存某些值,或者更常見的是在記憶體中儲存會話時,如果均衡器在下一個請求期間將您重定向到另一個程序,那麼你將無法在那裡找到它。

這適用於會話資料和內部值,如任何型別的應用程式範圍的設定。對於可在執行時更改的設定或配置,解決方案是將它們儲存在外部資料庫(儲存或記憶體中)上,以使所有程序都可以訪問它們。

使用 JWT 進行無狀態身份驗證

身份驗證是開發無狀態應用程式時要考慮的首要主題之一。 如果將會話儲存在記憶體中,它們將作用於這單個程序。

為了正常工作,應該將網路負載均衡器配置為,始終將同一使用者重定向到同一臺機器,並將本地使用者重定向到同一使用者始終重定向到同一程序(粘性會話)。

解決此問題的一個簡單方法是將會話的儲存策略設定為任何形式的永續性,例如,將它們儲存在 DB 而不是 RAM 中。 但是,如果你的應用程式檢查每個請求的會話資料,那麼每次 API 呼叫都會進行磁碟讀寫操作(I / O),從效能的角度來看,這絕對不是好事。

更好,更快的解決方案(如果你的身份驗證框架支援)是將會話儲存在像 Redis 這樣的記憶體資料庫中。 Redis 例項通常位於應用程式例項外部,例如 DB 例項,但在記憶體中工作會使其更快。 無論如何,在 RAM 中儲存會話會在併發會話數增加時需要更多記憶體。

如果想採用更有效的無狀態身份驗證方法,可以看看 JSON Web Tokens。

JWT 背後的想法很簡單:當用戶登入時,伺服器生成一個令牌,該令牌本質上是包含有效負載的 JSON 物件的 base64 編碼,加上簽名獲得的雜湊,該負載具有伺服器擁有的金鑰。 有效負載可以包含用於對使用者進行身份驗證和授權的資料,例如 userID 及其關聯的 ACL 角色。 令牌被髮送回客戶端並由其用於驗證每個 API 請求。

當伺服器處理傳入請求時,它會獲取令牌的有效負載並使用其金鑰重新建立簽名。 如果兩個簽名匹配,則可以認為有效載荷有效並且不被改變,並且可以識別使用者。

重要的是要記住 JWT 不提供任何形式的加密。 有效負載僅用 base64 編碼,並以明文形式傳送,因此如果需要隱藏內容,則必須使用 SSL。

被 jwt.io 借用的以下模式恢復了身份驗證過程:

在認證過程中,伺服器不需要訪問儲存在某處的會話資料,因此每個請求都可以由非常有效的方式由不同的程序或機器處理。 RAM 中不儲存資料,也不需要執行儲存 I / O,因此在向上擴充套件時這種方法非常有用。

S3 上的儲存

使用多臺機器時,無法將使用者生成的資產直接儲存在檔案系統上,因為這些檔案只能由該伺服器本地的程序訪問。 解決方案是,將所有內容儲存在外部服務上,可以儲存在像 Amazon S3 這樣的專用服務上,並在你的資料庫中僅儲存指向該資源的絕對 URL。

然後,每個程序 / 機器都可以以相同的方式訪問該資源。

使用 Node.js 的官方 AWS sdk 非常簡單,可以輕鬆地將服務整合到你的應用程式中。 S3 非常便宜並且針對此目的進行了優化。即使你的應用程式不是多程序的,它也是一個不錯的選擇。

正確配置 WebSockets

如果你的應用程式使用 WebSockets 進行客戶端之間或客戶端與伺服器之間的實時互動,則需要連結後端例項,以便在連線到不同節點的客戶端之間正確傳播廣播訊息或訊息。

Socket.io 庫為此提供了一個特殊的介面卡,稱為 socket.io-redis,它允許你使用 Redis pub-sub 功能連結伺服器例項。

為了使用多節點 socket.io 環境,還需要強制協議為“websockets”,因為長輪詢(long-polling)需要粘性會話才能工作。

以上這些對於單節點環境來說也是好的例項。

效率和效能的其他良好實踐

接下來,我們將介紹一些可以進一步提高效率和效能的其他實踐。

Web 和 worker 程序

你可能知道,Node.js 實際上是單執行緒的,因此該程序的單個例項一次只能執行一個操作。 在 Web 應用程式的生命週期中,執行許多不同的任務:管理 API 呼叫,讀取 / 寫入 DB,與外部網路服務通訊,執行某種不可避免的 CPU 密集型工作等。

雖然你使用非同步程式設計,但將所有這些操作委派給響應 API 呼叫的同一程序可能是一種非常低效的方法。

一種常見的模式是基於兩種不同型別的程序之間的職責分離,這兩種型別的程序組成了你的應用程式,通常是 Web 程序和 worker 程序。

Web 程序主要用於管理傳入的網路呼叫,並儘快傳送它們。 每當需要執行非阻塞任務時,例如傳送電子郵件 / 通知、編寫日誌、執行觸發操作,其結果是不需要響應 API 呼叫,web 程序將操作委派給 worker 程序。

Web 和 worker 程序之間的通訊可以用不同的方式實現。 一種常見且有效的解決方案是優先順序佇列,如下一段所描述的 Kue 中實現的優先順序佇列。

這種方法的一大勝利是,可以在相同或不同的機器上獨立擴充套件 web 和 worker 程序。

例如,如果你的應用程式是高流量應用程式,幾乎沒有生成的副作用,那麼可以部署比 worker 程序更多的 web 程序,而如果很少有網路請求為 worker 程序生成大量作業,則可以重新分發相應的資源。

Kue

為了使 web 和 worker 程序相互通訊,佇列是一種靈活的方法,可以讓你不必擔心程序間通訊。

Kue 是基於 Redis 的 Node.js 的通用佇列庫,允許你以完全相同的方式放入在相同或不同機器上生成的通訊程序。

任何型別的程序都可以建立作業並將其放入佇列,然後將 worker 程序配置為選擇這些作業並執行它們。 可以為每項工作提供許多選項,如優先順序、TTL、延遲等。

你生成的 worker 程序越多,執行這些作業所需的並行吞吐量就越多。

Cron

應用程式通常需要定期執行某些任務。 通常,這種操作通過作業系統級別的 cron 作業進行管理,從你的應用程式外部呼叫單個指令碼。

在新機器上部署你的應用程式時,用此方法就需要額外的工作,如果要自動部署,這會使程序感到不自在。

實現相同結果的更自在的方法是使用 NPM 上的可用 cron 模組。 它允許你在 Node.js 程式碼中定義 cron 作業,使其獨立於 OS 配置。

根據上面描述的 web / worker 模式,worker 程序可以建立 cron,它呼叫一個函式,定期將新作業放入佇列。

使用佇列使其更加乾淨,並可以利用 kue 提供的所有功能,如優先順序,重試等。

當你有多個 worker 程序時會出現問題,因為 cron 函式會同時喚醒每個程序上的應用程式,並將多次執行的同一作業放入佇列副本中。

為了解決這個問題,有必要確定將執行 cron 操作的單個 worker 程序。

領導者選舉(Leader election)和 cron-cluster(cron 叢集)

這種問題被稱為“領導者選舉”,對於這個特定的場景,有一個 NPM 包為我們做了一個叫做 cron-cluster 的技巧。

它暴露了為 cron 模組提供動力的相同 API,但在設定過程中,它需要一個 redis 連線,用於與其他程序通訊並執行領導者選舉演算法。

使用 redis 作為單一事實來源,所有程序都會同意誰將執行 cron,並且只有一份作業副本將被放入佇列中。 之後,所有 worker 程序都將有資格像往常一樣執行作業。

快取 API 呼叫

伺服器端快取是提高 API 呼叫的效能和反應性的常用方法,但它是一個非常廣泛的主題,有很多可能的實現。

在像我們所描述的分散式環境中,使用 redis 來儲存快取的值可能是使所有節點表現相同的最佳方法。

快取需要考慮的最困難的方面是失效。 快速而簡陋的解決方案只考慮時間,因此快取中的值在固定的 TTL 之後重新整理,缺點是不得不等待下一次重新整理以檢視響應中的更新。

如果你有更多的時間,最好在應用程式級別實現失效,在 DB 上值更改時手動重新整理 redis 快取上的記錄。

結 論

我們在本文中介紹了一些有關擴充套件和效能的一些主題。 文中提供的建議可以作為指導,可以根據你的專案的特定需求進行定製。

 

 

本次給大家推薦一個免費的學習群,裡面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。
對web開發技術感興趣的同學,歡迎加入Q群:943129070,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視訊資料。
最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。