您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“如何使用ebpf監(jiān)控Node.js事件循環(huán)的耗時(shí)”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
前言:
強(qiáng)大的 ebpf 使用越來越廣,能做的事情也越來越多,尤其是無侵入的優(yōu)雅方式更加是技術(shù)選型的好選擇。本文介紹如何使用 ebpf 來監(jiān)控 Node.js 的耗時(shí),從而了解 Node.js 事件循環(huán)的執(zhí)行情況。不過這只是粗粒度的監(jiān)控,想要精細(xì)地了解 Node.js 的運(yùn)行情況,需要做的事情還很多。
在 Node.js 里,我們可以通過 V8 Inspector 的 cpuprofile 來了解 JS 的執(zhí)行耗時(shí),但是 cpuprofile 無法看到 C、C++ 代碼的執(zhí)行耗時(shí),通常我們可以使用 perf 工具來或許 C、C++ 代碼的耗時(shí),不過這里介紹的是通過 ebpf 來實(shí)現(xiàn),不失為一種探索。
首先來看一下對(duì) poll io 階段的監(jiān)控。先定義一個(gè)結(jié)構(gòu)體用于記錄耗時(shí)。
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
// 用于記錄數(shù)據(jù)
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32);
__type(value, const char *);
} values SEC(".maps");
// 用于輸入數(shù)據(jù)到用戶層
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;
// 記錄開始時(shí)間和結(jié)束時(shí)間
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"
// 輸出結(jié)果函數(shù)
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;
// 監(jiān)控哪個(gè) Node.js 進(jìn)程
char * pid_str = argv[1];
pid_t pid = (pid_t)atoi(pid_str);
char execpath[500];
// 根據(jù) pid 找到 Node.js 的可執(zhí)行文件
int ret = get_pid_binary_path(pid, execpath, 500);
// 需要監(jiān)控的函數(shù),uv__io_poll 是處理 poll io 階段的函數(shù)
char * func = "uv__io_poll";
// 通過可執(zhí)行文件獲得函數(shù)的地址
uprobe_offset = get_elf_func_offset(execpath, func);
// 加載 bpf 程序到內(nèi)核
skel = uv_uprobe_bpf__open();
err = uv_uprobe_bpf__load(skel);
// 掛載監(jiān)控點(diǎn)
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);
// 設(shè)置回調(diào)處理 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 的輸出,然后執(zhí)行回調(diào)處理,基于 epoll 實(shí)現(xiàn)
perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
}
}
編譯以上代碼,然后啟動(dòng)一個(gè) Node.js 進(jìn)程,接著把 Node.js 進(jìn)程的 pid 作為參數(shù)執(zhí)行上面代碼,就可以看到 poll io 階段的耗時(shí),通常,如果 Node.js 里沒有任務(wù)會(huì)阻塞到 epoll_wait 中,所以我們無法觀察到耗時(shí)。我們只需要在代碼里寫個(gè)定時(shí)器就行。
setInterval(() => {}, 3000);
1
我們可以看到 poll io 耗時(shí)在 3s 左右,因?yàn)橛卸〞r(shí)器時(shí),poll io 最多等待 3s 后就會(huì)返回,也就是整個(gè) poll io 階段的耗時(shí)。了解了基本的實(shí)現(xiàn)后,我們來監(jiān)控整個(gè)事件循環(huán)每個(gè)階段的耗時(shí)。原理是類似的。先定義一個(gè)處理多個(gè)階段的宏。
#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 代碼是一樣的,只是通過宏的方式,方便定義多個(gè)階段,避免重復(fù)代碼。主要了使用 C 的一些知識(shí)。#a 等于 “a”,a##b 等于ab,“a” “b” 等于 “ab”(“a” “b” 中間有個(gè)空格)。同樣,寫完 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) 定義重復(fù)代碼。這里使用了 do while(false) 是因?yàn)槿绻硞€(gè)階段的處理過程有問題,則忽略,因?yàn)槲覀儾荒苤苯?return,所以 do while 是比較好的實(shí)現(xiàn)方式。因?yàn)樵谖覝y(cè)試的時(shí)候,有兩個(gè)階段是失敗的,原因是找不到對(duì)應(yīng)函數(shù)的地址。最后寫個(gè)測(cè)試代碼。
function compute() {
let sum = 0;
for(let i = 0; i < 10000000; i++) {
sum += i;
}
}
setInterval(() => {
compute();
setImmediate(() => {
compute();
});
}, 10000)
“如何使用ebpf監(jiān)控Node.js事件循環(huán)的耗時(shí)”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。