基於阿里雲的 Node.js 穩定性實踐

前言

若是你看過 2018 Node.js 的用戶報告,你會發現 Node.js 的使用有了進一步的增加,同時也出現了一些新的趨勢。html

  • Node.js 的開發者更多的開始使用容器並積極的擁抱 Serverless
  • Node.js 愈來愈多的開始服務於企業開發
  • 半數以上的 Node.js 應用都使用遠端服務
  • 前端開發者們開始愈來愈多的關心和參與到後端和全棧中去

能夠看到愈來愈多的前端開發者們具有了全棧的能力,更多的核心應用開始基於 Node.js 開發,而其中,保障應用的穩定性是每個開發者的「頭等大事」。前端

穩定性是什麼?通常來講,指的是應用持續提供可用服務的能力,一旦應用頻繁不可用或出現故障沒法及時恢復,對用戶的使用體驗都是巨大的傷害,甚至會形成不少更嚴重的後果。穩定性保障不只僅是開發階段的事情,它應該是貫穿應用的開發、測試、上線、監控等,覆蓋整個 DevOps 生命週期的事情。node

自己阿里雲提供了豐富的產品和服務來支持整個 DevOps。nginx

包括 Code 代碼託管PTS 性能測試SLS 日誌服務雲效 等等。git

本文也將圍繞整個 DevOps 生命週期,來介紹基於阿里雲的 Node.js 穩定性保障的實踐。程序員

應用開發

穩定性的保障從應用開發階段就已經開始了,這部分也是相關資料文章最多的,相信有追求的開發者都會關注而且已經應用和實踐。github

異常捕獲和處理

應用運行過程當中不免會有異常發生,再大神的程序員也不敢保證本身寫的代碼不出問題。其實出現異常不可怕,可怕的是異常沒有捕獲,進而引發應用進程 crash,致使應用不可用。數據庫

正常來講,捕獲異常有一下幾種方式:後端

  • try/catchapi

    try/catch 是捕獲異常的經常使用方式,能夠幫助咱們可控的捕獲錯誤,可是 try/catch 沒法捕獲異步異常。

    try {
        setTimeout(() => {
            throw new Error('error');
        }, 0);
    } catch(err) {
        // can't catch it
        console.log(err);
    }

    上面的異步異常使用 try/catch 是沒法捕獲的。捕獲異步平常咱們可使用一下的方式。

  • 異步異常

    • callback 異步回調

      經過異步回調來處理異步錯誤多是目前最普遍的方案。

      function demo(callback) {
      setTimeout(() => {
          callback(new Error('error'), null);
      }, 0);
      }
      demo((err, res) => {
      if (err) console.log(err);
      });

