極速Node.js:來自LinkedIn的10個性能提升祕籍

在之前的文章裏,我們討論了我們如何測試LinkedIn的移動服務集羣,包括我們的 Node.js 移動端服務器。今天,我們想給你們說說我們是如何讓這個服務器快速運行的。這裏就是我們在 Node.js 上的十大性能提升祕籍:

1. 避免同步的代碼

Node.js 從一開始的設計就是單線程的。要讓一個線程處理很多併發請求,你就永遠不要讓這個線程在阻塞、同步或運行時間很長的操作中等待。Node.js的一個超凡脫俗的特性就是它從上到下都是設計和實現爲異步模式。這使它對事件驅動的應用是絕配。

不幸的是,在Node.js裏進行同步/阻塞式的調用還是有可能的。例如,很多文件系統操作都提供了異步和同步版本,例如writeFile和writeFileSync。即便你在自己的代碼裏避免了同步方法,卻有可能漫不經心地引入了一個外部庫,而在該庫中使用了阻塞式調用。當你這麼做了之後,它對性能的影響是巨大的。

我們最初的日誌實現就偶然性地引入了一個寫入磁盤的同步調用。在我們進行性能測試之前,沒人注意到它的存在。當我們在一臺開發機上對比單個Node.js服務器性能的時候,這一個同步調用導致性能從每秒處理1000個請求下降到幾十個!

1
2
3
4
5
6
7
8
// Good: write files asynchronously
fs.writeFile('message.txt','Hello Node',function(err) {
  console.log("It's saved and the server remains responsive!");
});
  
// BAD: write files synchronously
fs.writeFileSync('message.txt', 'Hello Node');
console.log("It's saved, but you just blocked ALL requests!");

2. 關閉socket池

Node.js的http模塊會自動使用socket池,缺省是每臺主機限制爲5個socket。雖然這種socket複用方式可能對控制資源增長的速度有利,但如果你需要處理很多同時需要從同一臺主機獲取數據的併發請求,這個socket池會成爲嚴重的性能瓶頸。在這種情況下,好的辦法是增大maxSockets參數或者乾脆把socket池disable掉。

1
2
3
4
5
6
// Disable socket pooling
  
varhttp = require('http');
varoptions = {.....};
options.agent =false;
varreq = http.request(options)

3. 不要用Node.js處理靜態資源

對於靜態資源,例如CSS和圖像,使用標準的web服務器而不是Node.js來處理。例如,LInkedIn mobile使用nginx。我們還利用內容分發網絡(CDN),它能把靜態資源複製到分佈在全世界的很多服務器上。這樣做有兩大益處:①減少了Node.js服務器的負載,②CDN能讓靜態內容從離用戶比較近的服務器上傳輸過去,這樣減少了延遲。

4. 在客戶端渲染頁面

讓我們快速比較一下服務器端渲染和客戶端渲染頁面。如果我們讓Node.js在服務器端渲染,我們會對所有請求發回一個類似於本文的HTML頁面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- An example of a simple webpage rendered entirely server side -->
  
<!DOCTYPE html>
<html>
  <head>
    <title>LinkedIn Mobile</title>
  </head>
  <body>
    <div class="header">
      <img src="http://mobile-cdn.linkedin.com/images/linkedin.png"alt="LinkedIn"/>
    </div>
    <div class="body">
      Hello John!
    </div>
  </body>
</html>

請注意,在這個頁面上,除了用戶名之外,所有內容都是靜態的:也就是說,對於每個用戶和每次刷新都是完全相同的。所以更有效率的方法是讓Node.js只以JSON格式返回頁面所需的動態數據。

1
{"name":"John"}

頁面的其他部分,也就是所有的靜態HTML標記,可以放到一個JavaScript模板裏(例如一個underscore.js模板)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- An example of a JavaScript template that can be rendered client side -->
  
<!DOCTYPE html>
<html>
  <head>
    <title>LinkedIn Mobile</title>
  </head>
  <body>
    <div class="header">
      <img src="http://mobile-cdn.linkedin.com/images/linkedin.png"alt="LinkedIn"/>
    </div>
    <div class="body">
      Hello <%= name %>!
    </div>
  </body>
</html>

性能的受益是這麼來的:根據祕籍3,靜態JavaScript模板可以從你的web服務器(例如nginx)或者CDN獲取。而且,JavaScript模板可以緩存在客戶端或保存在本地存儲,這樣初始加載頁面之後,唯一需要傳給客戶端的就是動態JSON數據,這是最高效率的做法。該方法極大地減少了Node.js服務器上佔用的CPU、 IO和負載。

5. 使用gzip壓縮

大部分服務器和客戶端都支持gzip壓縮請求和響應的數據。一定要在對客戶端發送響應和對遠程服務器發送請求時利用它。

6. 並行

儘量讓你所有的阻塞式操作(比如,對遠程服務的請求,數據庫調用,和文件系統訪問)併發進行。這樣會把總延遲減少爲最慢的哪個阻塞式操作的時間,而不是每個操作順序進行的延遲總和。爲了保持回調函數和錯誤處理整潔清楚,我們使用了Step來進行流程控制。

7. 避免使用session

LinkedIn mobile使用Express框架來管理請求/響應循環。

缺省情況下,session數據是保存在內存裏的,這樣會給服務器增加相當大的開銷,特別是在用戶數增長的情況下。你可以改用外部session存儲,例如MongoDB或Redis,可是這樣每個請求又增加了獲取session數據的遠程調用的開銷。在可能的情況下,最好的辦法是根本就不在服務器端保存狀態信息。通過去掉Express裏類似於 「app.use(express.session()); 」 的配置,實現無session後,你會看到更好的性能。(【譯者注】原文缺了具體配置,這條是譯者自己琢磨着加的)

1
app.use(express.session({ secret:"keyboard cat"}));

8. 使用已編譯的模塊

儘可能使用已編譯的模塊而不是JavaScript模塊。例如,當我們把SHA模塊從一個用JavaScript寫的版本轉到一個包含在Node.js裏的已編譯版本,我們看到了一個巨大的性能飛躍。

1
2
3
// Use built in or binary modules
varcrypto = require('crypto');
varhash = crypto.createHmac("sha1",key).update(signatureBase).digest("base64");

9. 使用標準V8 JavaScript而不是客戶端的庫

大部分JavaScript庫是做出來用在web瀏覽器上的。而在瀏覽器上JavaScript環境真是千差萬別:例如,某個瀏覽器可能支持類似於forEach, map/reduce這樣的功能,而其他瀏覽器卻不支持。結果,客戶端的庫通常包含了很多低效率的代碼來克服瀏覽器帶來的差別。另一方面,在Node.js裏,你能明確知道有哪些JavaScript函數,因爲支撐Node.js的V8 JavaScript引擎是按第5版 ECMA-262 標準描述的ECMAScript標準實現的。通過直接使用標準的V8函數而不是客戶端庫裏的東西,你可以看到相當大的性能改進。

10. 保持你的代碼小規模、輕量級

在設備慢、延遲高的移動客戶端環境下工作,你會學着保持代碼的小規模和輕量級。把這個思路也帶到你服務器端的代碼中去。經常重新思考你的決定,問自己一些問題,例如:「我們真的需要這個模塊嗎?」,「我們爲啥要用這個框架?與其產生的開銷相比它是否值得用?」,「我們能不能用更簡單的方式來做這件事?」。更小,更輕量的代碼總是會更有效率,也會更快。

轉載於:https://my.oschina.net/leyou/blog/467942