使用 Ebpf 监控 Node.js 事件循环的耗时

运维 系统运维
本文介绍如何使用 ebpf 来监控 Node.js 的耗时,从而了解 Node.js 事件循环的执行情况。不过这只是粗粒度的监控,想要精细地了解 Node.js 的运行情况,需要做的事情还很多。

[[441086]]

前言:强大的 ebpf 使用越来越广,能做的事情也越来越多,尤其是无侵入的优雅方式更加是技术选型的好选择。本文介绍如何使用 ebpf 来监控 Node.js 的耗时,从而了解 Node.js 事件循环的执行情况。不过这只是粗粒度的监控,想要精细地了解 Node.js 的运行情况,需要做的事情还很多。

在 Node.js 里,我们可以通过 V8 Inspector 的 cpuprofile 来了解 JS 的执行耗时,但是 cpuprofile 无法看到 C、C++ 代码的执行耗时,通常我们可以使用 perf 工具来或许 C、C++ 代码的耗时,不过这里介绍的是通过 ebpf 来实现,不失为一种探索。首先来看一下对 poll io 阶段的监控。先定义一个结构体用于记录耗时。

  1. struct event  
  2.     __u64 start_time; 
  3.     __u64 end_time;  
  4. }; 

接着写 bpf 程序。

  1. #include <linux/bpf.h> 
  2.  
  3. #include <linux/ptrace.h> 
  4.  
  5. #include <bpf/bpf_helpers.h> 
  6.  
  7. #include <bpf/bpf_tracing.h> 
  8.  
  9. #include "uv.h" 
  10.  
  11. #include "uv_uprobe.h" 
  12.  
  13.  
  14.  
  15. char LICENSE[] SEC("license") = "Dual BSD/GPL"
  16.  
  17. #define MAX_ENTRIES 10240 
  18.  
  19. // 用于记录数据 
  20.  
  21. struct { 
  22.  
  23.     __uint(type, BPF_MAP_TYPE_HASH); 
  24.     __uint(max_entries, MAX_ENTRIES); 
  25.     __type(key, __u32); 
  26.     __type(value, const char *);} values SEC(".maps"); 
  27.  
  28.  
  29.  
  30. // 用于输入数据到用户层 
  31.  
  32. struct { 
  33.  
  34.     __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); 
  35.     __uint(key_size, sizeof(__u32)); 
  36.     __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){ 
  37.     __u64 current_id = id; 
  38.     __u64 time = bpf_ktime_get_ns(); 
  39.     bpf_map_update_elem(&values, &current_id, &time, BPF_ANY); 
  40.     return 0; 
  41.  
  42.  
  43.  
  44.  
  45. SEC("uretprobe/uv__io_poll"
  46.  
  47. int BPF_KRETPROBE(uretprobe_uv__io_poll){    
  48.     __u64 current_id  
  49.  
  50.     __u64 current_id = id; 
  51.     __u64 *time = bpf_map_lookup_elem(&values, &current_id); 
  52.     if (!time) { 
  53.         return 0; 
  54.     } 
  55.     struct event e; 
  56.     // 记录开始时间和结束时间 
  57.     e.start_time = *time
  58.     e.end_time = bpf_ktime_get_ns(); 
  59.     // 输出到用户层 
  60.     bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e)); 
  61.     bpf_map_delete_elem(&values, &current_id); 
  62.     id++; 
  63.     return 0; 
  64.  

最后编写使用 ebpf 程序的代码,只列出核心代码。

  1. #include <errno.h> 
  2.  
  3. #include <stdio.h> 
  4.  
  5. #include <unistd.h> 
  6.  
  7. #include <sys/resource.h> 
  8.  
  9. #include <bpf/libbpf.h> 
  10.  
  11. #include "uv_uprobe.skel.h" 
  12.  
  13. #include "uprobe_helper.h" 
  14.  
  15. #include <signal.h> 
  16.  
  17. #include <bpf/bpf.h> 
  18.  
  19. #include "uv_uprobe.h" 
  20.  
  21. // 输出结果函数 
  22.  
  23. static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz){ 
  24.  
  25.     const struct event *e = (const struct event *)data; 
  26.     printf("%s %llu\n""poll io", (e->end_time - e->start_time) / 1000 / 1000); 
  27.  
  28.  
  29.  
  30.  
  31. int main(int argc, char **argv){ 
  32.  
  33.     struct uv_uprobe_bpf *skel; 
  34.     long base_addr, uprobe_offset; 
  35.     int err, i; 
  36.     struct perf_buffer_opts pb_opts; 
  37.     struct perf_buffer *pb = NULL
  38.     // 监控哪个 Node.js 进程 
  39.     char * pid_str = argv[1]; 
  40.     pid_t pid = (pid_t)atoi(pid_str); 
  41.     char execpath[500]; 
  42.     // 根据 pid 找到 Node.js 的可执行文件 
  43.     int ret = get_pid_binary_path(pid, execpath, 500); 
  44.     // 需要监控的函数,uv__io_poll 是处理 poll io 阶段的函数 
  45.     char * func = "uv__io_poll"
  46.     // 通过可执行文件获得函数的地址 
  47.     uprobe_offset = get_elf_func_offset(execpath, func); 
  48.     // 加载 bpf 程序到内核 
  49.     skel = uv_uprobe_bpf__open(); 
  50.     err = uv_uprobe_bpf__load(skel); 
  51.     // 挂载监控点 
  52.     skel->links.uprobe_uv__io_poll = bpf_program__attach_uprobe(skel->progs.uprobe_uv__io_poll, 
  53.                             false /* not uretprobe */, 
  54.                             -1, 
  55.                             execpath, 
  56.                             uprobe_offset); 
  57.     skel->links.uretprobe_uv__io_poll = bpf_program__attach_uprobe(skel->progs.uretprobe_uv__io_poll, 
  58.                                true /* uretprobe */, 
  59.                                -1 /* any pid */, 
  60.                                execpath, 
  61.                                uprobe_offset); 
  62.     // 设置回调处理 bpf 的输出 
  63.     pb_opts.sample_cb = handle_event; 
  64.     pb_opts.lost_cb = handle_lost_events; 
  65.     pb = perf_buffer__new(bpf_map__fd(skel->maps.events), PERF_BUFFER_PAGES, 
  66.                   &pb_opts); 
  67.     printf("%-7s %-7s\n""phase""interval");            
  68.     for (i = 0; ; i++) { 
  69.         // 等待 bpf 的输出,然后执行回调处理,基于 epoll 实现 
  70.         perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS); 
  71.     } 
  72.  

编译以上代码,然后启动一个 Node.js 进程,接着把 Node.js 进程的 pid 作为参数执行上面代码,就可以看到 poll io 阶段的耗时,通常,如果 Node.js 里没有任务会阻塞到 epoll_wait 中,所以我们无法观察到耗时。我们只需要在代码里写个定时器就行。

  1. setInterval(() => {}, 3000); 

我们可以看到 poll io 耗时在 3s 左右,因为有定时器时,poll io 最多等待 3s 后就会返回,也就是整个 poll io 阶段的耗时。了解了基本的实现后,我们来监控整个事件循环每个阶段的耗时。原理是类似的。先定义一个处理多个阶段的宏。

  1. #define PHASE(uprobe) \ 
  2.     uprobe(uv__run_timers) \  
  3.     uprobe(uv__run_pending) \ 
  4.     uprobe(uv__run_idle) \ 
  5.     uprobe(uv__run_prepare) \ 
  6.     uprobe(uv__io_poll) \ 
  7.     uprobe(uv__run_check) \ 
  8.     uprobe(uv__run_closing_handles) 

接着改一下 bpf 代码。

  1. #define PROBE(type) \ 
  2. SEC("uprobe/" #type) \ 
  3. int BPF_KPROBE(uprobe_##type) \ 
  4. { \ 
  5.     char key[20] = #type; \ 
  6.     __u64 time = bpf_ktime_get_ns(); \ 
  7.     bpf_map_update_elem(&values, &key, &time, BPF_ANY); \ 
  8.     return 0; \ 
  9. } \ 
  10. SEC("uretprobe/" #type) \ 
  11. int BPF_KRETPROBE(uretprobe_##type) \ 
  12. {   \ 
  13.     char key[20] = #type; \ 
  14.     __u64 *time = bpf_map_lookup_elem(&values, &key); \ 
  15.     if (!time) { \ 
  16.         return 0; \ 
  17.     } \ 
  18.     struct event e = { \ 
  19.         .name=#type \ 
  20.     }; \ 
  21.     e.start_time = *time; \ 
  22.     e.end_time = bpf_ktime_get_ns(); \ 
  23.     bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e)); \ 
  24.     bpf_map_delete_elem(&valueskey); \ 
  25.     return 0; \ 
  26.  
  27. PHASE(PROBE) 

我们看到代码和之前的 bpf 代码是一样的,只是通过宏的方式,方便定义多个阶段,避免重复代码。主要了使用 C 的一些知识。#a 等于 "a",a##b 等于ab,"a" "b" 等于 "ab"("a" "b" 中间有个空格)。同样,写完 bpf 代码后,再改一下主程序的代码。

  1. #define ATTACH_UPROBE(type)  \ 
  2.       do \ 
  3.       {   char * func_##type = #type; \ 
  4.           uprobe_offset = get_elf_func_offset(execpath, func_##type); \ 
  5.           if (uprobe_offset == -1) { \ 
  6.               fprintf(stderr, "invalid function &s: %s\n", func_##type); \ 
  7.               break; \ 
  8.           } \ 
  9.           fprintf(stderr, "uprobe_offset: %ld\n", uprobe_offset);\ 
  10.           skel->links.uprobe_##type = bpf_program__attach_uprobe(skel->progs.uprobe_##type,\ 
  11.                                   false /* not uretprobe */,\ 
  12.                                   pid,\ 
  13.                                   execpath,\ 
  14.                                   uprobe_offset);\ 
  15.           skel->links.uretprobe_##type = bpf_program__attach_uprobe(skel->progs.uretprobe_##type,\ 
  16.                                   true /* uretprobe */,\ 
  17.                                   pid /* any pid */,\ 
  18.                                   execpath,\ 
  19.                                   uprobe_offset);\ 
  20.       } while(false);  
  21.  
  22.      PHASE(ATTACH_UPROBE) 

同样,代码还是一样的,只是变成了宏定义,然后通过 PHASE(ATTACH_UPROBE) 定义重复代码。这里使用了 do while(false) 是因为如果某个阶段的处理过程有问题,则忽略,因为我们不能直接 return,所以 do while 是比较好的实现方式。因为在我测试的时候,有两个阶段是失败的,原因是找不到对应函数的地址。最后写个测试代码。

  1. function compute() { 
  2.     let sum = 0; 
  3.     for(let i = 0; i < 10000000; i++) { 
  4.         sum += i; 
  5.     } 
  6.  
  7.  
  8. setInterval(() => { 
  9.  
  10.     compute(); 
  11.     setImmediate(() => { 
  12.         compute(); 
  13.     }); 
  14.  
  15. }, 10000) 

执行后看到输出。

后记:本文大致介绍了基于 ebpf 实现对 Node.js 事件循环的耗时监控,这只是非常初步的探索。

 

责任编辑:姜华 来源: 编程杂技
相关推荐

2021-05-27 09:00:00

Node.js开发线程

2024-01-05 08:49:15

Node.js异步编程

2021-12-01 00:05:03

Js应用Ebpf

2012-02-03 09:25:39

Node.js

2011-09-08 13:46:14

node.js

2023-01-31 16:43:31

​Node.js事件循环

2021-10-22 08:29:14

JavaScript事件循环

2021-12-08 07:55:41

EventLoop浏览器事件

2021-06-10 07:51:07

Node.js循环机制

2011-09-08 14:16:12

Node.js

2022-08-28 16:30:34

Node.jsDocker指令

2017-08-16 10:36:10

JavaScriptNode.js事件驱动

2023-01-10 14:11:26

2015-03-10 10:59:18

Node.js开发指南基础介绍

2013-11-01 09:34:56

Node.js技术

2017-04-20 12:30:57

声明式爬虫网络

2020-02-25 12:27:59

Node.jsWeb开发前端

2021-01-14 10:48:34

Docker CompNode.js开发

2020-05-29 15:33:28

Node.js框架JavaScript

2021-12-25 22:29:57

Node.js 微任务处理事件循环
点赞
收藏

51CTO技术栈公众号