1. 程式人生 > >前後端分離的思考與實踐(2)

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

前後端分離模式下的安全解決方案

前言

在前後端分離的開發模式中,從開發的角色和職能上來講,一個最明顯的變化就是:以往傳統中,只負責瀏覽器環境中開發的前端同學,需要涉獵到服務端層面,編寫服務端程式碼。而擺在面前的一個基礎性問題就是如何保障Web安全?

  本文就在前後端分離模式的架構下,針對前端在Web開發中,所遇到的安全問題以及應對措施和注意事項,並提出解決方案。

跨站指令碼攻擊(XSS)的防禦

問題及解決思路

  跨站指令碼攻擊(XSS,Cross-site scripting)是最常見和基本的攻擊Web網站的方法。攻擊者可以在網頁上釋出包含攻擊性程式碼的資料,當瀏覽者看到此網頁時,特定的指令碼就會以瀏覽者使用者的身份和許可權來執行。通過XSS可以比較容易地修改使用者資料、竊取使用者資訊以及造成其它型別的攻擊,例如:CSRF攻擊。

  預防XSS攻擊的基本方法是:確保任何被輸出到HTML頁面中的資料以HTML的方式進行轉義(HTML escape)。例如下面的模板程式碼:

<textarea name="description">$description</textarea>
  這段程式碼中的$description為模板的變數(不同模板中定義的變數語法不同,這裡只是示意一下),由使用者提交的資料,那麼攻擊者可以輸入一段包含”JavaScript”的程式碼,使得上述模板語句的結果變成如下的結果:
<textarea name="description">
</textarea><script>alert('hello')'</script>
</textarea>
  上述程式碼,在瀏覽器中渲染,將會執行JavaScript程式碼並在螢幕上alert hello。當然這個程式碼是無害的,但攻擊者完全可以建立一個JavaScript來修改使用者資料或者竊取cookie資料。

  解決方法很簡單,就是將$description的值進行html escape,轉義後的輸出程式碼如下:

  1. <textarea name="description">
  2. &lt;/textarea&gt;&lt;script&gt;alert(&quot;hello!&quot;)&lt;/script&gt;
  3. </textarea>

  以上經過轉義後的HTML程式碼是沒有任何危害的。

Midway的解決方案

轉義頁面中所有使用者輸出的資料

  對資料進行轉義有以下幾種情況和方法:

1. 使用模板內部提供的機制進行轉義

  中途島內部使用KISSY xtemplate作為模板語言。

  在xtemplate實現中,語法上使用兩個中括號( {{val}})解析模板資料, ,預設既是對資料進行HTML轉義的,所以開發者可以這樣寫模板:

<textarea name="description">{{description}}</textarea>

  在xtemplate中,如果不希望輸出的資料被轉義,需要使用三個中括號({{{val}}})。

2. 在Midway中明確的呼叫轉義函式

  開發者可以在Node.js程式或者模板中,直接呼叫Midway提供的HTML轉義方法,顯示的對資料進行轉義,如下:

  方法1:在Node.js程式中對資料進行HTML轉義

var Security= require('midway-security');
//data from server,eg {html:'</textarea>',other:""}
data.html =Security.escapeHtml(data.html);
xtpl = xtpl.render(data);
  方法2:在模板中對HTML資料進行HTML轉義
<textarea name="description">Security.escapeHtml({{{description}}})</textarea>

  注意:只有當模板內部沒有對資料進行轉義的時候才使用Security.escapeHtml進行轉義。 否則,模板內部和程式會兩次轉義疊加,導致不符合預期的輸出。

  推薦:如果使用xtemplate,建議直接使用模板內建的{{}}進行轉義; 如果使用其他模板,建議使用Security.escapeHtml進行轉義。

過濾頁面中使用者輸出的富文字

  你可能會想到:“其實我就是想輸出富文字,比如一些留言板、論壇給使用者提供一些簡單的字型大小、顏色、背景等功能,那麼我該如何處理這樣的富文字來防止XSS呢?”

1. 使用Midway中Security提供的richText函式

  Midway中提供了richText方法,專門用來過濾富文字,防止XSS、釣魚、cookie竊取等漏洞。

  有一個留言板,模板程式碼可能如下:

