提升 Node.js 服務穩定性,需要關注哪些指標?

作爲一個前端工程師,大家日常也會維護一些 Node.js 服務,對於一個服務我們首先要關注的就是它的穩定性,可能大部分同學對服務端的很多概念不會理解的特別深刻,所以在穩定性上面也不知道去關注什麼。

上週在團隊分享了我的一些 Node.js 服務穩定性的優化實踐,後面也會慢慢分享給大家,本篇文章我先給大家介紹一些在服務端穩定性上面我會關注的一些指標。

整體分爲兩個大的方面:

  • 資源穩定性:即當前服務所處的運行環境的一些指標,一般如果資源穩定性的指標除了問題,那麼服務有可能已經有了大問題,甚至處於不可用狀態。
  • 服務運行穩定性:服務運行過程中產生的異常、日誌、延遲等等。

資源穩定性

CPU

CPU Load

CPU Load 即 CPU 的負載,表示在一段時間內 CPU 正在處理以及等待 CPU 處理的進程數之和的統計信息。CPU 完全空閒時,CPU Load 爲0,CPU 工作越飽和,CPU Load 越大。

如果 CPU 每分鐘最多處理 100 個進程,系統負荷0.2,意味着 CPU 在這1分鐘裏只處理20個進程。

下面借用下阮一峯的例子:我們把 CPU 想象成一座大橋,橋上只有一根車道,所有車輛都必須從這根車道上通過。系統負荷爲0,意味着大橋上一輛車也沒有。系統負荷爲0.5,意味着大橋一半的路段有車。

系統負荷爲 1.0,意味着大橋的所有路段都有車,也就是說大橋已經"滿"了。但是必須注意的是,直到此時大橋還是能順暢通行的。系統負荷爲 1.7,意味着車輛太多了,大橋已經被佔滿了(100%),後面等着上橋的車輛爲橋面車輛的70%

如果容器有 2個CPU 表明系統負荷可以達到2.0,此時每個CPU都達到100%的工作量。推廣開來,n個CPU的電腦,可接受的系統負荷最大爲n。多核CPU與多CPU效果類似,所以考慮系統負荷的時候,必須考慮這臺電腦有幾個CPU、每個CPU有幾個核心。

CPU Usage

CPU Usage 代表了程序對 CPU 時間片的佔用情況,也就是我們常說的 CPU 利用率,它可以反應某個採樣時間內 CPU 的使用情況,是否處於持續工作狀態,可以從 CPU 核心、佔用率百分比兩個角度來看。

正常情況下,CPU Usage 高,CPU Load 也會比較高。CPU Usage 低,CPU Load 也會比較低。也有例外情況:

  1. CPU Load 低, CPU Usage 高:如果 CPU 執行的任務數很少,則 CPU Load 會低,但是這些任務都是CPU密集型,那麼利用率就會高。
  2. CPU Load 高, CPU Usage 低:如果CPU執行的任務數很多,則 CPU Load 會高,但是在任務執行過程中 CPU 經常空閒(比如等待IO),那麼利用率就會低。

內存

內存 RSS

RSS :常駐內存集(Resident Set Size)用於表示系統有多少內存分配給當前進程,它能包括所有堆棧和堆內存,是 OOM 主要參考的指標。

內存 V8 Heap

表示 JavaScript 代碼執行佔用的內存。

一般我們可以看到 V8 Heap 區分了 UsedTotal,這裏是主要是因爲 V8 的內存回機制,在進程中有一些內存是可回收並且沒有馬上被回收的,Total - Used 實際上是指當前可以回收但沒有回收的內存。

內存 max-old-space-size

V8 允許的最大的老生代內存大小,可以簡單認爲是一個 Node.js 進程長期可維持的最大內存大小。進程的 HeapTotal 接近這個值時,進程很可能會因爲 V8 abort 而退出。

內存 External

Node.js 中的 Buffer 是基於 V8 Uint8Array 的封裝,因此在 Node.js 中使用 Buffer 時,其內存佔用量會被記錄到 External 中。

加之 external string 在 Node.js 中使用的得很少,因此我們可以認爲對一個常見的 Node.js web 應用來說,process.memoryUsage() 中 的 External 主要指的就是Buffer佔用的內存量。Buffer經常被用在 Node.js 中與 IO 相關的 api 上,如:文件操作、網絡通信等。

Libuv

Libuv 是跨平臺的、封裝操作系統 IO 操作的庫。Node.js 使用 Libuv 作爲自己的 event loop,並由 uv 負責 IO 操作,諸如:net、dgram、fs、tty 等模塊,以及 Timer 等類都可以認爲是基於 uv 的封裝。因此與 uv 相關的數據指標可以一定程度上反應出 Node.js 應用的穩定性。

Libuv Handles

libuv handles 指示了 Node.js 進程中各種IO對象(tcp, udp, fs, timer 等對象)的數量。對於常見的 web 應用來說, libuv handles 較高通常意味着當前請求量較大或者有 tcp 連接等未被正確釋放。之前在線上業務中還會經常發現有 handle 沒有被關閉,如:tcp、udp socket 不斷被創建,並且沒有被關閉,導致操作系統的端口被耗盡的問題出現。

Libuv Latency

libuv latency 並不是 libuvNode.js API 中可以直接獲取到的數據。目前主流的、對 libuv latency 的計算方式,都是通過 setTimeout() 來設置 timer ,並記錄回調函數被調用時所消耗的時間和預計消耗的時間之間的差值作爲 latency ,如:

