天天看點

前後端分離的思考與實踐(六)Nginx + Node.js + Java 的軟體棧部署實踐

在這個體系中,Nginx 将請求轉發給 Java 應用,後者處理完事務,再将資料用 Velocity 模闆渲染成最終的頁面。

引入 Node.js 之後,我們勢必要面臨以下幾個問題:

技術棧的拓撲結構該如何設計,部署方式該如何選擇,才算是科學合理?

項目完成後,該如何切分流量,對運維來說才算是友善快捷?

遇到線上的問題,如何最快地解除險情,避免更大的損失?

如何確定應用的健康情況,在負載均衡排程的層面加以管理?

<a href="http://jbcdn2.b0.upaiyun.com/2014/06/2dfe58e34863283cabb8e71a6486de87.png" target="_blank"></a>

<a href="http://jbcdn2.b0.upaiyun.com/2014/06/636063a0d0062abcc824ef4ebbb164e0.png" target="_blank"></a>

上面的結構看起來沒什麼問題了,但其實新問題還等在前面。在傳統結構中,Nginx 與 Java 是部署在同一台伺服器上的,Nginx 監聽 80 端口,與監聽高位 7001 端口的 Java 通信。現在引入了 Node.js ,需要新跑一個監聽端口的程序,到底是将 Node.js 與 Nginx + Java 部署在同一台機器,還是将 Node.js 部署在單獨的叢集呢?

我們來比較一下兩種方式各自特點:

<a href="http://jbcdn2.b0.upaiyun.com/2014/06/2dfce85760e3bb15814e89375f2b5b43.png" target="_blank"></a>

淘寶網收藏夾是一個擁有千萬級日均 PV 的應用,對穩定性的要求性極高(事實上任何産品的線上不穩定都是不能接受的)。如果采用同叢集部署方案,隻需要一次檔案分發,兩次應用重新開機即可完成釋出,萬一需要復原,也隻需要操作一次基線包。性能上來說,同叢集部署也有一些理論優勢(雖然内網的交換機帶寬與延時都是非常樂觀的)。至于一對多或者多對一的關系,理論上可能做到伺服器更加充分的利用,但相比穩定性上的要求,這一點并不那麼急迫需要去解決。是以在收藏夾的改造中,我們選擇了同叢集部署方案。

為了保證最大程度的穩定,這次改造并沒有直接将 Velocity 代碼完全去掉。應用叢集中有将近 100 台伺服器,我們以伺服器為粒度,逐漸引入流量。也就是說,雖然所有的伺服器上都跑着 Java + Node.js 的程序,但 Nginx 上有沒有相應的轉發規則,決定了擷取這台伺服器上請求寶貝收藏的請求是否會經過 Node.js 來處理。其中 Nginx 的配置為:

XHTML

1

2

3

location = "/item_collect.htm" {

    proxy_pass http://127.0.0.1:6001; # Node.js 程序監聽的端口

}

隻有添加了這條 Nginx 規則的伺服器,才會讓 Node.js 來處理相應請求。通過 Nginx 配置,可以非常友善快捷地進行灰階流量的增加與減少,成本很低。如果遇到問題,可以直接将 Nginx 配置進行復原,瞬間回到傳統技術棧結構,解除險情。

第一次釋出時,我們隻有兩台伺服器上啟用了這條規則,也就是說大緻有不到 2% 的線上流量是走 Node.js 處理的,其餘的流量的請求仍然由 Velocity 渲染。以後視情況逐漸增加流量,最後在第三周,全部伺服器都啟用了。至此,生産環境 100% 流量的商品收藏頁面都是經 Node.js 渲染出來的(可以檢視源代碼搜尋 Node.js 關鍵字)。

灰階過程并不是一帆風順的。在全量切流量之前,遇到了一些或大或小的問題。大部分與具體業務有關,值得借鑒的是一個技術細節相關的陷阱。

在傳統的架構中,負載均衡排程系統每隔一秒鐘會對每台伺服器 80 端口的特定 URL 發起一次 <code>get</code> 請求,根據傳回的 HTTP Status Code 是否為 <code>200</code> 來判斷該伺服器是否正常工作。如果請求 1s 後逾時或者 HTTP Status Code 不為 <code>200</code>,則不将任何流量引入該伺服器,避免線上問題。

這個請求的路徑是 Nginx -&gt; Java -&gt; Nginx,這意味着,隻要傳回了 <code>200</code>,那這台伺服器的 Nginx 與 Java 都處于健康狀态。引入 Node.js 後,這個路徑變成了 Nginx -&gt; Node.js -&gt; Java -&gt; Node.js -&gt; Nginx。相應的代碼為:

4

5

6

7

8

9

10

11

12

13

var http = require('http');

    app.get('/status.taobao', function(req, res) {

        http.get({

            host: '127.1',

            port: 7001,

            path: '/status.taobao'

        }, function(res) {

            res.send(res.statusCode);

        }).on('error', function(err) {

            logger.error(err);

            res.send(404);

        });

    });

但是在測試過程中,發現 Node.js 在轉發這類請求的時候,每六七次就有一次會耗時幾秒甚至十幾秒才能得到 Java 端的傳回。這樣會導緻負載均衡排程系統認為該伺服器發生異常,随即切斷流量,但實際上這台伺服器是能夠正常工作的。這顯然是一個不小的問題。

排查一番發現,預設情況下, Node.js 會使用 <code>HTTP Agent</code> 這個類來建立 HTTP 連接配接,這個類實作了 socket 連接配接池,每個主機+端口對的連接配接數預設上限是 5。同時 <code>HTTP Agent</code> 類發起的請求中預設帶上了 <code>Connection: Keep-Alive</code>,導緻已傳回的連接配接沒有及時釋放,後面發起的請求隻能排隊。

最後的解決辦法有三種:

禁用 <code>HTTP Agent</code>,即在在調用 <code>get</code> 方法時額外添加參數 <code>agent: false</code>,最後的代碼為:

14

            agent: false,

設定 <code>http</code> 對象的全局 socket 數量上限:

http.globalAgent.maxSockets = 1000;

在請求傳回的時候及時主動斷開連接配接:

http.get(options, function(res) {

    }).on("socket", function (socket) {

    socket.emit("agentRemove"); // 監聽 socket 事件,在回調中派發 agentRemove 事件

});

實踐上我們選擇第一種方法。這麼調整之後,健康檢查就沒有再發現其它問題了。

Node.js 與傳統業務場景結合的實踐才剛剛起步,仍然有大量值得深入挖掘的優化點。比比如,讓 Java 應用徹底中心化後,是否可以考分叢集部署,以提高伺服器使用率。或者,釋出與復原的方式是否能更加靈活可控。等等細節,都值得再進一步研究。

本文轉自 h2appy  51CTO部落格,原文連結:http://blog.51cto.com/h2appy/1851568,如需轉載請自行聯系原作者