关于开源:龙蜥社区开源-coolbpfBPF-程序开发效率提升百倍

6次阅读

共计 10113 个字符,预计需要花费 26 分钟才能阅读完成。

文 / 零碎运维 SIG(Special Interest Group)

引言

BPF 是一个新的动静跟踪技术,目前这项技术正在粗浅的影响着咱们的生产和生存。BPF 在四大利用场景施展着巨大作用:

  • 零碎故障诊断:它能够动静插桩透视内核。
  • 网络性能优化:它能够对接管和发送的网络包做批改和转发。
  • 系统安全:它能够监控文件关上和敞开从而做出平安决策等。
  • 性能监控:它能够查看函数消耗工夫从而晓得性能瓶颈点。

BPF 技术也是随着 Linux 内核的倒退而倒退的,Linux 内核版本经验了 3.x 向 4.x 到 5.x 演进,eBPF 技术的反对也是从 4.x 开始更加欠缺起来,特地是 5.x 内核也减少了十分多的高级个性。然而云上服务器有大量的 3.10 内核版本是不反对 eBPF 的,为了让咱们现有的 eBPF 工具在这些存量机器得以运行,咱们移植了 BPF 到低版本内核,同时基于 libbpf 的 CO-RE 能力,保障一个工具可运行在 3.x/4.x/5.x 的低、中、高内核版本。

BPF 的开发方式有很多,以后比拟热门的有:

1)纯 libbpf 利用开发:借助 libbpf 库加载 BPF 程序到内核的形式:这种开发方式不仅效率低,没有根底库封装,所有必备步骤和根底函数都须要本人摸索。

2)借助 BCC 等开源我的项目:开发效率高、可移植性好,并且反对动静批改内核局部代码,非常灵活。但存在部署依赖 Clang/LLVM 等库;每次运行都要执行 Clang/LLVM 编译,重大耗费 CPU、内存等资源,容易与其它服务争抢。

coolbpf 我的项目,以 CO-RE(Compile Once-Run Everywhere)为根底实现,保留了资源占用低、可移植性强等长处,还交融了 BCC 动静编译的个性,适宜在生产环境批量部署所开发的利用。coolbpf 创始了一个新的思路,利用近程编译的思维,把用户的 BPF 程序推送到远端的服务器并返回给用户.o 或.so,提供高级语言如 Python/Rust/Go/C 等进行加载,而后在全量内核版本平安运行。用户只需专一本人的性能开发,不必关怀底层库(如 LLVM、python 等)装置、环境搭建,给宽广 BPF 爱好者提供一种新的摸索和实际。

一、BPF 开发方式比照

BPF 经验了传统的 setsockopt 形式的 sock filter 报文过滤,到现在应用 libbpf CO-RE 形式进行监控和诊断性能的开发,是和 eBPF 与硬件紧密结合的优良的指令集能力及 libbpf 通用库的开源凋谢分不开的,让咱们一起回顾一下 BPF 的开发方式,并在此基础上推出基于近程编译思维为外围的 coolbpf,它站在了伟人的肩膀上,进行了资源优化、简洁编程和效率晋升。

1、原始阶段

在 BPF 还叫伯克利报文过滤 (cBPF) 的时候,它通过 sock filter 将原始的 BPF 指令码,利用 setsockopt 加载到内核,通过 setsockopt 加载到内核,通过在 packet_rcv 调用 runfilter 运行这段程序来进行报文过滤。这种形式,BPF 字节码的生成十分原始,相似于手工编写汇编程序,过程是十分苦楚的。

static struct sock_filter filter[6] = {{ OP_LDH, 0, 0, 12}, // ldh [12]
 {OP_JEQ, 0, 2, ETH_P_IP}, // jeq #0x800, L2, L5
 {OP_LDB, 0, 0, 23}, // ldb [23]
 {OP_JEQ, 0, 1, IPPROTO_TCP}, // jeq #0x6, L4, L5
 {OP_RET, 0, 0, 0}, // ret #0x0
 {OP_RET, 0, 0, -1,}, // ret #0xffffffff
};
int main(int argc, char **argv)
{
…
 struct sock_fprog prog = {6, filter};
 …
 sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
 …
 if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog))) {return 1;}
…
 }

2、激进阶段

