Netflix分享Node.js效能調優

banq發表於2014-11-20
Netflix最近分享了它們使用Node.js建設新一代Netflix.com的Web應用過程中的效能調優心得

首先注意到的是Node.JS的請求延遲,JS應用程式隨著時間增加,其耗費CPU也是超預期的,稱為燃燒CPU,從而帶來了高延遲。不斷重啟作為臨時解決方案,然後在Linux EC2使用新的效能分析工具迅速找到問題根源。

具體來說,一些端點伺服器會有1毫秒延遲,並且隨著每小時增加10ms,同時還看到CPU使用量增加:

[img index=2]

該圖為每個地區的請求延遲發生時間,每種顏色對應不同AWS,你可以看到延遲每小時穩步遞增10ms,峰值大概在60ms。

最初推測,自己的請求處理程式可能有一些錯誤,比如記憶體洩漏導致延遲。 我們隔離測試了應用程式,js堆大小增加到32 gb,新增了對應用程式對每個請求處理的延遲和總體請求延遲等指標測量。

我們看到應用程式對每個請求處理的延遲一直保持不變,程式堆的大小也基本保持不變,大概是1.2GB,但是總體請求延遲和CPU使用率在上升。這就撇清了我們自己的應用程式的責任,將問題根源指向堆疊等更深層次。

一些服務會額外需要60ms,我們需要CPU消耗使用率的校驗調校虛擬圖即火焰圖。

對於那些不熟悉火焰圖,最好讀布蘭登格雷格優秀的文章,這裡進行大概總結:
1.每個框代表一個在堆疊(堆疊幀)中的函式
2.y軸顯示堆疊深度(堆疊上的幀數),頂部框顯示了佔據cpu的函式。 而下面的一切都是起源。 下面的函式是上上面函式的“父母”,就像前面所示的堆疊跟蹤。
3.x軸跨越樣本標本。 它不像大多數圖那樣顯示從左到右時間的流逝, 從左到右排序沒有意義(這是按照字母順序排列)。
4.框寬度顯示對CPU使用時間,寬框代表的函式慢於窄框代表的函式。
5.如果採取多執行緒併發執行和取樣,取樣數量會超過執行時間。
6.顏色不重要,這裡隨機選擇暖色,代表CPU有多熱。

下面這張JS火焰圖是使用DTrace取樣得到,取樣方法見:Profiling Node.js,,Google v8團隊最近新增perf_events支援v8,它允許在Linux上採取的JavaScript堆疊分析。 Node.js 0.11.13版本支援,具體細節參考:node.js Flame Graphs on Linux

[img index=1]

這裡有火焰圖的原始SVG,我們看到應用程式達到最高的棧見Y軸,而從X軸看到也在這些棧花費了很多時間,堆疊幀stack frame是由所有指向Express.js的路由器處理router.handle和 router.handle.next 函式。Express原始碼有一些有趣的特點:
1.路由處理程式將所有端點儲存在一個全域性的陣列中。
2.Express採取遞迴遍歷直至找到正確路由處理器並呼叫處理器。

全域性陣列不是理想的資料結構,不知Express為什麼不採取一個時間不變的資料結構如map來儲存它們的應用處理器,每個請求都需要昂貴的O(n)查詢陣列路線,更有甚者,陣列是遞迴遍歷,這就解釋了為什麼我們看見如此高高堆起的火焰圖

最要命的是Express.js允許你為同一個路由設定相同的路由處理器,你可以設定一個請求鏈(banq注:職責鏈模式確實浪費效能)。如下:

[a, b, c, c, c, c, d, e, f, g, h]
裡面有四個相同處理器C,程式會在陣列中第一個C出現的位置終止,而d處理器只會在陣列位置6地方結束,就不必要花費時間跨越a b和多個c了,下面透過應用驗證:

var express = require('express');
var app = express();
app.get('/foo', function (req, res) {
   res.send('hi');
}); 
// add a second foo route handler
app.get('/foo', function (req, res) {
   res.send('hi2');
});
console.log('stack', app._router.stack);
app.listen(3000);
<p class="indent">

上面增加兩個相同的路由/foo。執行堆疊資訊是:

stack [ { keys: [], regexp: /^\/?(?=/|$)/i, handle: [Function: query] },
 { keys: [],
   regexp: /^\/?(?=/|$)/i,
   handle: [Function: expressInit] },
 { keys: [],
   regexp: /^\/foo\/?$/i,
   handle: [Function],
   route: { path: '/foo', stack: [Object], methods: [Object] } },
 { keys: [],
   regexp: /^\/foo\/?$/i,
   handle: [Function],
   route: { path: '/foo', stack: [Object], methods: [Object] } } ]
<p class="indent">

這裡有兩個相同的路由處理器,當路由處理鏈中出現兩個相同的處理器時JS會丟擲錯誤。

這也解釋了隨著時間增加延遲增加的原因,最有可能的洩漏是在我們應用程式碼中,出現了相同重複的路由處理器,新增額外日誌觀察路由處理器陣列,發現每小時增加10個處理器元素,這些處理器都是相同的。


<p class="indent">[...
{ handle: [Function: serveStatic],
   name: 'serveStatic',
   params: undefined,
   path: undefined,
   keys: [],
   regexp: { /^\/?(?=\/|$)/i fast_slash: true },
   route: undefined },
 { handle: [Function: serveStatic],
   name: 'serveStatic',
   params: undefined,
   path: undefined,
   keys: [],
   regexp: { /^\/?(?=\/|$)/i fast_slash: true },
   route: undefined },
 { handle: [Function: serveStatic],
   name: 'serveStatic',
   params: undefined,
   path: undefined,
   keys: [],
   regexp: { /^\/?(?=\/|$)/i fast_slash: true },
   route: undefined },
...
]
<p class="indent">

新增相同的Express路由, js一個小時會增加10倍處理時間。 進一步的基準測試顯示僅僅是遍歷每一個處理程式例項成本約1毫秒的CPU時間。 這解釋了為什麼響應延遲每小時增加10 ms。

當我們停止增加相同的路由處理器以後,延遲和CPU耗費陡然就下降了。最後問題解決了。


[該貼被admin於2014-11-20 22:26修改過]

相關文章