<div class="message-board">
    {{{message}}}
</div>
  因為message是使用者的輸入資料,其留言板的內容,包含了富文字資訊,所以這裡在xtemplate中,使用了三個大括號,預設不進行HTML轉義;那麼使用者輸入的資料假如如下:
<script src="http://eval.com/eval.js"></script><span style="color:red;font-size:20px;position:fixed;">我在留言中</span>

  上述的富文字資料如果直接輸出到頁面中,必然會導致eval.com站點的js注入到當前頁面中,造成了XSS攻擊。為了防止這個漏洞,我們只要在模板或者程式中,呼叫Security.richText方法,處理使用者輸入的富文字。

  呼叫方法與escapeHtml類似,有如下兩種方式。

  方法1: 直接在Node.js程式中呼叫

message =Security.richText(message);
var html = xtpl.render(message)
  方法2: 在模板中呼叫
<div class="message-board">
    Security.richText({{{message}}})
</div>
  通過呼叫Security的richText方法後,最終的輸出如下:
<div class="message-board">
    <span style="color:red;font-size:20px;">我在留言中</span>
</div>

  可以看出,首先:會造成XSS攻擊的script標籤被直接過濾掉;同時style標籤中CSS屬性position:fixed;樣式也被過濾了。最終輸出了無害的HTML富文字

瞭解其他可能導致XSS攻擊的途徑

  除了在頁面的模板中可能存在XSS攻擊之外,在Web應用中還有其他幾個途徑也可能會有風險。

1. 出錯頁面的漏洞
404 NotFound
Page /page/not/found does not exsit

  很顯然:攻擊者可以利用這個頁面,構造一個類似這樣的連線,http://localhost/%3Cscript%3Ealert%28%27hello%27%29%3C%2Fscript%3E,並引誘受害者點選 ;假如出錯頁面未對輸出變數進行轉義的話,那麼連線中隱藏的<script>alert('hello')</script> 將會被執行。

  在express中,傳送一個404頁面的方法如下:

res.send(404,'Sorry,we don\'t find that!')

  這裡就需要開發者注意錯誤頁面(404或者其他錯誤狀態)的處理方式。如果錯誤資訊的返回內容帶有路徑資訊(其實更準確的講,是使用者輸入資訊),就一定要進行escapeHtml了。

  後續,錯誤處理的安全機制,會在Midway框架層面中完成。

Midway解決方案的補充說明

其他模板引擎

  Midway預設支援xtemplate模板,但將來也有可能支援其他模板:如jade、mustache、ejs等。目前在主流模板中,都提供了預設轉義和不轉義的輸出變數寫法,需要開發者特別留意其安全性。

關於escape的其他支援

  除了對頁面中輸出的普通資料和富文字資料,一些場景中也還包含其他可能需要轉義的情況,Midway提供瞭如下幾個常用的轉義方法,供開發者使用:

  • escapeHtml 過濾指定的HTML中的字元,防XSS漏洞
  • jsEncode 對輸入的String進行JavaScript 轉義,對中文進行unicode轉義,單引號,雙引號轉義
  • escapeJson 不破壞JSON結構的escape函式,只對json結構中name和vaule做escapeHtml處理
  • escapeJsonForJsVar 可以理解就是jsEncode+escapeJson

  例子如下:

    var jsonText ="{\"<script>\":\"<script>\"}";
    console.log(SecurityUtil.escapeJson(jsonText));// {"<script>":"<script>"}
    var jsonText ="{\"你好\":\"<script>\"}";
    console.log(SecurityUtil.escapeJsonForJsVar(jsonText));//{\"\u4f60\u597d\":\"<script>\"}
    var str ="alert(\"你好\")";
    console.log(SecurityUtil.jsEncode(str));// alert(\"\u4f60\u597d\")

跨站請求偽造攻擊(CSRF)的預防