例子为 samples/bpf 上面的 sockex1_kern.c 和 sockex1_user.c,代码分为两局部,通常命名为 xxx_kern.c 和 xxx_user.c,前者加载到内核空间中执行,后者在用户空间执行。BPF 程序编写实现后就通过 Clang/LLVM 进行编译,xxx_user.c 里显式的去加载生成的 xxx_kernel.o 文件。这种形式尽管应用了编译器反对主动生成了 BPF 字节码,但代码组织和 BPF 加载形式比拟激进,用户须要写十分多的反复代码。

struct {__uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, u32);
    __type(value, long);
    __uint(max_entries, 256);
} my_map SEC(".maps");

SEC("socket1")
int bpf_prog1(struct __sk_buff *skb)
{int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
    long *value;

    if (skb->pkt_type != PACKET_OUTGOING)
        return 0;

    value = bpf_map_lookup_elem(&my_map, &index);
    if (value)
        __sync_fetch_and_add(value, skb->len);

    return 0;
}
char _license[] SEC("license") = "GPL";
int main(int ac, char **argv)
{
    struct bpf_object *obj;
    struct bpf_program *prog;
    int map_fd, prog_fd;
    char filename[256];
    int i, sock, err;
    FILE *f;

    snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);

    obj = bpf_object__open_file(filename, NULL);
    if (libbpf_get_error(obj))
        return 1;

    prog = bpf_object__next_program(obj, NULL);
    bpf_program__set_type(prog, BPF_PROG_TYPE_SOCKET_FILTER);

    err = bpf_object__load(obj);
    if (err)
        return 1;

    prog_fd = bpf_program__fd(prog);
    map_fd = bpf_object__find_map_fd_by_name(obj, "my_map");
    ...
 }

3、BCC 初始阶段

BCC 的呈现突破了激进的开发方式,杰出的运行时编译和根底库封装能力,极大的升高了开发难度,有了不少迷妹,而后开始攻城略地,相似资本的疾速扩张。用户只须要在 Python 程序里 attach 一段 prog,而后进行数据分析和解决,毛病是必须在生产环境上装置 Clang 和 python 库,运行时有 CPU 资源刹时冲高,导致呈现加载 BPF 程序后问题不复现的可能。

int trace_connect_v4_entry(struct pt_regs *ctx, struct sock *sk)
{if (container_should_be_filtered()) {return 0;}

  u64 pid = bpf_get_current_pid_tgid();
  ##FILTER_PID##
  u16 family = sk->__sk_common.skc_family;
  ##FILTER_FAMILY##

  // stash the sock ptr for lookup on return
  connectsock.update(&pid, &sk);

  return 0;
}
# initialize BPF
b = BPF(text=bpf_text)
if args.ipv4:
    b.attach_kprobe(event="tcp_v4_connect", fn_name="trace_connect_v4_entry")
    b.attach_kretprobe(event="tcp_v4_connect", fn_name="trace_connect_v4_return")
b.attach_kprobe(event="tcp_close", fn_name="trace_close_entry")
b.attach_kretprobe(event="inet_csk_accept", fn_name="trace_accept_return")

4、BCC 高级阶段

BCC 风行一时,俘获了不少开发者。因为时代在提高,需要也在变。libbpf 横空出世及 CO-RE 思维流行,BCC 本人也在改革,开始借助 BTF 的形式反对重定位,心愿同一套程序在任何 Linux 零碎都能顺利运行。然而,有些构造体在不同内核版本上,或者成员名字变了、或者成员的含意变了(从微秒变成了毫秒),这种形式就须要程序处理。在 4.x 等中版本内核上,还须要通过 debuginfo 生成独立的 BTF 文件,过程还是相当简单。

SEC("kprobe/inet_listen")
int BPF_KPROBE(inet_listen_entry, struct socket *sock, int backlog)
{__u64 pid_tgid = bpf_get_current_pid_tgid();
    __u32 pid = pid_tgid >> 32;
    __u32 tid = (__u32)pid_tgid;
    struct event event = {};

    if (target_pid && target_pid != pid)
        return 0;

    fill_event(&event, sock);
    event.pid = pid;
    event.backlog = backlog;
    bpf_map_update_elem(&values, &tid, &event, BPF_ANY);
    return 0;
}
#include "solisten.skel.h"
...
int main(int argc, char **argv)
{
    ...
    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    libbpf_set_print(libbpf_print_fn);

    obj = solisten_bpf__open();
    obj->rodata->target_pid = target_pid;
    err = solisten_bpf__load(obj);
    err = solisten_bpf__attach(obj);
    pb = perf_buffer__new(bpf_map__fd(obj->maps.events), PERF_BUFFER_PAGES,
                  handle_event, handle_lost_events, NULL, NULL);
    ...
}

