如何更好的利用Node.js的性能極限

jopen 9年前發布 | 16K 次閱讀 Node.js Node.js 開發

 

通過使用非阻塞、事件驅動的I/O操作, Node.js 為構建和運行大規模網絡應用及服務提供了很好的平臺,也受到了廣泛的歡迎。其主要特性表現為能夠處理龐大的并且高吞吐量的并發連接,從而構建高性能、高擴 展性的互聯網應用。然而,Node.js單線程的的工作方式及有限的可管理內存使得其計算性能十分有限,限制了某些場景中的應用。近日,Jut開發團隊的 工程師Dave Galbraith 分享了 他們所遇到的Node.js的限制以及超越這些限制的方法。接下來,本文就詳細分析其所遇到的問題及解決思路。

首先,Jut團隊所研發的產品稱為操作數據中心(operations data hub)。該產品是一個專門為研發團隊所設計的流分析平臺,主要用于收集日志以及事件等操作數據,然后根據整體做分析和關聯。其核心功能就是要能夠同時處 理實時數據、歷史數據、結構和非結構數據。具體產品架構如下圖所示。

如何更好的利用Node.js的性能極限

從上圖可以看出,該產品的核心就是數據引擎,包括底層大數據后端和JPC(Juttle Processing Core)兩部分。其中,整體系統需要依賴 ElasticSearchCassandra 等這些大數據后端分系統,進行歷史數據的處理和存儲以及一般數據的復原和管理;JPC采用了Node.js,完成同等對待歷史數據和實時數據、利用日志數據/度量數據/事件數據提出問題以及發送實時數據到瀏覽器來利用 d3 進行可視化等。而且,JPC負責運行Juttle程序。當用戶點擊Juttle程序時,瀏覽器把程序發送到JPC,將其轉換為JavaScript進行執 行。Galbraith提出,Jut團隊選擇JPC中使用Node.js的原因包括采用JavaScript等高級編程語言可以快速完成建模和迭代過程; 鑒于程序前端采用JavaScript實現,后端同樣采用JavaScript可以方便前后端配合和溝通;Node.js擁有強大的開源社區,使得開發團 隊可以有效利用社區的力量等三個方面。JPC就利用了社區中103個NPM包,同時也共享了自己開發的7個包。

