Nginx + Node.js + Java 的軟件棧部署實踐
# 起
關于前后端分享的思考,我們已經有五篇文章闡述思路與設計。本文介紹淘寶網[收藏夾](http://shoucang.taobao.com/)將 Node.js 引入傳統技術棧的具體實踐。
淘寶網線上應用的傳統軟件棧結構為 Nginx + Velocity + Java,即:

在這個體系中,Nginx 將請求轉發給 Java 應用,后者處理完事務,再將數據用 Velocity 模板渲染成最終的頁面。
引入 Node.js 之后,我們勢必要面臨以下幾個問題:
1. 技術棧的拓撲結構該如何設計,部署方式該如何選擇,才算是科學合理?
2. 項目完成后,該如何切分流量,對運維來說才算是方便快捷?
3. 遇到線上的問題,如何最快地解除險情,避免更大的損失?
4. 如何確保應用的健康情況,在負載均衡調度的層面加以管理?
# 承
## 系統拓撲
按照我們在[前后端分離的思考與實踐(二)- 基于前后端分離的模版探索](http://ued.taobao.org/blog/2014/04/xtpl/)一文中的思路,Velocity 需要被 Node.js 取代,從而讓這個結構變成:

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

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

淘寶網收藏夾是一個擁有千萬級日均 PV 的應用,對穩定性的要求性極高(事實上任何產品的線上不穩定都是不能接受的)。如果采用同集群部署方案,只需要一次文件分發,兩次應用重啟即可完成發布,萬一需要回滾,也只需要操作一次基線包。性能上來說,同集群部署也有一些理論優勢(雖然內網的交換機帶寬與延時都是非常樂觀的)。至于一對多或者多對一的關系,理論上可能做到服務器更加充分的利用,但相比穩定性上的要求,這一點并不那么急迫需要去解決。所以在收藏夾的改造中,我們選擇了同集群部署方案。
## 灰度方式
為了保證最大程度的穩定,這次改造并沒有直接將 Velocity 代碼完全去掉。應用集群中有將近 100 臺服務器,我們以服務器為粒度,逐漸引入流量。也就是說,雖然所有的服務器上都跑著 Java + Node.js 的進程,但 Nginx 上有沒有相應的轉發規則,決定了獲取這臺服務器上請求寶貝收藏的請求是否會經過 Node.js 來處理。其中 Nginx 的配置為:
~~~
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 發起一次?`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。相應的代碼為:
~~~
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 會使用?`HTTP Agent`?這個類來創建 HTTP 連接,這個類實現了 socket 連接池,每個主機+端口對的連接數默認上限是 5。同時?`HTTP Agent`?類發起的請求中默認帶上了?`Connection: Keep-Alive`,導致已返回的連接沒有及時釋放,后面發起的請求只能排隊。
最后的解決辦法有三種:
* 禁用?`HTTP Agent`,即在在調用?`get`?方法時額外添加參數?`agent: false`,最后的代碼為:
~~~
var http = require('http');
app.get('/status.taobao', function(req, res) {
http.get({
host: '127.1',
port: 7001,
agent: false,
path: '/status.taobao'
}, function(res) {
res.send(res.statusCode);
}).on('error', function(err) {
logger.error(err);
res.send(404);
});
});
~~~
* 設置?`http`?對象的全局 socket 數量上限:
~~~
http.globalAgent.maxSockets = 1000;
~~~
* 在請求返回的時候及時主動斷開連接:
~~~
http.get(options, function(res) {
}).on("socket", function (socket) {
socket.emit("agentRemove"); // 監聽 socket 事件,在回調中派發 agentRemove 事件
});
~~~
實踐上我們選擇第一種方法。這么調整之后,健康檢查就沒有再發現其它問題了。
# 合
Node.js 與傳統業務場景結合的實踐才剛剛起步,仍然有大量值得深入挖掘的優化點。比比如,讓 Java 應用徹底中心化后,是否可以考分集群部署,以提高服務器利用率。或者,發布與回滾的方式是否能更加靈活可控。等等細節,都值得再進一步研究。
- 開始
- 微信小程序
- 獲取用戶信息
- 記錄
- HTML
- HTML5
- 文檔根節點
- 你真的了解script標簽嗎?
- 文檔結構
- 已經落后的技術
- form表單
- html實體
- CSS
- css優先級 & 設計模式
- 如何編寫高效的 CSS 選擇符
- 筆記
- 小計
- flex布局
- 細節體驗
- Flex
- Grid
- tailwindcss
- JavaScript
- javascript物語
- js函數定義
- js中的數組對象
- js的json解析
- js中數組的操作
- js事件冒泡
- js中的判斷
- js語句聲明會提前
- cookie操作
- 關于javascript你要知道的
- 關于innerHTML的試驗
- js引擎與GUI引擎是互斥的
- 如何安全的修改對象
- 當渲染引擎遇上強迫癥
- 不要使用連相等
- 修改數組-對象
- 算法-函數
- 事件探析
- 事件循環
- js事件循環中的上下文和作用域的經典問題
- Promise
- 最佳實踐
- 頁面遮罩加載效果
- 網站靜態文件之思考
- 圖片加載問題
- 路由及轉場解決方案
- web app
- 寫一個頁面路由轉場的管理工具
- 談編程
- 技術/思想的斗爭
- 前端技術選型分析
- 我想放點html模板代碼
- 開發自適應網頁
- 后臺前端項目的開發
- 網站PC版和移動版的模板方案
- 前后端分離
- 淘寶前后端分離
- 前后端分離的思考與實踐(一)
- 前后端分離的思考與實踐(二)
- 前后端分離的思考與實踐(三)
- 前后端分離的思考與實踐(四)
- 前后端分離的思考與實踐(五)
- 前后端分離的思考與實踐(六)
- 動畫
- 開發小技巧
- Axios
- 屏幕適配
- 理論基礎
- 思考
- flexible.js原理
- 實驗
- rem的坑,為什么要設置成百分比,為什么又是62.5%
- 為什么以一個標準適配的,其它寬度也能同等適配
- 自適應、響應式、彈性布局、屏幕適配
- 適配:都用百分比?
- 番外篇
- 給你看看0.5px長什么樣?
- 用事實證明viewport scale縮放不會改變rem元素的大小
- 為什么PC端頁面縮放不會影響rem元素
- 究竟以哪個為設備獨立像素
- PC到移動端初試
- 深入理解px
- 響應式之柵格系統
- 深入理解px(二)
- 一篇搞定移動端適配
- flex版柵格布局
- 其他
- 瀏覽器加載初探
- 警惕你的開發工具
- JS模塊化
- webpack
- 打包原理
- 異步加載
- gulp
- 命名規范
- 接口開發
- sea.js學習
- require.js學習
- react學習
- react筆記
- vue學習
- vue3
- 工具、技巧
- 臨時筆記
- 怎么維護好開源項目
- 待辦
- 對前端MVV*C框架的思考
- jquery問題
- 臨時
- 好文
- 節流防抖