5、资源共享阶段

BCC 尽管也反对了 CO-RE,然而依然存在代码绝对固定,无奈动静配置的问题,同时还须要搭建编译工程。coolbpf 把编译资源放到一台服务器上,提供近程编译能力,大家共享近程服务器资源,只须要把 bpf.c 推送到远端服务器,这台服务器会开动马达,减速输入 .o 和 .so。不论用户应用 Python 还是 Go 语言、Rust 或 C 语言,只须要在程序 ini t 的时候加载这些 .o 或 .so 就能够把 BPF 程序 attach 到内核的 hook 点,而后专一于解决来自 BPF 程序输入的信息,进行性能开发。

coolbpf 把 BTF 制作、代码编译、数据处理、功能测试集一身,生产效率大幅晋升,使 BPF 开发进入一个更优雅境界:

  • 开箱即用:内核侧仅提供 bpf.c 即可,齐全剥离出内核编译工程。
  • 复用编译成绩:本地侧无编译过程,不存在库依赖和 CPU、内存等资源耗费问题。
  • 自适应不同版本差别:更适宜在集群多个不同内核版本共存的场景。
先在本地装置 coolbpf,外面带的命令会把 xx.bpf.c 发送到编译服务器编译。pip install coolbpf

...
import time
from pylcc.lbcBase import ClbcBase

bpfPog = r"""#include"lbc.h"SEC("kprobe/wake_up_new_task")
int j_wake_up_new_task(struct pt_regs *ctx)
{struct task_struct* parent = (struct task_struct *)PT_REGS_PARM1(ctx);

bpf_printk("hello lcc, parent: %d\n", _(parent->tgid));
return 0;
}

char _license[] SEC("license") = "GPL";
"""

class Chello(ClbcBase):
    def __init__(self):
        super(Chello, self).__init__("hello", bpf_str=bpfPog)
        while True:
            time.sleep(1)
            
            if __name__ == "__main__":
                hello = Chello()
    pass

二、coolbpf 性能及架构

后面剖析了 BPF 的开发方式,coolbpf 借助近程编译把开发和编译这个过程进一步优化,总结一下它以后蕴含的 6 大性能:

1)本地编译服务,根底库封装:客户应用本地容器镜像编译程序,调用封装的通用函数库简化程序编写和数据处理。

本地编译服务,把同样的库和常用工具放在容器镜像里,编译时间接到容器外面编译。咱们应用如下镜像进行编译,用户也能够通过 docker 本人搭建容器镜像。

容器镜像:registry.cn-hangzhou.aliyuncs.com/alinux/coolbpf:latest

用户能够 pull 这个镜像进行本地编译,一些罕用的库和工具,通过咱们提供的镜像就曾经蕴含在外面,省去了构建环境的繁冗。

2)近程编译服务:接管 bpf.c,生成 bpf.so 或 bpf.o,提供给高级语言进行加载,用户只专一本人的性能开发,不必关怀底层库装置、环境搭建。

近程编译服务,目前用户开发代码时只须要 pip install coolbpf,程序就会主动到咱们的编译服务器进行编译。你也能够参考 compile/remote-compile/lbc/ 本人搭建编译服务器(咱们前面会陆续开源这个编译服务器源码),过程可能会比较复杂。这样搭建好的服务器,你能够集体应用或者在公司提供给大家一起应用。

3)高版本个性通过 kernel module 形式补齐到低版本,如 ring buffer 个性,backport BPF 个性到 3.10 内核。

因为存量 3.10 内核的服务器仍然很多,为了让同一个 BPF 程序也能运行在低版本内核,为了保护不便且不必批改程序代码,只须要 install 一个 ko,就能够反对 BPF,让低版本也享受到了 BPF 的红利。

4)BTF 的主动生成和全网最新内核版本爬虫。主动发现最新的 CentOS、ubuntu、Anolis 等内核版本,主动生成对应的 BTF。

