本篇內容介紹了“如何使用ebpf監控Node.js事件循環的耗時”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
前言:
強大的 ebpf 使用越來越廣,能做的事情也越來越多,尤其是無侵入的優雅方式更加是技術選型的好選擇。本文介紹如何使用 ebpf 來監控 Node.js 的耗時,從而了解 Node.js 事件循環的執行情況。不過這只是粗粒度的監控,想要精細地了解 Node.js 的運行情況,需要做的事情還很多。
在 Node.js 里,我們可以通過 V8 Inspector 的 cpuprofile 來了解 JS 的執行耗時,但是 cpuprofile 無法看到 C、C++ 代碼的執行耗時,通常我們可以使用 perf 工具來或許 C、C++ 代碼的耗時,不過這里介紹的是通過 ebpf 來實現,不失為一種探索。
首先來看一下對 poll io 階段的監控。先定義一個結構體用于記錄耗時。
struct event
{
__u64 start_time;
__u64 end_time;
};
接著寫 bpf 程序。
#include <linux/bpf.h>
#include <linux/ptrace.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "uv.h"
#include "uv_uprobe.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
#define MAX_ENTRIES 10240
// 用于記錄數據
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32);
__type(value, const char *);
} values SEC(".maps");
// 用于輸入數據到用戶層
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u32));
} events SEC(".maps");
static __u64 id = 0;
SEC("uprobe/uv__io_poll")
int BPF_KPROBE(uprobe_uv__io_poll, uv_loop_t* loop, int timeout)
{
__u64 current_id = id;
__u64 time = bpf_ktime_get_ns();
bpf_map_update_elem(&values, ¤t_id, &time, BPF_ANY);
return 0;
}
SEC("uretprobe/uv__io_poll")
int BPF_KRETPROBE(uretprobe_uv__io_poll)
{
__u64 current_id = id;
__u64 *time = bpf_map_lookup_elem(&values, ¤t_id);
if (!time) {
return 0;
}
struct event e;
// 記錄開始時間和結束時間
e.start_time = *time;
e.end_time = bpf_ktime_get_ns();
// 輸出到用戶層
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e));
bpf_map_delete_elem(&values, ¤t_id);
id++;
return 0;
}
最后編寫使用 ebpf 程序的代碼,只列出核心代碼。
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "uv_uprobe.skel.h"
#include "uprobe_helper.h"
#include <signal.h>
#include <bpf/bpf.h>
#include "uv_uprobe.h"
// 輸出結果函數
static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)
{
const struct event *e = (const struct event *)data;
printf("%s %llu\n", "poll io", (e->end_time - e->start_time) / 1000 / 1000);
}
int main(int argc, char **argv)
{
struct uv_uprobe_bpf *skel;
long base_addr, uprobe_offset;
int err, i;
struct perf_buffer_opts pb_opts;
struct perf_buffer *pb = NULL;
// 監控哪個 Node.js 進程
char * pid_str = argv[1];
pid_t pid = (pid_t)atoi(pid_str);
char execpath[500];
// 根據 pid 找到 Node.js 的可執行文件
int ret = get_pid_binary_path(pid, execpath, 500);
// 需要監控的函數,uv__io_poll 是處理 poll io 階段的函數
char * func = "uv__io_poll";
// 通過可執行文件獲得函數的地址
uprobe_offset = get_elf_func_offset(execpath, func);
// 加載 bpf 程序到內核
skel = uv_uprobe_bpf__open();
err = uv_uprobe_bpf__load(skel);
// 掛載監控點
skel->links.uprobe_uv__io_poll = bpf_program__attach_uprobe(skel->progs.uprobe_uv__io_poll,
false /* not uretprobe */,
-1,
execpath,
uprobe_offset);
skel->links.uretprobe_uv__io_poll = bpf_program__attach_uprobe(skel->progs.uretprobe_uv__io_poll,
true /* uretprobe */,
-1 /* any pid */,
execpath,
uprobe_offset);
// 設置回調處理 bpf 的輸出
pb_opts.sample_cb = handle_event;
pb_opts.lost_cb = handle_lost_events;
pb = perf_buffer__new(bpf_map__fd(skel->maps.events), PERF_BUFFER_PAGES,
&pb_opts);
printf("%-7s %-7s\n", "phase", "interval");
for (i = 0; ; i++) {
// 等待 bpf 的輸出,然后執行回調處理,基于 epoll 實現
perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
}
}
編譯以上代碼,然后啟動一個 Node.js 進程,接著把 Node.js 進程的 pid 作為參數執行上面代碼,就可以看到 poll io 階段的耗時,通常,如果 Node.js 里沒有任務會阻塞到 epoll_wait 中,所以我們無法觀察到耗時。我們只需要在代碼里寫個定時器就行。
setInterval(() => {}, 3000);
1
我們可以看到 poll io 耗時在 3s 左右,因為有定時器時,poll io 最多等待 3s 后就會返回,也就是整個 poll io 階段的耗時。了解了基本的實現后,我們來監控整個事件循環每個階段的耗時。原理是類似的。先定義一個處理多個階段的宏。
#define PHASE(uprobe) \
uprobe(uv__run_timers) \
uprobe(uv__run_pending) \
uprobe(uv__run_idle) \
uprobe(uv__run_prepare) \
uprobe(uv__io_poll) \
uprobe(uv__run_check) \
uprobe(uv__run_closing_handles)
接著改一下 bpf 代碼。
#define PROBE(type) \
SEC("uprobe/" #type) \
int BPF_KPROBE(uprobe_##type) \
{ \
char key[20] = #type; \
__u64 time = bpf_ktime_get_ns(); \
bpf_map_update_elem(&values, &key, &time, BPF_ANY); \
return 0; \
} \
SEC("uretprobe/" #type) \
int BPF_KRETPROBE(uretprobe_##type) \
{ \
char key[20] = #type; \
__u64 *time = bpf_map_lookup_elem(&values, &key); \
if (!time) { \
return 0; \
} \
struct event e = { \
.name=#type \
}; \
e.start_time = *time; \
e.end_time = bpf_ktime_get_ns(); \
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e)); \
bpf_map_delete_elem(&values, key); \
return 0; \
}
PHASE(PROBE)
我們看到代碼和之前的 bpf 代碼是一樣的,只是通過宏的方式,方便定義多個階段,避免重復代碼。主要了使用 C 的一些知識。#a 等于 “a”,a##b 等于ab,“a” “b” 等于 “ab”(“a” “b” 中間有個空格)。同樣,寫完 bpf 代碼后,再改一下主程序的代碼。
#define ATTACH_UPROBE(type) \
do \
{ char * func_##type = #type; \
uprobe_offset = get_elf_func_offset(execpath, func_##type); \
if (uprobe_offset == -1) { \
fprintf(stderr, "invalid function &s: %s\n", func_##type); \
break; \
} \
fprintf(stderr, "uprobe_offset: %ld\n", uprobe_offset);\
skel->links.uprobe_##type = bpf_program__attach_uprobe(skel->progs.uprobe_##type,\
false /* not uretprobe */,\
pid,\
execpath,\
uprobe_offset);\
skel->links.uretprobe_##type = bpf_program__attach_uprobe(skel->progs.uretprobe_##type,\
true /* uretprobe */,\
pid /* any pid */,\
execpath,\
uprobe_offset);\
} while(false);
PHASE(ATTACH_UPROBE)
同樣,代碼還是一樣的,只是變成了宏定義,然后通過 PHASE(ATTACH_UPROBE) 定義重復代碼。這里使用了 do while(false) 是因為如果某個階段的處理過程有問題,則忽略,因為我們不能直接 return,所以 do while 是比較好的實現方式。因為在我測試的時候,有兩個階段是失敗的,原因是找不到對應函數的地址。最后寫個測試代碼。
function compute() {
let sum = 0;
for(let i = 0; i < 10000000; i++) {
sum += i;
}
}
setInterval(() => {
compute();
setImmediate(() => {
compute();
});
}, 10000)
“如何使用ebpf監控Node.js事件循環的耗時”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。