Deepflow Agent代码阅读杂记

2024-06-28 10:00:10 浏览数 (2)

本文章是前端时间读代码时的随手记录,没有做系统整理,估计也不会填坑了,大家随便看看就好。

1 主体结构

1.1 构建思路:

用户态代码:rust c,rust使用FFI(Foreign Function Interface)调用c,过程中使用 libc crate,它包含了 C 标准库中的类型别名和函数定义,编译时会静态连接libc。

1.2 构建结构:

build.rs

代码语言:make复制
main()
|-- set_build_info()         这里还会检查git仓的信息
    set_build_libtrace()     这里会执行src/ebpf下的make
    set_linkage()            打印出链接信息

src/ebpf

代码语言:make复制
|-- kernel    内核态ebpf程序
|-- user      用户态程序c实现
|-- mod.rs    rust FFI对user封装的mod

src/ebpf/Makefile

代码语言:txt复制
定义了函数compile_socket_trace_elf:构建socket_trace.elf,使用bintobuffer把字节码转成buffer放到一个.c文件(bintobuffer是这个项目自带的一个工具)
定义了函数compile_perf_profiler_elf:构建perf_profiler.elf,使用bintobuffer把字节码转成buffer放到一个.c文件
build:先编译内核态生成.elf,然后再编译用户态生成.a(.a会被rust静态链接)

src/ebpf/kernel/Makefile

代码语言:txt复制
将socket_trace和perf_profiler编译成.elf(elf文件收敛到两个,其他.c被include到这两个文件里)
将编译后的文件反编译成.objdump
剥离掉对象文件中的调试信息

1.3 初始化流程

src/main.rs

代码语言:rust复制
main()
|-- src/trident.rs
     Trident::start()
     -> Self::run()
     -> Components::new()
     -> AgentComponents::new()
          |-- src/ebpf_dispatcher/ebpf_dispatcher.rs
               EbpfCollector::new()
               -> Self::ebpf_init()
                    |-- src/ebpf/mod.rs
                         bpf_tracer_init()

2 grpc接口

2.1 接口目录

  • proto的目录在和agent同级的message目录下
  • 生成的接口文件在agent/crates/public/src/proto下(其中telemetry是submodule,代码仓要git clone)
  • 使用tonic进行build,build文件为agent/crates/public/build.rs

2.2 trident.proto

rpc

  • Sync:向Server同步所在主机信息,Server会返回本机的容器列表
  • Push:向Server同步所在主机信息,Server会持续应答(Stream,猜测用于实时变化)
  • AnalyzerSync:和Sync的消息一样
  • Upgrade:应答是Stream,返回一堆二进制报文
  • Query:时钟同步
  • GenesisSync:待研究
  • KubernetesAPISync:k8 API信息,盲猜agent没用
  • PrometheusAPISync:同上

3 用户态

3.1 src/ebpf/kernel/include/socket_trace_common.h

  • pid这里是线程号,tgid是进程号,二者一致表示是单线程;
  • coroutine_id是go的协程号

3.2 src/ebpf/kernel/socket_trace.c

  • 这里定义了一个PERF_EVENT的map:socket_data,给用户态传数据
  • 两个尾调跳转PROG_ARRAY的map:progs_jmp_kp_map(kprobe/uprobe),progs_jmp_tp_map(for tracepoint)
  • Tracepoint 则更像是静态的,已经存在于内核中的 hook 点,不够灵活,但是相对固定,在不同版本的操作系统中变化不大,开销也更小;
  • k retprobe 是动态追踪,我们可以在内核函数的开头和结尾进行追踪,相对更灵活;但是其开销也更大。
  • infer_tcp_seq_offset:这个方法获取tcp_sock的copied_seq_offset,这里实现和操作系统有耦合,tcp_seq_offset对于不支持的btf的系统是通过推断得到的,对于支持btf的内核直接从btf文件读取得到的bpf_skc_to_tcp_sock() 这个辅助函数从5.9内核引入。
