前言
如果你看過
2018 Node.js 的使用者報告,你會發現 Node.js 的使用有了進一步的增長,同時也出現了一些新的趨勢。

- Node.js 的開發者更多的開始使用容器并積極的擁抱 Serverless
- Node.js 越來越多的開始服務于企業開發
- 半數以上的 Node.js 應用都使用遠端服務
- 前端開發者們開始越來越多的關心和參與到後端和全棧中去
可以看到越來越多的前端開發者們具備了全棧的能力,更多的核心應用開始基于 Node.js 開發,而其中,保障應用的穩定性是每一個開發者的“頭等大事”。
穩定性是什麼?一般來說,指的是應用持續提供可用服務的能力,一旦應用頻繁不可用或出現故障無法及時恢複,對使用者的使用體驗都是巨大的傷害,甚至會造成很多更嚴重的後果。穩定性保障不僅僅是開發階段的事情,它應該是貫穿應用的開發、測試、上線、監控等,覆寫整個 DevOps 生命周期的事情。
本身阿裡雲提供了豐富的産品和服務來支援整個 DevOps。
包括
Code 代碼托管、
PTS 性能測試 SLS 日志服務 雲效等等。
本文也将圍繞整個 DevOps 生命周期,來介紹基于阿裡雲的 Node.js 穩定性保障的實踐。
應用開發
穩定性的保障從應用開發階段就已經開始了,這部分也是相關資料文章最多的,相信有追求的開發者都會關注并且已經應用和實踐。
異常捕獲和處理
應用運作過程中難免會有異常發生,再大神的程式員也不敢保證自己寫的代碼不出問題。其實出現異常不可怕,可怕的是異常沒有捕獲,進而引起應用程序 crash,導緻應用不可用。
正常來說,捕獲異常有一下幾種方式:
-
try/catch
try/catch 是捕獲異常的常用方式,可以幫助我們可控的捕獲錯誤,但是 try/catch 無法捕獲異步異常。
上面的異步異常使用 try/catch 是無法捕獲的。捕獲異步日常我們可以使用一下的方式。try { setTimeout(() => { throw new Error('error'); }, 0); } catch(err) { // can't catch it console.log(err); }
- 異步異常
-
callback 異步回調
通過異步回調來處理異步錯誤可能是目前最廣泛的方案。
當然,callback 方式存在一直被人诟病的嵌套問題function demo(callback) { setTimeout(() => { callback(new Error('error'), null); }, 0); } demo((err, res) => { if (err) console.log(err); });
-
promise
使用 promise 可以通過 reject 抛出錯誤,通過 catch 捕獲錯誤
new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('error')); }, 0); }) .catch(err => { console.log(err); });
-
generator
使用 generator 可以讓我們使用同步的代碼寫法來調用異步函數,可以直接 try/catch 來捕獲異常
function* demo() { try { yield new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('error')); }, 0); }); } catch(err) { // can catch console.log(err); } } yield demo();
-
async/await
async/await 應該是目前最簡單和優雅的異步解決方案了,寫起來和同步代碼一樣直覺,可以直接使用 try/catch 來捕獲異常
const demo = async function() { try { await new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('error')); }, 0); }); } catch(err) { // can catch console.log(err); } };
-
-
uncaughtException
當異常抛出未被捕獲時,會觸發 uncaughtException 事件。隻要監聽了 uncaughtException 事件并設定了回調,Node 程序就不會異常退出。
但是這時異常的上下文會丢失(respond 對象),無法給使用者友好的傳回。而且由于uncaughtException 事件發生後,會丢失目前環境的堆棧,可能導緻 Node 不能正常進行記憶體回收,進而導緻記憶體洩露。是以,使用 uncaughtException 的正确做法一般是,當 uncaughtException 發生時,記錄詳細的日志,然後結束程序,通過日志和報警來及時的定位和排查問題。process.on('uncaughtException', function(err) { console.error(err); });
-
domain
為了彌補 try/catch、uncaughtException 的不足,Node 新增了一個 domain 子產品,可以捕獲異步異常并且不會丢失上下文。
聽起來很完美,但是該子產品目前是不穩定的(Stability: 0 - Deprecated)。同時其可能存在穩定性和記憶體洩露的問題,是以要謹慎使用。
一般來說,我們開發 Node 應用,隻需要關注我們應用邏輯異常的捕獲即可,本身我們使用的 Node 架構,比如:Egg、Midway 等都會在底層幫我們進行處理,保證一些我們不可控或者未預期的異常出現時,不會導緻應用崩潰。
雖然架構幫我們進行的兜底,但是依然需要我們針對自己的應用邏輯進行異常處理,給使用者友好的異常提示。一般出現異常時,我們需要盡可能保證:
- 對出現異常的使用者,進行友好的提示
- 不影響應用其他使用者的正常使用
- 不影響應用程序的正常運作
- 詳細的異常日志記錄和報警機制,友善快速定位、解決問題
如果你使用的是
Egg,你可以使用
onerror插件來做統一的處理。同時不建議将異常資訊直接傳回給使用者,傳回使用者的應該是更語義化更友好的資訊,而原始的錯誤堆棧和資訊等,你可以通過日志進行記錄,日志資訊越詳細越好,比如除了最基本的 name、message、stack 外,你還可以記錄目前一些關鍵的參數以及目前調用鍊路的 traceId 等,這樣的目的隻有一個,就是可以快速定位到錯誤,以及錯誤發生的上下文。具體的鍊路監控下文會講到。
強弱依賴
在設計應用架構時,重要的一步就是區分強弱依賴。強弱依賴的定義應該視對業務的影響程度而定,并不能單純的認為會導緻系統挂掉的依賴才是強依賴。盡量減少強依賴,因為強依賴意味着,一旦該強依賴出現問題,會導直接影響業務的進行。一個應用的依賴可能涉及到以下幾個部分。
- 資料
- 應用的開發基本離不開資料的讀寫,這也導緻我們的應用基本都是強依賴 DB 的,DB 一旦出現問題,那我們的應用可能就不可用了,是以我們可以通過 DB 上加一層緩存來增加一層保險,當資料更新的時候重新整理對應的緩存,這樣任何一層出現問題,都不會對應用帶來災難性後果。這裡你需要額外注意資料同步的機制和一緻性的保證,同時對于資料讀取要設定合理的逾時時間,比如讀取緩存,如果 10ms 内沒有響應就直接讀取資料庫,再有就是異常的處理,比如要保證讀取緩存時出現異常不能影響 DB 的正常讀取。
- 中間件
- 如果依賴了其他的中間件,也要考慮是否對某個中間件進行了強依賴,如果這個中間件故障了,會不會對我們的應用造成嚴重故障。
- 二方/三方系統
- 我們的應用或多或少都會依賴其他的二方或者三方系統,對我們依賴的這些系統的穩定性,我們盡量要做到心中有數,盡量不進行強依賴,如果出現異常,要做好詳細的日志記錄,快速定位出現問題的依賴方和出現問題的上下文,不然定位問題和複現問題可能就要花去你大部分時間了,同時提前做好處理方案,不要出現問題了就抓瞎了。當然如果我們依賴其他系統提供的資料,那依然可以使用緩存來加一層保障。
多程序
我們知道 JavaScript 單線程運作的,換句話說一個 Node.js 程序隻能運作在一個 CPU 上,是以無法享受到多核運算的好處。Node.js 針對這個問題提供了
Cluster子產品,可以在伺服器上同時啟動多個程序,每個程序裡都跑的是同一份源代碼,并且可以同時監聽一個端口。當然作為一個對外服務的應用來說,要考慮的東西還有很多,比如異常如何處理,程序間如何共享資源,程序間如何排程等等。如果你使用的是 Egg/Midway,這些問題架構已經幫你解決掉了。對于 Egg 來說,你可以詳細參考:
多程序模型和程序間通訊。這裡不再贅述。
單元/功能測試
單元/功能測試的重要性毋容置疑,為代碼品質提供持續性的保障,同時可以增強你修改、釋出代碼的信心。單元測試用于測試最小功能單元,比如單個方法。而針對 Node 開發的 Web 應用,我們可以直接針對接口進行功能測試,如果針對函數方法寫單元測試的話,成本有點高,而接口的功能測試基本可以覆寫 Router、Controller、Model 整條鍊路了,覆寫不到的函數邏輯再對其單獨編寫單元測試用例,這樣成本會小很多,而且達到的測試覆寫率并沒有折扣。
如果你使用了 Egg/Midway 等架構,架構本身對單元測試能力已經幫你進行了內建,你隻需要按照約定編寫用例并使用即可,可以參考
Egg 單元測試。
持續內建
有了單元/功能測試以後,下一步就需要考慮持續內建了。阿裡雲提供了
CodePipline以及
幫助你進行快速可靠的持續內建與傳遞
流程規範
開發、測試、釋出過程中的流程規範也是保障穩定性的重要一環,可以有效避免一些人為的疏忽。比如應用寫了測試用例,但是在用例沒通過的情況下釋出上線等等。是以配置一套自動化的流程規範十分有必要,阿裡雲的
提供了完整的項目管理、持續內建的能力,在上面可以完成日常開發、測試、釋出的流程。詳細的操作可以參考其
幫助文檔。這裡補充一些流程上的實踐。
CodeReview
CodeReview 十分重要,它可以及時發現一些比較明顯的代碼、邏輯問題,同時可以保證多人合作的代碼了解和維護。但是如果沒有一個流程規範和卡口,CodeReview 是很難自發堅持下去的。
CodeReview 可以分為送出前(pre-commit)和送出後(post-commit)兩種。本身就是字面意思,pre-commit 既必須通過 CodeReview 才可以送出代碼,而 post-commit 既先送出代碼,然後發起 CodeReview。相比起來,pre-commit 流程更加合理,因為 post-commit 不阻礙代碼送出變更、釋出的流程,既即使沒有 reivew 通過,依然可以送出變更并釋出。而 post-commit 相對于 pre-commit 來說會更容易實施。
而對于 post-commit,如果其 review 的結果并不影響代碼送出變更和釋出,那如何做流程卡口呢?你可以使用雲效自定義流水線,通過人工卡點的方式來保證流程。
通過人工卡點,來增加流程卡口,後續雲效也會上線 CodeReivew 功能,敬請期待。更多流水線的操作,你可以參考其
如果你覺得配置 pre-commit 過于麻煩,而 post-commit 流程上過于滞後的話,也可以采用依靠約定的折中方案,使用 Git 的 PR 功能。我們不從部署分支上進行開發,而是基于部署分支繼續檢出開發分支,開發完成需要送出部署時,送出 PR,指定給需要 review 的同學,通過後會将開發分支合并到部署分支。當然這種方式依賴流程規範的約定,無法進行強制的卡口。
增加測試卡點
前文講過,我們需要為應用實作單元/功能測試,那如何保證應用部署釋出前一定通過了單元/功能測試呢?我們可以在雲效的流程中增加測試卡點,來保證我們編寫的測試用例通過後,目前部署分支才可進行釋出,通過雲效的自動化測試卡口保障持續傳遞品質。
首先我們需要建立一個測試任務,在
雲效的測試服務中選擇“單元測試”。
将建立的測試任務和流水線關聯,作為持續內建傳遞的測試卡口。每次內建傳遞,都會運作測試任務,同時保證測試結果達到紅線要求,否則流水線運作失敗。
更多操作步驟可以參考
性能測試
應用在釋出前以及上線後周期性的,都需要做性能測試,一方面讓我們對應用的吞吐心裡有數,另一方面保證長時間運作的穩定,畢竟有些問題可能是運作很多次才可能出現的,比如 OOM 等。阿裡雲提供了友善的性能測試産品:
PTSPTS 支援建構串行、并行的建構你的
壓測場景,并且支援并發和 TPS 模式來控制你的
壓測流量,最後,PTS 還提供了豐富的
監控和壓測報告,實時監控和報告中包括但不局限于各 API 的并發、TPS、響應時間和采樣的日志,請求和響應時間還有不同的細分資料,和阿裡雲生态内的雲監控、ARMS監控無縫內建。
建立壓測場景
首先你需要對壓測進行計劃,需要明确場景,對流量進行預估,設定目标值,否則壓測毫無意義,你完全無法明确目前系統是否可以穩定的支撐你的業務場景。其次需要對各種系統預案進行摸高壓測,明确各個預案下能支援的壓力上限,以此來保證在合适的情況下可以執行對應的預案并可以達到預期效果。
詳細的建立壓測場景的步驟,可以參考
PTS 幫助文檔。一般來說,我們可以建立兩個場景,分别用來回歸測試和容量評估,回歸測試的場景,可以設定固定的并發數量,周期性的持續壓測,來暴露一些長時間運作可能的潛在問題、而容量評估場景,需要設定自動增長的方式,用來尋找系統的壓力上限。
施壓配置
對于容量評估的場景,我們可以開啟自動增長,按照固定比例進行壓測量級的遞增,并在每個量級維持固定壓測時長,以便觀察業務系統運作情況。
同時 PTS 給我們提供了更加友善的智能測試模式,幫我們探測系統的最佳壓力點、極限壓力點和破壞壓力點,幫助我們評估系統容量。更詳細的操作步驟,可以參考
PTS 容量評估性能名額
對于預估正常的并發量來說,性能測試一般通過标準為:
- 逾時率小于萬分之一
- 錯誤率小于萬分之一
- CPU 使用率小于 75%
- Load 平均每核 CPU 小于 1
- 記憶體使用率小于 80%
更多可參考
PTS 測試名額。對于壓力測試來說,一般我們把 CPU 壓到 100% 或者記憶體壓到 90% 左右,既可認為壓到了極限,如果此時你發現其他名額可能都是正常的,那麼說明你的應用可能還有很大的優化空間,可以有針對性的去檢查并進一步優化。
回歸測試
我們需要保證應用長時間持續性的穩定,而有些問題可能是運作很多次才可能出現的,比如 OOM 等。而回歸測試指的是周期性的持續壓測,通過回歸測試,來提前暴露出系統長時間運作中可能出現的潛在問題。
PTS 為我們提供的友善的定時功能,可以指定測試任務的執行日期、執行時間、循環周期和通知方式等,進而實作定時壓測。你可以參考
PTS 定時壓測來配置自己的回歸測試。
當然雲效也給我們提供了功能更為強大的
回歸測試平台,可以将線上真實流量複制并用于自動回歸測試的平台。通過它,不僅能夠實作低成本的日常自動化回歸,同時通過它提供擴充能力可以支援系統重構更新的自動回歸。比如系統重構時,複制真實線上環境流量到被測試環境進行回歸,相當于在不影響業務的情況下提前上線檢測系統潛在的問題。同時還可以将錄制的流量作為用例管理起來進行自動化回歸。
你可以參考
自動回歸服務接入使用文檔來配置功能強大的回歸測試。
監控報警
應用出現異常并不可怕,可怕的是出現問題以後而并不自知。沒有哪個系統可以保證線上不出現問題,重要的是及時發現問題并解決,不讓問題持續惡化。是以線上的監控和報警十分重要。
監控與日志
一般來說我們需要進行三個方面的監控:業務可用性、業務名額衡量、業務錯誤追蹤,而對應的方式為:健康檢查、單點度量、錯誤日志和鍊路。
健康檢查
健康檢查是用來定義一個應用目前的狀态,它需要能頻繁調用并快速傳回,而健康檢查包含着一系列的檢查項,比如:
一般來說,我們可以通過
Pandora+
雲監控 CloudMonitor來幫助我們進行健康檢查。
首先 Pandora 是阿裡内部開源出去的,提供一個通用的 Node.js 應用運作時模型和相關基礎設施。提供一個标準的 Node.js 的 DevOps 流程。其提供了一些基礎的檢查,比如磁盤檢查,端口檢查等。同時我們也可以自定義更多的檢查項。
Pandora 健康檢查來使用其提供的健康檢查能力。
Pandora 配置好後,我們可以通過雲監控對暴露出來的檢查服務進行監控。
來配置你的監控能力。
單點度量
阿裡雲提供了
Node.js 性能平台來幫助我們對 Node.js 應用進行單點度量。
是面向中大型 Node.js 應用提供性能監控、安全提醒、故障排查、性能優化等服務的整體性解決方案。
Node.js 性能平台提供了豐富的度量名額,包括系統、程序的記憶體、CPU、QPS 等等。
同時,其還為我們提供的故障排查的能力,比如熱點函數分析、記憶體洩露分析等。你可以參考
Node 應用記憶體洩漏分析方法論與實戰來學習使用 Node.js 性能平台發現、定位解決記憶體洩露問題。
錯誤日志和鍊路
一般來說,我們需要采集以下幾類日志:
- trace:請求鍊路的監控日志。當出現錯誤時,可以根據 traceId 快讀的定位到産生問題的那個請求鍊路,還原上下文。尤其是我們的應用如果依賴了其他二方/三方系統,鍊路比較長時,可以明确的知道調用依賴系統時的入參和傳回,快讀定位出現問題的環節,減少扯皮和定位還原問題的時間。
- error:錯誤日志。包括應用本身和業務邏輯的錯誤。
- metric:CPU、記憶體等機器名額
- nginx:如果你的應用用了 nginx,nginx 的錯誤日志的采集也是很關鍵的。nginx 的錯誤日志可能是最容易被忽略的,經常見到這樣的場景,應用沒有異常,但是通路就是挂的,開發吭哧吭哧排查半天,終于定位到 nginx 有錯誤抛出。
其中,trace 鍊路日志是很重要但是容易被忽略的日志,鍊路的重要性不言而喻,可以幫助我們分析上下遊依賴、進行節點分析和故障排查,尤其是依賴其他二方/三方系統時,trace 鍊路日志十分重要,但是也是需要花非常大的精力去做,業界的 newRelic,oneAPM 都有着非常明顯的鍊路視圖。
一般來說我們采用
來進行日志收集。
其中 Pandora 通過攔截 httpServer 和 httpClient,在對我們系統業務沒有侵入性的同時幫助我們收集 trace 鍊路日志,詳細的配置,你可以參考
Pandora 鍊路追蹤及監控會幫助我們收集 error 日志。
配合
,可以幫助我們無死角的采集我們需要的任何日志資訊。SLS 詳細的配置可以參考其
報警
應用出現異常後,需要有及時的報警機制來提醒我們,以便快速響應和處理。
監控項與報警名額
一般來說,需要的監控項及報警名額為:
- 日志監控
- Nginx 錯誤日志
- 應用 Error 日志
- Trace 鍊路日志
- 日志報警
- 每分鐘錯誤日志數量 > 流量 * SLA 等級
- 機器名額
- CPU > 70%
- 記憶體洩露:@heap_used / @heap_limit > 0.7
- Load > CPU 核數
- 流量監控
- 周同比監控:同比下降 > SLA 的承諾
- 流量預警,接近 QPS 峰值
其中 SLA 為服務等級,用百分比的服務可用性來來定義服務品質。
報警配置
一般來說我們使用
的報警配置即可。
其中
的報警主要針對上文提到的健康檢查。你可以參考
雲監控報警服務來配置報警功能。
對于 SLS,我們可以對錯誤數量進行報警,或者根據同比環比來進行報警。比如我們可以建立兩個快速查詢,針對我們應用 error 和 nginx error 日志。
這裡的查詢語句為
* | select count(*) as sum
。然後将快速查詢另存為告警,根據需要配置告警規則,觸發告警時,可以選擇通過釘釘機器人進行通知。詳細的配置,可以參考 SLS 官方文檔
設定告警對于伺服器名額告警,比如 CPU、記憶體等。我們可以利用
配置監控。
可以看到,上面配置的告警規則是:堆上線 80%、load1 和 load5 <= 3、cpu 上線 80%。這裡需要編寫監控項的表達式,可以參考
如何進行監控項表達式的編寫最後
其實穩定性的保障還有很多工作和措施可以做,比如我們的部署可以采取多叢集、多 Region 的部署,這樣可以保證當某個叢集或者 Region 出現故障,不會造成更大範圍的問題,保證故障範圍可控。同時我們還可以采取灰階釋出的方式,在不斷驗證新上線功能的情況下,平滑的過渡釋出上線,保證應用整體穩定性等等。
最後的最後,穩定性保障是應用整個生命周期内的事情,是每個開發者的責任和義務。