使用 Ebpf 監(jiān)控 Node.js 事件循環(huán)的耗時(shí)
前言:強(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
- __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);
我們可以看到 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)
執(zhí)行后看到輸出。
后記:本文大致介紹了基于 ebpf 實(shí)現(xiàn)對(duì) Node.js 事件循環(huán)的耗時(shí)監(jiān)控,這只是非常初步的探索。