溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何使用ebpf監(jiān)控Node.js事件循環(huán)的耗時(shí)

發(fā)布時(shí)間:2022-01-21 10:41:34 來源:億速云 閱讀:198 作者:iii 欄目:web開發(fā)

本篇內(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, &current_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, &current_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, &current_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í)用文章!

向AI問一下細(xì)節(jié)

免責(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)容。

AI