固然,callback 方式存在一直被人詬病的嵌套問題

    • 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 進程就不會異常退出。

      process.on('uncaughtException', function(err) {
          console.error(err);
      });

      可是這時異常的上下文會丟失(respond 對象),沒法給用戶友好的返回。並且因爲uncaughtException 事件發生後,會丟失當前環境的堆棧,可能致使 Node 不能正常進行內存回收,從而致使內存泄露。所以,使用 uncaughtException 的正確作法通常是,當 uncaughtException 發生時,記錄詳細的日誌,而後結束進程,經過日誌和報警來及時的定位和排查問題。

    • 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 的同窗,經過後會將開發分支合併到部署分支。固然這種方式依賴流程規範的約定,沒法進行強制的卡口。

    增長測試卡點

    前文講過,咱們須要爲應用實現單元/功能測試,那如何保證應用部署發佈前必定經過了單元/功能測試呢?咱們能夠在雲效的流程中增長測試卡點,來保證咱們編寫的測試用例經過後,當前部署分支纔可進行發佈,經過雲效的自動化測試卡口保障持續交付質量。

    首先咱們須要新建一個測試任務,在 [雲效的測試服務]https://testing.rdc.aliyun.com/))中選擇「單元測試」。

    將建立的測試任務和流水線關聯,做爲持續集成交付的測試卡口。每次集成交付,都會運行測試任務,同時保證測試結果達到紅線要求,不然流水線運行失敗。

    更多操做步驟能夠參考幫助文檔

    性能測試

    應用在發佈前以及上線後周期性的,都須要作性能測試,一方面讓咱們對應用的吞吐內心有數,另外一方面保證長時間運行的穩定,畢竟有些問題多是運行不少次纔可能出現的,好比 OOM 等。阿里雲提供了方便的性能測試產品:PTS

    PTS 支持構建串行、並行的構建你的壓測場景,而且支持併發和 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 應用提供性能監控、安全提醒、故障排查、性能優化等服務的總體性解決方案。

    Node.js 性能平臺提供了豐富的度量指標,包括系統、進程的內存、CPU、QPS 等等。

    同時,其還爲咱們提供的故障排查的能力,好比熱點函數分析、內存泄露分析等。你能夠參考 Node 應用內存泄漏分析方法論與實戰來學習使用 Node.js 性能平臺發現、定位解決內存泄露問題。

    錯誤日誌和鏈路

    通常來講,咱們須要採集如下幾類日誌:

    • trace:請求鏈路的監控日誌。當出現錯誤時,能夠根據 traceId 快讀的定位到產生問題的那個請求鏈路,還原上下文。尤爲是咱們的應用若是依賴了其餘二方/三方系統,鏈路比較長時,能夠明確的知道調用依賴系統時的入參和返回,快讀定位出現問題的環節,減小扯皮和定位還原問題的時間。
    • error:錯誤日誌。包括應用自己和業務邏輯的錯誤。
    • metric:CPU、內存等機器指標
    • nginx:若是你的應用用了 nginx,nginx 的錯誤日誌的採集也是很關鍵的。nginx 的錯誤日誌多是最容易被忽略的,常常見到這樣的場景,應用沒有異常,可是訪問就是掛的,開發吭哧吭哧排查半天,終於定位到 nginx 有錯誤拋出。

    其中,trace 鏈路日誌是很重要可是容易被忽略的日誌,鏈路的重要性不言而喻,能夠幫助咱們分析上下游依賴、進行節點分析和故障排查,尤爲是依賴其餘二方/三方系統時,trace 鏈路日誌十分重要,可是也是須要花很是大的精力去作,業界的 newRelic,oneAPM 都有着很是明顯的鏈路視圖。

    通常來講咱們採用 Pandora + SLS 日誌服務 + Node.js 性能平臺 來進行日誌收集。

    其中 Pandora 經過攔截 httpServer 和 httpClient,在對咱們系統業務沒有侵入性的同時幫助咱們收集 trace 鏈路日誌,詳細的配置,你能夠參考 Pandora 鏈路追蹤及監控

    Node.js 性能平臺會幫助咱們收集 error 日誌。

    配合 SLS 日誌服務,能夠幫助咱們無死角的採集咱們須要的任何日誌信息。SLS 詳細的配置能夠參考其幫助文檔

    報警

    應用出現異常後,須要有及時的報警機制來提醒咱們,以便快速響應和處理。

    監控項與報警指標

    通常來講,須要的監控項及報警指標爲:

    • 日誌監控

      • Nginx 錯誤日誌
      • 應用 Error 日誌
      • Trace 鏈路日誌
    • 日誌報警

      • 每分鐘錯誤日誌數量 > 流量 * SLA 等級
    • 機器指標

      • CPU > 70%
      • 內存泄露:@heap_used / @heap_limit > 0.7
      • Load > CPU 核數
    • 流量監控

      • 周同比監控:同比降低 > SLA 的承諾
      • 流量預警,接近 QPS 峯值

    其中 SLA 爲服務等級,用百分比的服務可用性來來定義服務質量。

    報警配置

    通常來講咱們使用 雲監控 CloudMonitor + SLS 日誌服務 + Node.js 性能平臺的報警配置便可。

    其中雲監控 CloudMonitor的報警主要針對上文提到的健康檢查。你能夠參考雲監控報警服務來配置報警功能。

    對於 SLS,咱們能夠對錯誤數量進行報警,或者根據同比環比來進行報警。好比咱們能夠新建兩個快速查詢,針對咱們應用 error 和 nginx error 日誌。

    這裏的查詢語句爲 * | select count(*) as sum。而後將快速查詢另存爲告警,根據須要配置告警規則,觸發告警時,能夠選擇經過釘釘機器人進行通知。詳細的配置,能夠參考 SLS 官方文檔設置告警

    對於服務器指標告警,好比 CPU、內存等。咱們能夠利用 Node.js 性能平臺 配置監控。

    能夠看到,上面配置的告警規則是:堆上線 80%、load1 和 load5 <= 三、cpu 上線 80%。這裏須要編寫監控項的表達式,能夠參考如何進行監控項表達式的編寫

    最後

    其實穩定性的保障還有不少工做和措施能夠作,好比咱們的部署能夠採起多集羣、多 Region 的部署,這樣能夠保證當某個集羣或者 Region 出現故障,不會形成更大範圍的問題,保證故障範圍可控。同時咱們還能夠採起灰度發佈的方式,在不斷驗證新上線功能的狀況下,平滑的過渡發佈上線,保證應用總體穩定性等等。

    最後的最後,穩定性保障是應用整個生命週期內的事情,是每一個開發者的責任和義務。


    本文做者:冬萌

    閱讀原文

    本文爲雲棲社區原創內容,未經容許不得轉載。

    相關文章
    相關標籤/搜索