問題及解決思路

  名詞解釋: 表單:泛指瀏覽器端用於客戶端提交資料的形式;包括a標籤、ajax提交資料、form表單提交資料等,而非對等於HTML中的form標籤。

  跨站請求偽造(CSRF,Cross-site request forgery)是另一種常見的攻擊。攻擊者通過各種方法偽造一個請求,模仿使用者提交表單的行為,從而達到修改使用者的資料或執行特定任務的目的。

  為了假冒使用者的身份,CSRF攻擊常常和XSS攻擊配合起來做,但也可以通過其它手段:例如誘使使用者點選一個包含攻擊的連結。

  解決CSRF攻擊的思路分如下兩個步驟

  1. 增加攻擊的難度。GET請求是很容易建立的,使用者點選一個連結就可以發起GET型別的請求,而POST請求相對比較難,攻擊者往往需要藉助JavaScript才能實現;因此,確保form表單或者服務端介面只接受POST型別的提交請求,可以增加系統的安全性。
  2. 對請求進行認證,確保該請求確實是使用者本人填寫表單或者發起請求並提交的,而不是第三者偽造的。

  一個正常使用者修改網站資訊的過程如下:

  • 使用者請求修改資訊(1) -> 網站顯示使用者修改資訊的表單(2) -> 使用者修改資訊並提交(3) -> 網站接受使用者修改的資料並儲存(4)

  而一個CSRF攻擊則不會走這條路線,而是直接偽造第2步使用者提交資訊:

  • 直接跳到第2步(1) -> 偽造要修改的資訊並提交(2) -> 網站接受攻擊者修改引數資料並儲存(3)

  只要能夠區分這兩種情況,就能夠預防CSRF攻擊。那麼如何區分呢? 就是對第2步所提交的資訊進行驗證,確保資料來源自第一步的表單。具體的驗證過程如下:

  • 使用者請求修改資訊(1) -> 網站顯示用於修改資訊的空白表單,表單中包含特殊的token同時把token儲存在session中(2) -> 使用者修改資訊並提交,同時發回token資訊到服務端(3) -> 網站比對使用者發回的token和session中的token,應該一致,則接受使用者修改的資料,並儲存

  這樣,如果攻擊者偽造要修改的資訊並提交,是沒辦法直接訪問到session的,所以也沒辦法拿到實際的token值;請求傳送到服務端,服務端進行token校驗的時候,發現不一致,則直接拒絕此次請求。

Midway解決方案

禁用GET提交表單

  如果服務端不接受GET方式提交的表單資料,那麼將會給攻擊者帶來非常大的難度;因為在頁面上構造一個a標籤href屬性或者img標籤src屬性來構造一個請求是非常容易的,但是如果要POST提交,就必須要通過指令碼才可以實現。

用CSRF token驗證請求

  因為Midway不涉及到淘寶分散式session及token校驗這一層面邏輯,所以在Midway框架中,只將token在server和客戶端之間進行轉發,本身不做實際的校驗工作。流程如下:


圖1

其他安全問題

  關於常見的Web安全問題,還有如下幾種,這裡只做一些簡介,後續會持續繼承到Midway framework中。

  • HTTP Headers安全
    • CRLF Injection 攻擊者想辦法在響應頭中注入兩個CRLF特殊字元,導致響應資料格式異常,從而注入script等
    • 拒絕訪問攻擊 每個請求因為都會預設帶上cookie,而伺服器一般都會限制cookie的大小,這就導致了,如果使用者客戶端cookie被設定成了超過某個閥值,那麼使用者就再也無法訪問網站了
    • cookie防竊取 一般cookie竊取都是通過JavaScript(XSS漏洞)獲取到的,所以儘量將cookie設定成http only,並且加上cookie過期時間

  關於cookie的安全問題,之前WebX已經有較好的解決方案;此次Midway不負責cookie的設定和校驗等工作,只負責轉發到WebX層面進行check

關於Node.js

  XSS等注入性漏洞是所有漏洞中最容易被忽略,佔網際網路總攻擊的70%以上;開發者編寫Node.js程式碼時,要時刻提醒自己,永遠不要相信使用者的輸入。

  比如下面幾個例子。

  • var mod = fs.readFileSync('path'); 如果path來源於使用者輸入,那麼假設使用者輸入/etc/password,則會讀取到不應該讀取的內容,造成密碼洩漏風險
  • var result = eval(jsonVal); 一定要確保jsonVal是json,而不是使用者的輸入
  • …… 其他可能包含使用者輸入的地方,一定要確認使用者的輸入是我們期望的值