要具备一次编译多处运行 CO-RE 能力,没有 BTF 是行不通的。coolbpf 不仅提供一个制作 BTF 的工具,还会主动发现和制作最新内核版本的 BTF,以供大家下载和应用。

5)各内核版本功能测试自动化,工具编写后主动进行装置测试,保障用户性能在生产环境运行前预测试。

没有上线运行过的 BPF 程序和工具,肯定概率上是存在危险的。coolbpf 提供一套自动化测试流程,在大部分内核环境都事后进行根本的功能测试,保障工具真正运行在生产环境时不会出大问题。

6)Python、Rust、Go、C 等高级语言反对。

目前 coolbpf 我的项目反对应用 Python、Rust、Go 及 C 语言的用户程序开发,不同语言开发者都能在本人最善于的畛域施展最大的劣势。

总之,coolbpf 使得 BPF 程序和利用程序开发在一个平台上闭环解决了,无效晋升了生产力,笼罩了以后支流的开发语言,适宜更多的 BPF 爱好者入门学习,也适宜零碎运维人员高效开发监控和诊断程序。

下图为 coolbpf 的性能和工具反对状况,欢送更多优良 BPF 工具退出:

三、实际阐明

coolbpf 目前蕴含 pylcc、rlcc、golcc 和 clcc,以及 glcc 子目录,别离是高级语言 Python、Rust 和 Go 语言反对近程和本地编译的能力,glcc(g 代表 generic)是通过将高版本的 BPF 个性移植到低版本,通过 kernel module 的形式在低版本上运行。上面咱们别离简略介绍它的应用。

1、pylcc(基于 Python 的 LCC)

pylcc 在 libbpf 根底上进行封装,将简单的编译工程交由容器执行。

代码编写十分简洁,只须要三步就能实现,pyLCC 技术关键点:

1)执行 pip install coolbpf 装置

2)xx.bpf.c 的编写:

bpfPog = r"""#include"lbc.h"
LBC_PERF_OUTPUT(e_out, struct data_t, 128);
LBC_HASH(pid_cnt, u32, u32, 1024);
LBC_STACK(call_stack,32);

3)xx.py 编写,只须要这一步,程序就能够运行起来。用户关注从内核收到的数据进行剖析就能够:

importtimefrompylcc.lbcBaseimportClbcBase
classPingtrace(ClbcBase):def__init__(self):super(Pingtrace, self).__init__("pingtrace")

bpf.c 里须要被动蕴含 lbc.h,它告知近程服务器的行为,本地不须要有这个文件。其内容如下:

#include "vmlinux.h"
#include <linux/types.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_tracing.h>

2、rlcc(基于 Rust 的 LCC)

Rust 语言反对近程编译和本地编译的能力。通过在 makefile 中应用 coolbpf 的命令把 bpf.c 发送到服务端,服务端返回 .o,这个与 Python 和 C 返回 .so 有很大区别,Rust 本人解决通用的 load、attach 的过程。其余相似于 Python 的开发,不再赘述。

编译 example 流程:
SKEL_RS=1 cargo build --release 生成 rust skel 文件;
SKEL_RS=0 cargo build --release 无需在生成 rust skel 文件;
默认 SKEL_RS 为 1.

编译 rexample 流程:
rexample 应用了近程编译性能,具体编译流程如下:
运行命令 mkdir build & cd build 创立编译目录;
运行命令 cmake .. 生成 Makefile 文件;
运行命令 make rexample;
运行 example 程序: ../lcc/rlcc/rexample/target/release/rexample.
fn main() -> Result<()>{let opts = Command::from_args();
    let mut skel_builder = ExampleSkelBuilder::default();
    if opts.verbose {skel_builder.obj_builder.debug(true);
    }
    
    bump_memlock_rlimit()?;
    let mut open_skel = skel_builder.open()?;
    
    let mut skel = open_skel.load()?;
    skel.attach()?;
    let perf = PerfBufferBuilder::new(skel.maps_mut().events())
    .sample_cb(handle_event)
    .lost_cb(handle_lost_events)
    .build()?;
    
    loop {perf.poll(Duration::from_millis(100))?;
    }
}

3、glcc(generic LCC,高版本个性移植到低版本)

背景:

  • 目前基于 eBPF 编写的程序只能在高版本内核(反对 eBPF 的内核)上运行,无奈在不反对 eBPF 性能的内核上运行。
  • 线上有很多 Alios 或者 CentOS 低版本内核须要保护。
  • 存量 BPF 工具或我的项目代码,心愿不做批改能跨内核运行。

为此咱们提出了一种在低版本内核运行 eBPF 程序的办法,使得二进制程序无需任何批改即可在不反对 BPF 的内核上运行。

上面从架构上梳理,低版本内核运行 BPF 的可能。

Hook 是一个动静库,因为低版本内核不反对 bpf() 的零碎调用,原来在用户态创立 map、创立 prog 以及很多 helper 函数(如 bpf_update_elem 等)将不能运行,Hook 提供一个动静机制,把这些零碎调用转成 ioctl 命令,设置到一个叫 ebpfdriver 的 kernel module,通过他进行创立一些数据结构模仿 map 和 prog,同时注册 kprobe 和 tracepoint 的 handler。这样有数据到来时,就会运行注册在 kprobe 和 tracepoint 的回调。

运行机制见下图:

利用 Hook 程序将 BPF 的 syscall 转换成 ioctl 模式,将零碎调用参数传递给 eBPF 驱动,蕴含以下性能:

#define IOCTL_BPF_MAP_CREATE _IOW(';', 0, union bpf_attr *)
#define IOCTL_BPF_MAP_LOOKUP_ELEM _IOWR(';', 1, union bpf_attr *)
#define IOCTL_BPF_MAP_UPDATE_ELEM _IOW(';', 2, union bpf_attr *)
#define IOCTL_BPF_MAP_DELETE_ELEM _IOW(';', 3, union bpf_attr *)
#define IOCTL_BPF_MAP_GET_NEXT_KEY _IOW(';', 4, union bpf_attr *)
#define IOCTL_BPF_PROG_LOAD _IOW(';', 5, union bpf_attr *)
#define IOCTL_BPF_PROG_ATTACH _IOW(';', 6, __u32)
#define IOCTL_BPF_PROG_FUNCNAME _IOW(';', 7, char *)
#define IOCTL_BPF_OBJ_GET_INFO_BY_FD _IOWR(';', 8, union bpf_attr *)

eBPF 驱动收到 Ioctl 申请,会依据 cmd 来进行相应的操作,如:

A. IOCTL_BPF_MAP_CREATE:创立 map。

B. IOCTL_BPF_PROG_LOAD:加载 eBPF 字节码,进行字节码的平安验证和 jit 生成机器码。

C. IOCTL_BPF_PROG_ATTACH:将该 eBPF 程序 attach 到指定的内核函数,利用 register_kprobe 和 tracepoint_probe_register 性能实现 eBPF 程序的 attach。

另外,高版本的一些个性,比方 ringbuff,也能够通过 ko 等形式用在低版本。像 clcc 和 golcc 的应用形式,请参考 coolbpf 的 github 链接(见文末),这里不在赘述。

四、总结

coolbpf 以后具备以上 6 大性能,其目标是简化开发和编译过程,让用户专一本人的性能开发,使得宽广 BPF 爱好者疾速入门,疾速编写本人的性能程序而不必放心环境问题。明天咱们把这套零碎开源,让它服务更多人,以晋升他们的生产力,促成社会提高,让更多人参加到这个我的项目建设中来,造成一股合力,冲破一项技术。

咱们的近程编译服务,解决的是生产力的效率问题;低版本的 BPF 反对,解决的是困扰各个开发者的同一个 bin 文件如何在多内核版本无差别运行的目标,同时也心愿更多人参加进来共同提高,让云计算产业和企业服务的兄弟姐妹们全面享受到 BPF 技术的红利。

龙蜥社区零碎运维 SIG(Special Interest Group)致力于打造一个集主机治理、配置部署、监控报警、异样诊断、平安审计等一系列性能的自动化运维平台,coolbpf 是社区的一个子项目,指标是提供一个编译和开发平台,解决 BPF 在不同零碎平台的运行和生产效率晋升问题。

欢送更多开发者退出零碎运维 SIG:

网址:https://openanolis.cn/sig/sysom

邮件列表:sysom@lists.openanolis.cn

coolbpf 链接:git@github.com:aliyun/coolbpf.git

—— 完 ——

正文完
 0