const kInterval = 1000;
const start = getCurrentTs();
setTimeout(() => {
    const delay = Math.max(getCurrentTs() - start - kInterval, 0);
}, kInterval);

latency 數值較高通常意味着當前應用的 eventloop 過於繁忙,導致簡單的操作也不能按時完成。而對於 Node.js 進程來說,這類情況很可能是由調用了耗時較長的同步函數或是阻塞的 IO 操作導致。

發生這類問題時,對應的線程將沒辦法進行正常的服務,比如對於 http server 來說,在這段時間內的請求會得不到響應。因此我們需要保證主線程的 libuv latency 儘可能的小。

服務運行穩定性

狀態碼

這個應該不用多說,對於服務產生的所有 5xx 的狀態碼都屬於服務器在嘗試處理請求時發生內部錯誤,這些錯誤可能是服務器本身的錯誤,而不是請求出錯,都是需要我們關注的:

  • 500 (服務器內部錯誤)  服務器遇到錯誤,無法完成請求。
  • 501 (尚未實施) 服務器不具備完成請求的功能。例如,服務器無法識別請求方法時可能會返回此代碼。
  • 502 (錯誤網關) 服務器作爲網關或代理,從上游服務器收到無效響應。
  • 503 (服務不可用) 服務器目前無法使用(由於超載或停機維護)。通常,這只是暫時狀態。
  • 504 (網關超時)  服務器作爲網關或代理,但是沒有及時從上游服務器收到請求。
  • 505 (HTTP 版本不受支持) 服務器不支持請求中所用的 HTTP 協議版本。
  • 506 由《透明內容協商協議》(RFC 2295)擴展,代表服務器存在內部配置錯誤:被請求的協商變元資源被配置爲在透明內容協商中使用自己,因此在一個協商處理中不是一個合適的重點。
  • 507 服務器無法存儲完成請求所必須的內容。這個狀況被認爲是臨時的。
  • 509 服務器達到帶寬限制。這不是一個官方的狀態碼,但是仍被廣泛使用。
  • 510 獲取資源所需要的策略並沒有沒滿足。

錯誤日誌

服務運行過程中產生的錯誤日誌數量也是衡量一個服務是否穩定的重要指標,對於錯誤日誌上報,不同公司的業務可能有不同的實現,但是應該大同小異,一般日誌都分爲 INFO、WARN、ERROR 幾個級別,我們需要關注的是 ERROR 及以上級別的日誌。

一般在我們的業務邏輯中,都需要對服務運行的過程中產生的異常進行捕獲以及日誌上報,但是我們不可能在所有程序運行的節點進行異常捕獲,另外 try catch 也不是萬能的,它並不能捕獲異步異常,所以我們一般在我們使用的 Node.js 框架中的關鍵節點也會集成日誌的上報,以 KOA 爲例,我們需要監聽 app 的 error 事件:

        this.on('error', (error, ctx) => {
            if (error.status === 404) {
                return;
            }
            const message = error.stack || error.message;
            log(message);
        });

另外,我們還需要在 uncaughtException、unhandledRejection 中進行異常上報:

        process.on('unhandledRejection', (error) => {
            if (error) {
                log({
                    level'error',
                    location'[gulu-core]::UnhandledRejection',
                    message: error.stack || error.message,
                });
            }
        });
        process.on('uncaughtException', (error) => {
            log({
                level'error',
                location'[gulu-core]::UncaughtException',
                message: error.stack || error.message,
            });
            process.exit(1);
        });

進行了這樣的操作後,所有在你的業務邏輯中產生的異常都會被捕獲並上報,所以對於你想了解到的異常你不應該手動進行 try catch,而是將它們拋出到框架進行捕獲上報。

pm2 日誌

對於程序中我們自己打印出的一些 console ,一般生產環境是默認不會被記錄的。例如某些程序異常我們可能自己通過 try catch 進行了捕獲,並使用 console 輸出了 ERROR INFO ,這樣的異常並不會被當作錯誤日誌進行捕獲。

一般在線上運行的 Node 服務都是使用 PM2 啓動的。PM2node 進程管理工具,可以利用它來簡化很多 node 應用管理的繁瑣任務,如性能監控、自動重啓、負載均衡等。

我們可以通過 pm2 log 命令來查看當前程序運行的實時日誌,注意這個日誌是包括開發者自己打出來的一些 console 的。

另外 pm2 也支持查看所有歷史產生的日誌,我們可以通過一些 Error 之類的關鍵字去檢索錯誤日誌。

延時

延時情況也是衡量一個服務穩定性的重要指標,一些非常慢的接口除了會影響用戶體驗,還有可能會影響數據庫的穩定性,一般我們在接口的延時和數據庫的延時兩個方面關注服務延時,這個比較好理解,這裏我就不再多說了。

QPS

QPS:全名 Queries Per Second,意思是“每秒查詢率”,是一臺服務器每秒能夠響應的查詢次數,是對一個特定的查詢服務器在規定 Queries Per Second 時間內所處理流量多少的衡量標準。簡單的說,QPS = req / sec = 請求數/秒。它代表的是服務器的機器的性能最大吞吐能力。

正常來講服務的 QPS 可能隨着時間的變化進行有規律的增長或減小,但是如果在某段時間內 QPS 發生了成倍的數量級的增長,那麼有可能你的服務正在遭受 DDoS 攻擊,或者正在被非法調用。

本文分享自微信公衆號 - 1024譯站(trans1024)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章