前後端分離的思考與實踐(六)

發表於2014-06-20

Nginx + Node.js + Java 的軟體棧部署實踐

關於前後端分享的思考,我們已經有五篇文章闡述思路與設計。本文介紹淘寶網收藏夾將 Node.js 引入傳統技術棧的具體實踐。

淘寶網線上應用的傳統軟體棧結構為 Nginx + Velocity + Java,即:2kbuaECNpd

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

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

  1. 技術棧的拓撲結構該如何設計,部署方式該如何選擇,才算是科學合理?
  2. 專案完成後,該如何切分流量,對運維來說才算是方便快捷?
  3. 遇到線上的問題,如何最快地解除險情,避免更大的損失?
  4. 如何確保應用的健康情況,在負載均衡排程的層面加以管理?

系統拓撲

按照我們在前後端分離的思考與實踐(二)- 基於前後端分離的模版探索一文中的思路,Velocity 需要被 Node.js 取代,從而讓這個結構變成:

2kcIkX7AXl

 

這當然是最理想的目標。然而,在傳統棧中首次引入 Node.js 這一層畢竟是個新嘗試。為了穩妥起見,我們決定只在收藏夾的寶貝收藏頁面(shoucang.taobao.com/item_collect.htm)啟用新的技術,其它頁面沿用傳統方案。即,由 Nginx 判斷請求的頁面型別,決定這個請求究竟是要轉發給 Node.js 還是 Java。於是,最後的結構成了:

2kc0OxfysP

 

部署方案

上面的結構看起來沒什麼問題了,但其實新問題還等在前面。在傳統結構中,Nginx 與 Java 是部署在同一臺伺服器上的,Nginx 監聽 80 埠,與監聽高位 7001 埠的 Java 通訊。現在引入了 Node.js ,需要新跑一個監聽埠的程式,到底是將 Node.js 與 Nginx + Java 部署在同一臺機器,還是將 Node.js 部署在單獨的叢集呢?
我們來比較一下兩種方式各自特點:

TB14LwSFFXXXXaUXFXXXx3jKpXX-1566-426

 

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

灰度方式

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

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

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

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

健康檢查

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

這個請求的路徑是 Nginx -> Java -> Nginx,這意味著,只要返回了 200,那這臺伺服器的 Nginx 與 Java 都處於健康狀態。引入 Node.js 後,這個路徑變成了 Nginx -> Node.js -> Java -> Node.js -> Nginx。相應的程式碼為:

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

排查一番發現,預設情況下, Node.js 會使用 HTTP Agent 這個類來建立 HTTP 連線,這個類實現了 socket 連線池,每個主機+埠對的連線數預設上限是 5。同時 HTTP Agent 類發起的請求中預設帶上了 Connection: Keep-Alive,導致已返回的連線沒有及時釋放,後面發起的請求只能排隊。

最後的解決辦法有三種:

  • 禁用 HTTP Agent,即在在呼叫 get 方法時額外新增引數 agent: false,最後的程式碼為:

設定 http 物件的全域性 socket 數量上限:

在請求返回的時候及時主動斷開連線:

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

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

【附】相關文章列表

  1. 《前後端分離的思考與實踐(一)》
  2. 《前後端分離的思考與實踐(二)》
  3. 《前後端分離的思考與實踐(三)》
  4. 《前後端分離的思考與實踐(四)》
  5. 《前後端分離的思考與實踐(五)》

 

 

相關文章