前言

咱们在《用eBPF/XDP来代替LVS》系列、《一张图感触实在的 TCP 状态转移》系列,以及《如何终结已存在的TCP连贯?》系列文章中,均通过纯 C 语言和 libbpf1 这个库来使用 eBPF。

然而很多的场景中(尤其是云原生场景),咱们出于防止反复造轮子、更快的迭代速度、运行时平安等起因,会抉择 go 语言来进行开发,ebpf-go2 这个库就是以后最好的抉择。

明天,咱们就对 ebpf-go 进行一个初体验,这个体验不是循序渐进的 API 文档,而是通过一个简略的需要,让大家失去一个真切的感触,这个需要就是:统计发向本机的每个 “连贯” 的包数量,并且每新增 5 个 “连贯” 就进行一次数据展现。

体验

依赖

本次体验须要许多前置条件:

  • Linux kernel 版本 5.7 以上,以反对 bpf_link(我是 6.6.5)
  • LLVM 版本 11 以上 (clang and llvm-strip,查看命令 clang --version )
  • libbpf headers (Debian/Ubuntu 是 libbpf-dev,Fedora 是 libbpf-devel )
  • Linux kernel headers (Debian/Ubuntu 是 linux-headers-amd64,Fedora 是 kernel-devel )
  • Go compiler 版本须要反对 ebpf-go (我装置了 GO 1.21,查看命令 go version)

我的项目初始化

# 创立我的项目mkdir ebpf-go-exp && cd ebpf-go-expgo mod init ebpf-go-expgo mod tidy# 手动引入依赖go get github.com/cilium/ebpf/cmd/bpf2go
如果依赖下载超时的话,能够设置下代理:go env -w GOPROXY=https://goproxy.cn,direct

代码

C 代码

...//go:build ignorestruct event {    __u32 count;};const struct event *unused __attribute__((unused));SEC("xdp") int count_packets(struct xdp_md *ctx) {    __u32 ip;    __u16 sport;    __u16 dport;    if (!parse_ip_src_addr(ctx, &ip, &sport, &dport)){        goto done;    }    __u16 r_sport = bpf_ntohs(sport);    bpf_printk("Process a packet of tuple from %u|%pI4n:%u|%u",ip,&ip,sport,r_sport);    if(8080 != bpf_ntohs(dport)){        goto done;    }        struct tuple key;    __builtin_memset(&key,0,sizeof(key));    key.addr = ip;    key.port = sport;    __u32 *pkt_count = bpf_map_lookup_elem(&pkt_count_map, &key);    if (!pkt_count) {        __u32 init_pkt_count = 1;        bpf_map_update_elem(&pkt_count_map, &key, &init_pkt_count, BPF_NOEXIST);        __u32 key    = 0;         __u64 *count = bpf_map_lookup_elem(&tuple_num, &key);         if (count) {             __sync_fetch_and_add(count, 1);             if(*count % 5 == 0){                struct event *e;                e = bpf_ringbuf_reserve(&events, sizeof(struct event), 0);                if (e){                    e->count = *count;                    bpf_ringbuf_submit(e, 0);                }            }        }    } else {        __sync_fetch_and_add(pkt_count, 1);    }done:    return XDP_PASS; }...

Go 代码

...//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event counter counter.c -- -I headersfunc main() {    // Load the compiled eBPF ELF and load it into the kernel.    var objs counterObjects    if err := loadCounterObjects(&objs, nil); err != nil {        log.Fatal("Loading eBPF objects:", err)    }    defer objs.Close()    ifname := "lo"    iface, err := net.InterfaceByName(ifname)    if err != nil {        log.Fatalf("Getting interface %s: %s", ifname, err)    }    // Attach count_packets to the network interface.    link, err := link.AttachXDP(link.XDPOptions{        Program:   objs.CountPackets,        Interface: iface.Index,    })    if err != nil {        log.Fatal("Attaching XDP:", err)    }    defer link.Close()    rd, err := ringbuf.NewReader(objs.Events)    if err != nil {        log.Fatalf("opening ringbuf reader: %s", err)    }    defer rd.Close()    stopper := make(chan os.Signal, 1)    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)    go func() {        <-stopper        if err := rd.Close(); err != nil {            log.Fatalf("closing ringbuf reader: %s", err)        }    }()    log.Println("Waiting for events..")    // counterEvent is generated by bpf2go.    var event counterEvent    for {        record, err := rd.Read()        if err != nil {            if errors.Is(err, ringbuf.ErrClosed) {                log.Println("Received signal, exiting..")                return            }            log.Printf("reading from reader: %s", err)            continue        }        if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {            log.Printf("parsing ringbuf event: %s", err)            continue        }        log.Printf("tuple num: %d", event.Count)        var (            key counterTuple            val uint32        )        iter := objs.PktCountMap.Iterate()        for iter.Next(&key, &val) {            sourceIP := key.Addr            sourcePort := key.Port            packetCount := val            log.Printf("%d/%s:%d => %d\n", sourceIP, int2ip(sourceIP), sourcePort, packetCount)        }    }}...

