微信公衆號:[前端一鍋煮]
一點技術、一點思考。
問題或建議,請公衆號留言。
Node.js 做爲後臺服務性能是很是關鍵的一點,而影響 Node.js 的性能不只僅要考慮其自己的因素,還應該考慮所在服務器的一些因素。好比網絡 I/O 、磁盤 I/O 以及其餘內存、句柄等一些問題。下面將詳細地分析影響其性能的因素緣由,以及部分優化解決方案。前端
CPU 負責了程序的運行和業務邏輯的處理,而 CPU 密集型表示的主要是 CPU 承載了比較複雜的運算。數據庫
在 Node.js 中因爲主線程是單線程的,不管是主線程邏輯,仍是回調處理邏輯,最終都是在主線程處理,那麼若是該線程一直在處理複雜的計算,其餘請求就沒法再次進來,也就是單個用戶就能夠阻塞全部用戶的請求。這樣就會由於某些用戶的複雜運算,而影響到整個系統的請求處理,並且這種複雜運算佔用的 CPU 時間越久,就會致使請求堆積,而進一步致使系統處於崩潰狀態沒法恢復。所以保持主線程的通暢是很是關鍵的。數組
在 Node.js 中有如下幾種狀況,會影響到主線程的運行,應主動避免:緩存
對此咱們考慮如下優化方向:服務器
I/O(Input/Output)意思是輸入輸出,其實就是數據傳遞的一個過程,做爲後臺服務須要更多地與外部進行數據交互,那麼就免不了 I/O 操做。微信
I/O 分爲如下 5 種模型,在介紹分類以前,咱們先了解 I/O 在系統層面會有 2 個階段(以讀爲例子):網絡
例如讀取一個文件,咱們必需要等待文件讀取完成後,也就是完成上面所說的兩個階段,才能執行其餘邏輯,而當前是沒法釋放 CPU 的,所以沒法去處理其餘邏輯。多線程
非阻塞的意思是,咱們發起了一個讀取文件的指令,系統會返回正在處理中,而後這時候若是要釋放進程中的 CPU 去處理其餘邏輯,你就必須間隔一段時間,而後不停地去詢問操做系統,使用輪詢的判斷方法看是否讀取完成了。併發
這一模型主要是爲了解決輪詢調度的問題,咱們能夠將這些 I/O Socket 處理的結果統一交給一個獨立線程來處理,當 I/O Socket 處理完成後,就主動告訴業務,處理完成了,這樣不須要每一個業務都來進行輪詢查詢了。異步
它包括目前常見的三種類型:select 、poll 和 epoll。首先 select 是比較舊的,它和 poll 的區別在於 poll 使用的是鏈表來保存 I/O Socket 數據,而 select 是數組,所以 select 會有上限 1024,而 poll 則沒有。select、poll 與 epoll 的區別在於,前二者不會告訴你是哪一個 I/O Socket 完成了,而 epoll 會通知具體哪一個 I/O Socket 完成了哪一個階段的操做,這樣就不須要去遍歷查詢了。
固然這裏有一個重點是這三者只會告知文件讀取進入了操做系統內核緩衝區,也就是上面咱們所說的第一階段,可是第二階段從內核拷貝到應用程序地址空間仍是同步等待的。
這種模式和多路複用的區別在於不須要有其餘線程來處理,而是在完成了讀取進入操做系統內核緩衝區後,立馬通知,也就是第一階段能夠由系統層面來處理,不須要獨立線程來管理,可是第二階段仍是和多路複用同樣。
和信號驅動不一樣的是,異步 I/O 是兩個階段都完成了之後,纔會通知,並非第一階段完成。
咱們常說 Node.js 是一個異步 I/O 這個是沒有錯的。具體來講 Node.js 是其 libv 庫自行實現的一種相似異步 I/O 的模型,對於 Node.js 應用來講是一個異步 I/O,所以無須處理兩個過程,而在 libv 內部實現,則是多線程的一個 epoll 模型。
在通常狀況下磁盤 I/O 不會影響到主線程性能,由於磁盤 I/O 是異步其餘線程處理。可是由於服務器磁盤性能是必定的,若是在高併發狀況下,磁盤 I/O 壓力較大,從而致使磁盤 I/O 的服務性能降低,就會從側面影響機器性能,致使 Node.js 服務性能受影響。
在後臺服務中常見的網絡 I/O 有以下幾種類型:
網絡 I/O 的成本是最高的,涉及兩個最重要的點:
對此,咱們能夠從如下幾個方面來考慮優化的策略:
網絡 I/O 通常不影響主線程邏輯,其請求的服務每每是瓶頸端,從而影響 Node.js 中涉及該網絡服務的請求。可是網絡 I/O 堆積較多也會側面影響:服務器自己的網絡模塊問題以及Node.js 性能,致使其餘服務接口受影響。
緩存是臨時的一塊存儲空間,用於存放訪問頻次較高的數據,用空間換響應速度,核心是減小用戶對數據庫的查詢壓力。可是若是沒有應用好緩存,將會致使一些不可見或者說很難定位的問題,主要是三點:緩存雪崩、緩存擊穿和緩存穿透。
大部分數據都有一個過時時間的概念,假設咱們有一批數據是經過定時服務從數據庫寫入緩存中,而後咱們統一設置了過時時間。當這個時間節點到了,可是因爲某種緣由數據又沒有從數據庫寫入緩存,致使這時候全部的數據都會前往數據庫查詢數據,從而引發數據庫查詢壓力,致使數據庫併發過大而癱瘓沒法正常服務。
那麼應該如何應對呢?
這個概念和緩存雪崩有點相似,但不是大面積的緩存過時失效,而是某個訪問頻次較高的數據失效了,從而致使這一刻高併發的請求所有穿透到了數據庫,從而數據庫併發壓力較高,響應較慢,也進一步致使數據庫異常,影響其餘業務。
那麼應該如何應對呢?
對於訪問頻繁的數據,這裏就會出現一種狀況,好比說查詢信息一直是空數據,空數據按理不屬於訪問頻繁較高的數據,因此通過了緩存,可是並無緩存該空數據,而是直接穿透進入了數據庫,雖然數據庫查詢也是空數據,可是仍是須要通過數據庫的查詢,這種現象就是擊穿了緩存直接前往了數據庫查詢。
那麼應該如何應對呢?
在多進程 cluster 模式中,由於全部的請求都必須通過 master 進程進行分發,同時接收處理 worker 進程的返回。
所以在實際開發過程當中,若是啓用了比較多的 worker 進程,而主進程只有一個,從而在單機高併發時(2 萬以上的每秒併發請求)會致使 master 進程處理瓶頸,這樣就影響到了服務性能,而且這時候你會發現 worker 進程的 CPU 並無任何壓力。
這點很是重要,在生產環境下通常很難發現這類問題,不過應該有這樣的一個概念:大概在 2 萬以上的併發時,master 進程會存在性能瓶頸。
在 32 位服務器上 Node.js 的內存限制是 0.7 G,而在 64 位服務器上則是 1.4 G,而這個限制主要是由於 Node.js 的垃圾回收線程在超過限制內存時,回收時長循環會大於 1s,從而會影響性能問題。
如今咱們通常會啓用多個進程,若是每一個進程損耗 1.4 G,那麼加起來可能超出了服務器內存上限,從而致使服務器癱瘓。其次若是內存不會超出服務器上限,而是在達到必定上限時,也就是咱們上面說的 0.7 G和 1.4 G,會致使服務器重啓,從而會致使接口請求失敗的問題。
句柄能夠簡單理解爲一個 ID 索引,經過這個索引能夠訪問到其餘的資源,好比說文件句柄、網絡 I/O 操做句柄等等,而通常服務器句柄都有上限。當 Node.js 沒有控制好句柄,好比說無限的打開文件並未關閉,就會出現句柄泄漏問題,而這樣會致使服務器異常,從而影響 Node.js 服務。