探索 Ebpf 在 Node.Js 中的應(yīng)用
前言
ebpf 是現(xiàn)代 Linux 內(nèi)核提供的非常復(fù)雜和強(qiáng)大的技術(shù),它使得 Linux 內(nèi)核變得可編程,不再是完全的黑盒子。隨著 ebpf 的發(fā)展和成熟,其應(yīng)用也越來(lái)越廣泛,本文介紹如何使用 ebpf 來(lái)追蹤 Node.js 底層的代碼。
介紹
ebpf 的設(shè)計(jì)思想雖然很簡(jiǎn)單,但是實(shí)現(xiàn)和使用上非常復(fù)雜。ebpf 本質(zhì)上內(nèi)核實(shí)現(xiàn)了一個(gè)虛擬機(jī),用戶可以把自己編寫的 c 代碼加載進(jìn)內(nèi)核中執(zhí)行,從而參與內(nèi)核的邏輯處理。這聽起來(lái)很簡(jiǎn)單,但是整個(gè)技術(shù)其實(shí)非常復(fù)雜,從實(shí)現(xiàn)來(lái)說(shuō),內(nèi)核需要對(duì)加載的代碼進(jìn)行非常多而復(fù)雜的校驗(yàn),以保證安全性,內(nèi)核還需要實(shí)現(xiàn)一個(gè)虛擬機(jī)來(lái)執(zhí)行用戶的代碼和在內(nèi)核代碼中加入支持 ebpf 機(jī)制的邏輯。從使用來(lái)說(shuō),使用或編寫 ebpf 代碼對(duì)我們來(lái)說(shuō)成本非常高,我們需要學(xué)會(huì)搭建環(huán)境,需要了解如何編譯 ebpf 程序,甚至還需要了解 Linux 內(nèi)核的一些知識(shí)。不過(guò)隨著 ebpf 多年的發(fā)展,這種情況已經(jīng)改善了很多。ebpf 的介紹在網(wǎng)上有很多,這里就不多介紹。
使用
下面來(lái)看一下如何基于 libbpf 寫一個(gè) ebpf 程序。ebpf 程序分為兩個(gè)部分,第一部分是 ebpf 代碼。hello.bpf.c
- #include <linux/bpf.h>
- #include <bpf/bpf_helpers.h>
- SEC("tracepoint/syscalls/sys_enter_execve")
- int handle_tp(void *ctx){
- int pid = bpf_get_current_pid_tgid()>> 32;
- char fmt[] = "BPF triggered from PID %d.\n";
- bpf_trace_printk(fmt, sizeof(fmt), pid);
- return 0;
- }
- char LICENSE[] SEC("license") = "Dual BSD/GPL";
以上是被加載進(jìn)內(nèi)核執(zhí)行的代碼,主要是利用內(nèi)核的 tracepoint 機(jī)制,給 sys_enter_execve 函數(shù)插入一個(gè)鉤子,每次執(zhí)行到這個(gè)函數(shù)時(shí),鉤子函數(shù)就會(huì)被執(zhí)行。另一部分是負(fù)責(zé)把 ebpf 代碼加載進(jìn)內(nèi)核的代碼。hello.c
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <assert.h>
- #include <errno.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <sys/resource.h>
- #include <bpf/libbpf.h>
- #include "hello.skel.h"
- int main(int argc, char **argv){
- struct hello_bpf *skel;
- int err;
- /* Open BPF application */
- skel = hello_bpf__open();
- /* Load & verify BPF programs */
- err = hello_bpf__load(skel);
- /* Attach tracepoint handler */
- err = hello_bpf__attach(skel);
- printf("Hello BPF started, hit Ctrl+C to stop!\n");
- // output
- read_trace_pipe();
- cleanup:
- hello_bpf__destroy(skel);
- return -err;
- }
這里只列出核心的代碼,hello.c 的邏輯很簡(jiǎn)單,打開 ebpf 然后加載到內(nèi)核,最后查看 ebpf 程序的輸入。這就是 ebpf 程序的整體邏輯,過(guò)程都差不多,重點(diǎn)是確定我們需要做什么事情,然后寫不同的代碼。最后,如果不再需要追蹤的時(shí)候,可以銷毀 ebpf 代碼。
應(yīng)用
在 ebpf 之前,內(nèi)核對(duì)我們來(lái)說(shuō)是一個(gè)黑盒子。有了 ebpf 之后,內(nèi)核對(duì)我們透明了很多。但是軟件是分層的,我們平時(shí)直接和內(nèi)核打交道并不多,我們更關(guān)心上層軟件的情況。具體來(lái)說(shuō),當(dāng)我們使用一個(gè) Node.js 的時(shí)候,除了關(guān)心業(yè)務(wù)代碼,我們也需要關(guān)心 Node.js 本身的代碼。但是 Node.js 對(duì)我們來(lái)說(shuō)也是個(gè)黑盒子,我們不知道它具體做了什么事情或者某一個(gè)時(shí)刻的運(yùn)行狀態(tài),這樣非常不利于我們排查問(wèn)題或者了解系統(tǒng)的運(yùn)行情況。有了 ebpf 后,我們就可以做更多的事情了。Linux 內(nèi)核提供了非常多的代碼追蹤技術(shù),其中有一種是 uprobe,uprobe 是一種動(dòng)態(tài)追蹤應(yīng)用代碼的技術(shù),比如我們想了解 Node.js 的 Libuv 中的 uv_tcp_listen 函數(shù),那么我們就可以通過(guò) ebpf 去實(shí)現(xiàn)這種效果。有了這種能力,我們就可以掌握系統(tǒng)更多的數(shù)據(jù)和信息。
實(shí)現(xiàn)
應(yīng)用層使用 uprobe 比 kprobe 復(fù)雜,kprobe 是用于追蹤內(nèi)核函數(shù),因?yàn)閮?nèi)核知道它的函數(shù)對(duì)應(yīng)的虛擬地址,所以我們只需要告訴它函數(shù)名就可以實(shí)現(xiàn)對(duì)該函數(shù)的追蹤,但是 uprobe 則不一樣,uprobe 是用于追蹤應(yīng)用層代碼的,內(nèi)核并不知道或者說(shuō)不應(yīng)該關(guān)注某個(gè)函數(shù)對(duì)應(yīng)的虛擬地址,所以這個(gè)難題需要應(yīng)用層解決。下面來(lái)看一下具體的實(shí)現(xiàn)。uprobe.bpf.c
- #include <linux/bpf.h>
- #include <linux/ptrace.h>
- #include <bpf/bpf_helpers.h>
- #include <bpf/bpf_tracing.h>
- #include "uv.h"
- char LICENSE[] SEC("license") = "Dual BSD/GPL";
- SEC("uprobe/uv_tcp_listen")
- int BPF_KPROBE(uprobe, uv_tcp_t* tcp, int backlog, uv_connection_cb cb){
- bpf_printk("uv_tcp_listen start %d \n", backlog);
- return 0;
- }
- SEC("uretprobe/uv_tcp_listen")
- int BPF_KRETPROBE(uretprobe, int ret){
- bpf_printk("uv_tcp_listen end %d \n", ret);
- return 0;
- }
這里我們實(shí)現(xiàn)了對(duì) libuv 的 uv_tcp_listen 函數(shù)進(jìn)行追蹤,包括函數(shù)開始執(zhí)行和執(zhí)行完畢兩個(gè)追蹤點(diǎn)。定義完 ebpf 程序后,來(lái)看一下如何加載到內(nèi)核。uprobe.c
- int main(int argc, char **argv){
- struct uprobe_bpf *skel;
- long base_addr, uprobe_offset;
- int err, i;
- // 要追蹤的可執(zhí)行文件
- char execpath[50] = "/usr/bin/node";
- char * func = "uv_tcp_listen";
- // 計(jì)算某個(gè)函數(shù)在可執(zhí)行文件里的地址偏移
- uprobe_offset = get_elf_func_offset(execpath, func);
- /* Load and verify BPF application */
- skel = uprobe_bpf__open_and_load();
- /* Attach tracepoint handler */
- skel->links.uprobe = bpf_program__attach_uprobe(skel->progs.uprobe,
- false /* not uretprobe */,
- -1, /* any pid */
- execpath,
- uprobe_offset);
- skel->links.uretprobe = bpf_program__attach_uprobe(skel->progs.uretprobe,
- true /* uretprobe */,
- -1 /* any pid */,
- execpath,
- uprobe_offset);
- // ...
- cleanup:
- uprobe_bpf__destroy(skel);
- return -err;
- }
uprobe.c 的重點(diǎn)在于計(jì)算某個(gè)函數(shù)在某個(gè)可執(zhí)行文件的地址信息,這個(gè)主要是利用 elf 文件來(lái)判斷,elf 是代碼編譯后生成的一個(gè)可執(zhí)行文件,它里面可以記錄了關(guān)于可執(zhí)行文件的一些元數(shù)據(jù)(也可以通過(guò) readelf -Ws exen_file 查看),比如符號(hào)表里記錄了函數(shù)的信息,拿到相關(guān)信息后,設(shè)置 uprobe 和 uretprobe就可以了。通過(guò)上面的 ebpf 代碼,我們就可以追蹤到 uv_tcp_listen 函數(shù)的調(diào)用情況,有了這種能力,我們就可以隨便監(jiān)聽自己想監(jiān)聽的函數(shù)。除了 uprobe 之后,我們還可以利用內(nèi)核的 kprobe 監(jiān)聽內(nèi)核函數(shù)。比如下面的 ebpf 代碼就可以實(shí)現(xiàn)對(duì)創(chuàng)建進(jìn)程的追蹤。
- SEC("kprobe/__x64_sys_execve")
- int BPF_KPROBE(__x64_sys_execve){
- pid_t pid;
- pid = bpf_get_current_pid_tgid() >> 32;
- bpf_printk("KPROBE ENTRY pid = %d", pid);
- return 0;
- }
- SEC("kretprobe/__x64_sys_execve")
- int BPF_KRETPROBE(__x64_sys_execve_exit){
- pid_t pid;
- pid = bpf_get_current_pid_tgid() >> 32;
- bpf_printk("KPROBE EXIT: pid = %d\n", pid);
- return 0;
- }
總結(jié)
簡(jiǎn)單地介紹了一下強(qiáng)大的 ebpf 技術(shù)和在 Node.js 中的應(yīng)用,但是這只是個(gè)簡(jiǎn)單的例子,我們還有很多事情需要做,比如能否結(jié)合 addon 來(lái)使用,如何支持動(dòng)態(tài)能力等等。另外因?yàn)?C++ 代碼編譯后的函數(shù)名和原來(lái)的是不太一樣的,這可能會(huì)導(dǎo)致我們通過(guò)函數(shù)名找虛擬地址時(shí)找不到,這里也還有很多需要研究的地方??偟膩?lái)說(shuō),ebpf 不僅對(duì) Node.js 來(lái)說(shuō)非常有價(jià)值,對(duì)其他應(yīng)用層來(lái)說(shuō)意義也是一樣的。這是一個(gè)非常值得探索的技術(shù)方向。
代碼倉(cāng)庫(kù):https://github.com/theanarkh/libbpf-code