盡管Node.js擁有著非常好的特性,JPC的開發團隊還是遇到了一些Node.js不能直接解決的問題:

  1. Node.js的應用程序都是單線程的。這就意味著即使計算機是多核或多處理器的,node.js的應用程序也只能利用其中一個,大大限制了系統性能。
  2. 隨著堆棧變大,Node.js的垃圾收集器變得非常低效。隨著堆棧使用空間超過1GB,垃圾收集的過程開始變得非常慢,會嚴重影響程序的性能。
  3. 因為以上的問題,Node.js限制了堆棧所能使用的空間為1.5GB。一旦超過該范圍,系統就會出錯。
    為了保證Jut系統的高效性,Jut團隊想出了一些解決方案。
  4. </ol>

    首先,針對Node.js單線程引起的性能低下問題,Jut團隊采用了盡量避免利用Node.js進行計算的方式。JPC會把Juttle流圖切割為一些 子圖,然后在Jut平臺的更深層再進行高效執行。以ElasticSearch為例,在未優化之前,數據請求的流程為:ElasticSearch把相關 數據從磁盤中取出->編碼為JSON->通過HTTP協議發送給JPC->JPC解碼JSON文件,執行預想的計算。然 而,ElasticSearch擁有一種 聚合(Aggregation) 功能,能夠跨數據集執行計算。這樣,一次大的請求就可以優化為一個ElasticSearch聚合,避免了中間多次JSON轉換以及Node.js針對大 規模數據進行計算的過程。而且,ElasticSearch和Cassandra都是采用Java編寫,可以有效利用多核或多處理器資源,實現高效率并行 計算。總之,通過盡量避免在Node.js中進行計算的方式,Jut團隊有效提高了系統的性能。

    其次,關于堆棧空間問題。每當用戶讓Node.js服務器向其他服務器發送請求時,用戶都會提供一些相應的函數,來對未來返回的數據進行處理。 Node.js就會把這些函數放到event loop中,等待數據返回,然后調用相應的函數進行處理。這種類似中斷的處理方式,可以大大提高單線程Node.js的效率。然而,一旦event loop中其中一個函數計算的時間過長,系統就會出現問題。以用戶向Node.js發送從其他服務器中請求若干行的數據,然后對這些數據進行數學計算為 例。如果請求的數據超過了1.5GB堆棧大小的限制,計算過程就會占用Node.js很長一段時間,甚至無法完成。由于Node.js為單線程,在這段時 間內,新的請求或者新返回的數據只能放置在event loop的待辦列表中。這樣,Node.js服務器的反應時間將會大大增加,影響其他請求的正常處理。

    為了解決該問題,Jut在任何可能的地方實現了分頁(paging)。這就意味著,系統將不會一次讀取大量數據,而是將其劃分為若干小的請求。在 這些請求中間,系統還可以處理器新的請求。當然,多次請求都需要一定的通信代價的。經過Jut團隊的摸索,20000個點是比較合適的規模——系統仍然能 夠在若干毫秒中執行完畢,而且一般的請求也不需要進行大量分割。

    針對這些問題,Galbraith分享了一個具體的使用案例。作為Jut的忠實客戶, NPM 一直伴隨著Jut從alpha版本一直走到了現在的beta版本。NPM一個具體的任務就是找到所有包中過去兩周下載量最大的前十名,然后在網站中以表格形式的顯示。Juttle程序可以利用非常簡單的代碼完成該任務:

    read -last :2 weeks: | reduce count() by package | sort count -desc | head 10 | @table

    但是,Jut第一次跑該程序的時候就遇到了問題。經過調試發現,問題的原因在于JPC優化了read和reduce操作,將其合并為一個 ElasticSearch聚合操作。由于聚合操作本身并不支持分頁,而NPM的包數要超過數百萬個,ElasticSearch就返回了一個超過百萬個 數組的巨大響應結果,總大小在幾百MB。收到該響應后,JPC就試圖一次處理完畢,導致內存空間使用超過了1.5GB的限制。垃圾收集器開始不斷嘗試回收 空間。結果,處理時間超過了JPC內置的監控服務認為出現異常的閾值——60s。監控服務直接重啟JPC,導致了NPM的任務一直無法完成。

    為了解決該問題,Jut團隊采用了模仿ElasticSearch針對聚合進行分頁的方法。針對返回的包含大量信息的結果,JPC將其切分為可以方便處理的小塊,一個個處理。在一些公開庫的幫助下,修改后的JavaScript代碼如下:

    var points = perform_elasticsearch_aggregtion();`
    Promise.each(_.range(points.length / 20000), function processChunk(n) {
            return Promise.try(function() {
            process(points.splice(0, 20000));       
            }).delay(1);
        });

    其中Promise.each(param1,param2)負責針對第一個參數param1中的每一個元素調用第二個參數中的函數param2;_.range(num)函數接收一個數字num,返回該數字大小的數組。以包含100萬個點為例,上述程序需要調用processChunk()函數50(points.length/20000=1000000/20000=50)次。每次調用負責把20000個點拉出數組,然后調用process()函數進行處理。一旦處理完畢,垃圾收集器就可以對這20000個點占用的空間進行回收。Promise.try()以一個函數作為參數,返回能夠控制其參數中函數執行的對象。該對象的.delay(1)方法表示在多次調用中間允許處理器1ms的暫停去處理其他請求。經過這樣的修改,程序只花費了大概20s的時間就完成了之前NPM的任務。而且,在此期間,服務器還對其他請求進行了響應。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!