残缺代码在:https://github.com/MageekChiu/epbf-go-exp

运行

# 生成脚手架代码go generate# 利用生成的 GO 代码,进行编译和运行go build && sudo ./ebpf-go-exp

本机运行一个 openresty 并监听 8080 端口,而后重复拜访测试

openrestycurl localhost:8080curl localhost:8080...

查看日志

# bpf 输入bpftool prog tracelog bpf_trace_printk: Process a packet of tuple from 16777343|127.0.0.1:31876|33916 ... bpf_trace_printk: Process a packet of tuple from 16777343|127.0.0.1:56449|33244 ...# go 输入Waiting for events..tuple num: 516777343/127.0.0.1:31876 => 716777343/127.0.0.1:31364 => 716777343/127.0.0.1:56449 => 116777343/127.0.0.1:10909 => 716777343/127.0.0.1:30340 => 7...

若迭代过程中,C 代码有变动,则须要执行 go generate,否则仅执行 Go 编译局部即可。

重点解析

  1. c 代码主体和咱们之前系列的文章相似,就是在 xdp hook 点解析出每个报文的四元组,而后存入 pkt_count_maptuple_num 这个 map 作为全局变量,通过 __sync_fetch_and_add 平安地并发,每当新增 5 个 “连贯” 就向用户空间发送事件。
  2. c 代码中 unused 这个 event 指针必须申明,不然用户态的 go 就拿不到这个数据结构。
  3. c 代码中开始的 ignore 是必须的,防止 go build 报错: C source files not allowed when not using cgo or SWIG
  4. go generate 命令会依据 go 代码中的 go:generate 语句生成脚手架代码,就像 BPF Skeleton3,而后咱们就能够在 go 代码中便捷地拜访 c 中定义的 map,prog 以及一些数据结构。-type event counter 使得 c 中定义的 event 构造体在 go 中就是 counterEvent 构造体。
  5. 内核空间向用户空间发送数据/事件能够通过 perfbuf 和 ringbuf 实现,从而防止用户空间轮询数据。这两者尽管性能有不少差异,然而api都差不多4。当咱们收到内核的告诉后,通过 Iterate 来遍历统计数据的 map,实现一种相似于推拉联合的架构。

小插曲

如果咱们的 pkt_count_map 这样写的话:

struct tuple key = {ip,bpf_ntohs(sport)};__u32 *pkt_count = bpf_map_lookup_elem(&pkt_count_map, &key);if (!pkt_count) {    ...}else{    ...}

就可能会得出一个看起来很奇怪的后果:

Waiting for events..tuple num: 516777343/127.0.0.1:60162 => 316777343/127.0.0.1:45076 => 316777343/127.0.0.1:45082 => 316777343/127.0.0.1:60162 => 416777343/127.0.0.1:45076 => 4

以及

bpftool map dump name pkt_count_map[{        "key": {            "addr": 16777343,            "port": 60162        },        "value": 3    },{        "key": {            "addr": 16777343,            "port": 45082        },        "value": 3    },{        "key": {            "addr": 16777343,            "port": 45076        },        "value": 3    },{        "key": {            "addr": 16777343,            "port": 45082        },        "value": 4    },{        "key": {            "addr": 16777343,            "port": 60162        },        "value": 4    },{        "key": {            "addr": 16777343,            "port": 45076        },        "value": 4    }]

乍一看你会发现 map 的 key 怎么有些反复了?这里先卖个关子,前面有机会再来剖析。

总结

本文咱们理解 ebpf-go 的一些常见用法,让大家对 ebpf-go 有了一个含糊但整体的意识,更多的细节,能够通过官网的文档5以及 examples6 进行理解。

下一篇文章,咱们就从实战的角度,看看 cilium7 是怎么通过 ebpf-go 来施展 ebpf 威力的。

参考


  1. https://libbpf.readthedocs.io/ ↩
  2. https://ebpf-go.dev/about/ ↩
  3. https://www.kernel.org/doc/html/next/bpf/libbpf/libbpf_overvi... ↩
  4. https://nakryiko.com/posts/bpf-ringbuf/#bpf-ringbuf-vs-bpf-pe... ↩
  5. https://ebpf-go.dev/guides/getting-started/ ↩
  6. https://github.com/cilium/ebpf/tree/main/examples ↩
  7. https://docs.cilium.io/en/stable/overview/intro/ ↩