代码语言:c复制
PROGTP(io_event)(void *ctx)
= SEC("prog/tp/io_event") int bpf_prog_tp__io_event(void *ctx)
{
     获取tgid,pid
    查询active_read_args_map
    如果被标记跟踪:
    {
        trace_io_event_common(ctx, data_args, T_INGRESS, id)
        {
            从trace_conf_map获取一下配置
            从trace_map中获取trace_info
            从io_event_buffer(map)中获取内存块读取buffer内容
            从data_buf(map)中获取存放socket_buffer的内存块v_buff(这个v_buff可能存多个__socket_data)
            将socket_buffer放到v_buff后面
            bpf_get_current_comm(用当前进程名填充socket_buffer的comm)
            设置尾调上下文socket_buffer->data
            触发尾调progs_jmp_tp_map(具体尾调函数看用户态代码)
        }
        删除跟踪
        return // 读写跟踪只能开启一个
    }
    查询active_write_args_map
    如果被标记跟踪:
    {
        trace_io_event_common(ctx, data_args, T_EGRESS, id)
        删除跟踪
        return // 读写跟踪只能开启一个
    }
}

4 probe挂载

4.1 src/ebpf_dispatcher/ebpf_dispatcher.rs

代码语言:rust复制
ebpf_init
|-- src/ebpf/mod.rs
     running_socket_tracer
     |-- src/ebpf/user/socket.c
          running_socket_tracer
          -> process_events_handle_main
          -> process_probes_act
          |-- src/ebpf/user/trace.c
               tracer_hooks_attach/tracer_hooks_dettach
               -> tracer_hooks_process
               -> probe_attach
               -> exec_attach_kprobe/exec_attach_uprobe
               |-- src/ebpf/user/probe.c
                    program__attach_kprobe/program__attach_uprobe
                    -> program__attach_probe
                    -> bpf_attach_kprobe/bpf_attach_uprobe(bcc)

4.2 src/ebpf/user/socket.c

代码语言:c复制
running_socket_tracer
bpf_bin_buffer指向ebpf字节码
buffer_sz字节码长度
|-- src/ebpf/user/trace.c
     tracer_bpf_load
     |-- src/ebpf/user/load.c
          ebpf_open_buffer
          |-- src/ebpf/user/elf.c
               elf_info_collect
          --> set_obj__version
          --> set_obj__license
          --> ebpf_obj__maps_collect
          --> ebpf_btf_collect
          --> ebpf_btf_ext_collect
          ebpf_obj_load
          --> bcc_create_map(bcc)
          |-- src/ebpf/user/btf_vmlinux.c
               ebpf_obj__load_vmlinux_btf
          --> load_obj__progs

4.3 用户编写的 eBPF 程序最终会被编译成 eBPF 字节码

4.3.1 eBPF 字节码使用 bpf_insn 结构来表示,如下:

代码语言:c复制
struct bpf_insn {
    __u8    code;       // 操作码
    __u8    dst_reg:4;  // 目标寄存器
    __u8    src_reg:4;  // 源寄存器
    __s16   off;        // 偏移量
    __s32   imm;        // 立即操作数
};

4.3.2 bpf_insn 结构各个字段的作用:

  1. code:指令操作码,如 mov、add 等。
  2. dst_reg:目标寄存器,用于指定要操作哪个寄存器。
  3. src_reg:源寄存器,用于指定数据来源于哪个寄存器。
  4. off:偏移量,用于指定某个结构体的成员。
  5. imm:立即操作数,当数据是一个常数时,直接在这里指定。 eBPF 程序会被 LLVM/Clang 编译成 bpf_insn 结构数组,当内核要执行 eBPF 字节码时,会调用 __bpf_prog_run() 函数来执行。 如果开启了 JIT(即时编译技术),内核会将 eBPF 字节码编译成本地机器码(Native Code)。这样就可以直接执行,而不需要虚拟机来执行。

4.4 agent是怎样拿到本机的socket句柄信息的?

socket 句柄是通过 hook 系统调用获取 (得到socket fd),socket相关信息是通过 get_socket_from_fd() 得到 socket address,相关 socket 信息(比如 元组信息,tcpseq number 等)是通过 socket 内核结构读取的.

5 tracepoint

syscalls::sys_enter_write

代码语言:c复制
TPPROG(sys_enter_write)
先把系统调用写到map里
代码语言:c复制
TPPROG(sys_exit_write)
从map里读出来处理
--> process_syscall_data
--> process_data
      |-- src/ebpf/kernel/include/task_struct_utils.h
           get_socket_from_fd 在这里判断是不是socket

0 人点赞