作爲一個前端工程師,大家日常也會維護一些 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
也會比較低。也有例外情況:
-
CPU Load
低,CPU Usage
高:如果 CPU 執行的任務數很少,則CPU Load
會低,但是這些任務都是CPU密集型,那麼利用率就會高。 -
CPU Load
高,CPU Usage
低:如果CPU執行的任務數很多,則CPU Load
會高,但是在任務執行過程中 CPU 經常空閒(比如等待IO),那麼利用率就會低。
內存
內存 RSS
RSS :常駐內存集(Resident Set Size)用於表示系統有多少內存分配給當前進程,它能包括所有堆棧和堆內存,是 OOM 主要參考的指標。
內存 V8 Heap
表示 JavaScript
代碼執行佔用的內存。
一般我們可以看到 V8 Heap
區分了 Used
和 Total
,這裏是主要是因爲 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
並不是 libuv
或 Node.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
啓動的。PM2
是 node
進程管理工具,可以利用它來簡化很多 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源創計劃”,歡迎正在閱讀的你也加入,一起分享。