總結

  前後端分離模式下,可以讓傳統的前端開發人員開始編寫後端程式碼,雖然從架構上講,只負責模板這一層,但也會接觸大量的後端程式碼;所以安全對於前端來說,這是一個不小的挑戰。

基於前後端分離的多終端適配

前言

  近年來各站點基於 Web 的多終端適配進行得如火如荼,行業間也發展出依賴各種技術的解決方案。有如基於瀏覽器原生 CSS3 Media Query 的響應式設計、基於雲端智慧重排的「雲適配」方案等。本文則主要探討在前後端分離基礎下的多終端適配方案。

關於前後端分離

關於前後端分離的方案,在《前後端分離的思考與實踐(一)》中有非常清晰的解釋。我們在服務端介面和瀏覽器之間引入 NodeJS 作為渲染層,因為 NodeJS 層徹底與資料抽離,同時無需關心大量的業務邏輯,所以十分適合在這一層進行多終端的適配工作。

UA 探測

  進行多終端適配首先要解決的是 UA 探測問題,對於一個過來的請求,我們需要知道這個裝置的型別才能針對對它輸出對應的內容。現在市面上已經有非常成熟的相容大量裝置的 User Agent 特徵庫和探測工具,這裡有 Mozilla 整理的一個列表。其中,既有執行在瀏覽器端的,也有執行在服務端程式碼層的,甚至有些工具提供了 Nginx/Apache 的模組,負責解析每個請求的 UA 資訊。

  實際上我們推薦最後一種方式。基於前後端分離的方案決定了 UA 探測只能執行在伺服器端,但如果把探測的程式碼和特徵庫耦合在業務程式碼裡並不是一個足夠友好的方案。我們把這個行為再往前挪,掛在 Nginx/Apache 上,它們負責解析每個請求的 UA 資訊,再通過如 HTTP Header 的方式傳遞給業務程式碼。

  這樣做有幾點好處:

  1. 我們的程式碼裡面無需再去關注 UA 如何解析,直接從上層取出解析後的資訊即可。
  2. 如果在同一臺伺服器上有多個應用,則能夠共同使用同一個 Nginx 解析後的 UA 資訊,節省了不同應用間的解析損耗。


圖2  來自天貓分享的基於Nginx的UA探測方案

  值得一提的是,選用UA探測工具時必須要考慮特徵庫的可維護性,因為市面上新增的裝置型別越來越多,每個裝置都會有獨立的 User Agent,所以該特徵庫必須提供良好的更新和維護策略,以適應不斷變化的裝置。

建立在 MVC 模式中的幾種適配方案

  取得 UA 資訊後,我們就要考慮如果根據指定的 UA 進行終端適配了。即使在 NodeJS 層,雖然沒有了大部分的業務邏輯,但我們依然把內部區分為 Model / Controller / View 三個模型。


圖3

  我們先利用上面的圖,去解析一些已有的多終端適配方案。

建立在 Controller 上的適配方案


圖4

  這種方案應該是最簡單粗暴的處理方法。通過路由(Router)將相同的 URL 統一傳遞到同一個控制層(Controller)。控制層再通過 UA 資訊將資料和模型(Model)邏輯派發到對應的展現(View)進行渲染,渲染層則按預先的約定提供了適配幾個終端的模板。

  這種方案的好處是,保持了資料和控制層的統一性,業務邏輯只需處理一次遍可以應用在所有終端上。但這種場景只適合如展示型頁面等低互動型的應用,一旦業務比較複雜,各個終端的 Controller 可能有各自的處理邏輯,如果還是共用一個 Controller ,會導致 Controller 非常的臃腫而且難以維護,這無疑是一個錯誤的選擇。

建立在 Router 上的適配方案

  為了解決上面遇到的問題,我們可以在 Router 上就將裝置區分,針對不同的終端分發到不同的 Controller 上:


