一起聊一聊如何計算 Node.js GC 負(fù)載
在 Node.js 中,我們關(guān)注的比較的是 CPU 負(fù)載,但是在有 GC 的語言中,GC 負(fù)載也是需要關(guān)注的一個指標(biāo),因為 GC 過高會影響我們應(yīng)用的性能。本文介紹關(guān)于 GC 負(fù)載的一些內(nèi)容。
如何獲取 GC 耗時
操作系統(tǒng)本身會計算每隔線程的 CPU 耗時,所以我們可以通過系統(tǒng)獲取這個數(shù)據(jù),然后計算出線程的 CPU 負(fù)載。但是 GC 不一樣,因為 GC 是應(yīng)用層的一個概念,操作系統(tǒng)是不會感知的,在 Node.js 里,具體來說,是在 V8 里,也沒有 API 可以直接獲取 GC 的耗時,但是 V8 提供了一些 GC 的鉤子函數(shù),我們可以借助這些鉤子函數(shù)來計算出 GC 的負(fù)載。其原理和 CPU 負(fù)載類似。V8 提供了以下兩個鉤子函數(shù),分別在 GC 開始和結(jié)束時會執(zhí)行。
Isolate::GetCurrent()->AddGCPrologueCallback();
Isolate::GetCurrent()->AddGCEpilogueCallback();
通過這兩個函數(shù),我們就可以得到每一次 GC 的耗時,再不斷累積就可以計算出 GC 的總耗時,從而計算出 GC 負(fù)載。下面看一下核心實現(xiàn)。
static void BeforeGCCallback(Isolate* isolate,
v8::GCType gc_type,
v8::GCCallbackFlags flags,
void* data) {
GCLoad* gc_load = static_cast<GCLoad*>(data);
if (gc_load->current_gc_type != 0) {
return;
}
gc_load->current_gc_type = gc_type;
gc_load->start_time = uv_hrtime();
}
static void AfterGCCallback(Isolate* isolate,
v8::GCType gc_type,
v8::GCCallbackFlags flags,
void* data) {
GCLoad* gc_load = static_cast<GCLoad*>(data);
if (gc_load->current_gc_type != gc_type) {
return;
}
gc_load->current_gc_type = 0;
gc_load->total_time += uv_hrtime() - gc_load->start_time;
gc_load->start_time = 0;
}
void GCLoad::Start(const FunctionCallbackInfo<Value>& args) {
GCLoad* obj = ObjectWrap::Unwrap<GCLoad>(args.Holder());
Isolate::GetCurrent()->AddGCPrologueCallback(BeforeGCCallback, static_cast<void*>(obj));
Isolate::GetCurrent()->AddGCEpilogueCallback(AfterGCCallback, static_cast<void*>(obj));
}
可以看到思路很簡單,就是注冊兩個 GC 鉤子函數(shù),然后在 GC 開始鉤子中記錄開始時間,然后在 GC 結(jié)束鉤子中記錄結(jié)束時間,并算出一次 GC 的耗時,再累加起來,這樣就可以得到任意時刻 GC 的總耗時,但是拿到總耗時如何計算出 GC 負(fù)載呢?
如何計算 GC 負(fù)載
負(fù)載 = 過去一段時間內(nèi)的消耗 / 過去的一段時間值,看看如何計算 GC 負(fù)載。
class GCLoad {
lastTime;
lastTotal;
binding = null;
start() {
if (!this.binding) {
this.binding = new binding.GCLoad();
this.binding.start();
}
}
stop() {
if (this.binding) {
this.binding.stop();
this.binding = null;
}
}
load() {
if (this.binding) {
const { lastTime, lastTotal } = this;
const now = process.hrtime();
const total = this.binding.total();
this.lastTime = now;
this.lastTotal = total;
if (lastTime && lastTotal) {
const cost = total - lastTotal;
const interval = (now[0] - lastTime[0]) * 1e6 + (now[1] - lastTime[1]) / 1e3;
return cost / interval;
}
}
}
total() {
if (this.binding) {
return this.binding.total();
}
}
}
計算算法也很簡單,就是記錄上次的時間和 GC 耗時,然后下次需要記錄某個時刻的 GC 負(fù)載時,就拿當(dāng)前的耗時減去上次的耗時,并拿當(dāng)前的時間減去上次的時間,然后得到過去一段時間內(nèi)的耗時和過去的時間大小,一處就得到 GC 負(fù)載了。
使用
下面看看如何使用。
const { GCLoad } = require('..');
const gcLoad = new GCLoad();
gcLoad.start();
setInterval(() => {
for (let i = 0; i < 1000; i++) {
new Array(100);
}
gc();
console.log(gcLoad.load());
}, 3000);
執(zhí)行上面代碼會(node --expose-gc demo.js) 在我電腦上輸出如下。
0.004235378248715853
0.004100483670865412
0.0017808558192331187
0.002371772559838465
0.0024768595957239477
這樣就可以得到了應(yīng)用的 GC 負(fù)載。
完整代碼參考 https://github.com/theanarkh/nodejs-native-gc-load。