圖5

  這也是最常見的方案之一,大多表現在針對不同終端使用各自獨立的一套應用。如 PC 淘寶首頁和 WAP 版的淘寶首頁,不同裝置訪問 www.taobao.com ,伺服器會通過 Router 的控制,重定向到 WAP 版的淘寶首頁或者 PC 版的淘寶首頁,它們各自是完全獨立的兩套應用。

  但這種方案無疑帶來了資料和部分邏輯無法共用的問題,各種終端之間無法分享同一份資料和業務邏輯,產生大量重複性工作,效率低下。

  為了緩解這個問題,有人提出了優化後的方案:依然是在同一套應用裡面,各個資料來源抽象成各個 Model,提供給不同終端的 Controller 組合使用:


圖6

  這個方案解決了前面資料無法共用的問題。在 Controller 上各個終端還是相互獨立,但能共同使用同一批資料來源,至少在資料上無需再針對終端型別開發獨立的介面了。

以上兩種基於 Router 的方案,由於 Controller 的獨立,各個終端可以為自己的頁面實現不同的互動邏輯,保證了各終端自身足夠的靈活度,這也是為什麼大部分應用採用這種方案的主要原因。

建立在 View 層的適配方案

  這是淘寶下單頁面使用的方案,不過區別是下單頁將整體的渲染層放在了瀏覽器端,而不是 NodeJS 層。不過無論是瀏覽器還是 NodeJS,整體設計思路還是一致的:


圖7

  在這個方案裡面,Router、Controller 和 Model 都無需關注裝置資訊,終端型別的判斷完全交給展現層來處理。圖中主要的模組是「View Factory」,Model 和 Controller 將資料和渲染邏輯傳遞過來之後,通過 View Factory 根據裝置資訊和其它狀態(不僅僅是 UA 資訊、也可以是網路環境、使用者地區等等)從一堆預設好的元件(View Component)中抓取特定的元件,再組合成最終的頁面。

  這種方案有幾個優勢:

  1. 上層無需關注裝置資訊(UA),多終端的視訊還是交由和最終展現最大關係的 View 層來處理;
  2. 不僅僅是多終端適配,除了 UA 資訊,各個 View Component 還可以根據使用者狀態決定自身輸出何種模版,如低網速下預設隱藏圖片、指定地區輸出活動 Banner。
  3. 每個 View Component 的不同模版間可以自行決定是否使用同一份資料、業務邏輯,提供十分靈活的實現方式。

  但明顯的是,這個方案也是最複雜的,尤其是要考慮一些富互動的應用場景時,Router 和 Controller 也許無法保持這麼純粹。特別對於一些整體性比較強的業務,本身無法被拆分成元件,這種方案也許並不適用;而且對於一些簡單的業務,使用這種架構可能不是最佳的選擇。

總結

  以上幾種方案,都各自體現在 MVC 模型中的一個或多個部分,在業務上如果一個方案不滿足需求,更可以採取多個方案同時採用的方式。或是可以理解為,業務上的複雜度和互動屬性決定了該產品更適合採用哪種多終端適配方案。

  對比基於瀏覽器的響應式設計方案,因為絕大部分終端探測和渲染邏輯遷移到了服務端,所以在 NodeJS 層進行適配無疑帶來了更好的效能和使用者體驗;另外,相對於一些所謂的「雲適配」方案帶來的轉換質量問題,在基於前後端分離的「定製式」方案中也不會存在。前後端分離的適配方案在這些方面有著天然優勢。

  最後,為了適應更靈活的強大的適配需求,基於前後端分離的適配方案將會面臨更多挑戰!


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

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

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

圖8

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

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

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

系統拓撲

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


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

圖10

部署方案

  上面的結構看起來沒什麼問題了,但其實新問題還等在前面。在傳統結構中,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 = <span class="hljs-number">1000</span>;
  • 在請求返回的時候及時主動斷開連線:
http.get(options, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(res)</span> {</span>
    }).on(<span class="hljs-string">"socket"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(socket)</span> {</span>
    socket.emit(<span class="hljs-string">"agentRemove"</span>); <span class="hljs-comment">// 監聽 socket 事件,在回撥中派發 agentRemove 事件</span>
});

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

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


本文轉自:http://ued.taobao.org/blog/2014/05/midway-deploy/