2. 基于eBPF的CNI网络延迟测量

任务目标

在CNI网络数据路径上,通过eBPF程序挂载到TC(Traffic Control)入口点,为每个经过的数据包打上时间戳,并在数据包离开网络命名空间时计算延迟。最终输出延迟分布直方图,用于观测CNI网络路径的转发性能。

环境依赖

  • Linux内核 ≥ 5.4(支持BPF_PROG_TYPE_SCHED_CLS和BPF_MAP_TYPE_PERCPU_HASH)
  • CNI网络已配置(本案例使用veth pair模拟容器网络路径)
  • 工具链:clang、llvm-strip、libbpf-dev、iproute2

实现原理

在veth设备的ingress方向挂载eBPF程序,记录数据包元组(源IP、目的IP、协议、端口)到哈希表,并存储到达时间戳。在veth设备的egress方向挂载另一个eBPF程序,查询同一元组并计算时间差(延迟)。用户态程序周期读取累积的延迟值,更新直方图。

关键完整代码

// packet_latency_kern.c
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

struct pkt_key {
    __u32 src_ip;
    __u32 dst_ip;
    __u16 src_port;
    __u16 dst_port;
    __u8 proto;
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 65536);
    __type(key, struct pkt_key);
    __type(value, __u64);  // 到达时间戳 (ns)
} ingress_map SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 32);  // 延迟分桶: 0-31
    __type(key, __u32);
    __type(value, __u64);     // 计数
} lat_histogram SEC(".maps");

// 延迟分桶边界 (us): 0,10,20,50,100,200,500,1000,2000,5000,10000,20000,50000,100000,200000,500000,1000000
static __always_inline int get_bucket(__u64 delta_ns) {
    __u64 delta_us = delta_ns / 1000;
    if (delta_us < 10) return 0;
    if (delta_us < 20) return 1;
    if (delta_us < 50) return 2;
    if (delta_us < 100) return 3;
    if (delta_us < 200) return 4;
    if (delta_us < 500) return 5;
    if (delta_us < 1000) return 6;
    if (delta_us < 2000) return 7;
    if (delta_us < 5000) return 8;
    if (delta_us < 10000) return 9;
    if (delta_us < 20000) return 10;
    if (delta_us < 50000) return 11;
    if (delta_us < 100000) return 12;
    if (delta_us < 200000) return 13;
    if (delta_us < 500000) return 14;
    if (delta_us < 1000000) return 15;
    return 16;  // ≥1s
}

static __always_inline int parse_pkt(struct __sk_buff *skb, struct pkt_key *key) {
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end) return -1;
    if (eth->h_proto != bpf_htons(ETH_P_IP)) return -1;

    struct iphdr *ip = (void *)eth + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end) return -1;
    key->src_ip = ip->saddr;
    key->dst_ip = ip->daddr;
    key->proto = ip->protocol;

    if (ip->protocol == IPPROTO_TCP) {
        struct tcphdr *tcp = (void *)ip + sizeof(*ip);
        if ((void *)tcp + sizeof(*tcp) > data_end) return -1;
        key->src_port = tcp->source;
        key->dst_port = tcp->dest;
    } else if (ip->protocol == IPPROTO_UDP) {
        struct udphdr *udp = (void *)ip + sizeof(*ip);
        if ((void *)udp + sizeof(*udp) > data_end) return -1;
        key->src_port = udp->source;
        key->dst_port = udp->dest;
    } else {
        key->src_port = 0;
        key->dst_port = 0;
    }
    return 0;
}

SEC("tc")
int ingress_timestamp(struct __sk_buff *skb) {
    struct pkt_key key = {};
    if (parse_pkt(skb, &key) != 0) return TC_ACT_OK;
    __u64 now = bpf_ktime_get_ns();
    bpf_map_update_elem(&ingress_map, &key, &now, BPF_ANY);
    return TC_ACT_OK;
}

SEC("tc")
int egress_latency(struct __sk_buff *skb) {
    struct pkt_key key = {};
    if (parse_pkt(skb, &key) != 0) return TC_ACT_OK;
    __u64 *t0 = bpf_map_lookup_elem(&ingress_map, &key);
    if (!t0) return TC_ACT_OK;

    __u64 now = bpf_ktime_get_ns();
    __u64 delta = now - *t0;
    bpf_map_delete_elem(&ingress_map, &key);

    __u32 bucket = get_bucket(delta);
    __u32 zero = 0;
    __u64 *cnt = bpf_map_lookup_elem(&lat_histogram, &zero);
    if (cnt) {
        __sync_fetch_and_add(cnt + bucket, 1);
    }
    return TC_ACT_OK;
}

char _license[] SEC("license") = "GPL";
// user_space.c (核心逻辑)
#include <bpf/libbpf.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

static volatile int stop = 0;
static void sig_handler(int sig) { stop = 1; }

int main(int argc, char **argv) {
    struct bpf_object *obj = NULL;
    struct bpf_program *prog_ingress, *prog_egress;
    struct bpf_map *map_hist;
    int ifindex = 0;
    if (argc > 1) ifindex = atoi(argv[1]);
    if (ifindex <= 0) {
        fprintf(stderr, "Usage: %s <ifindex>\n", argv[0]);
        return 1;
    }

    obj = bpf_object__open_file("packet_latency_kern.o", NULL);
    if (libbpf_get_error(obj)) { fprintf(stderr, "open obj failed\n"); return 1; }
    if (bpf_object__load(obj)) { fprintf(stderr, "load obj failed\n"); return 1; }

    prog_ingress = bpf_object__find_program_by_name(obj, "ingress_timestamp");
    prog_egress = bpf_object__find_program_by_name(obj, "egress_latency");
    map_hist = bpf_object__find_map_by_name(obj, "lat_histogram");
    if (!prog_ingress || !prog_egress || !map_hist) {
        fprintf(stderr, "find prog/map failed\n");
        return 1;
    }

    // 挂载到同一veth的ingress和egress
    if (bpf_program__attach_tcx(prog_ingress, ifindex, BPF_TCX_INGRESS) ||
        bpf_program__attach_tcx(prog_egress, ifindex, BPF_TCX_EGRESS)) {
        fprintf(stderr, "attach failed\n");
        return 1;
    }

    signal(SIGINT, sig_handler);
    printf("Measuring latency on ifindex %d... Ctrl-C to stop\n", ifindex);

    while (!stop) {
        sleep(2);
        // 打印直方图
        printf("\n--- Latency Histogram (counts) ---\n");
        const char *labels[] = {"<10us","10-20us","20-50us","50-100us","100-200us",
            "200-500us","500us-1ms","1-2ms","2-5ms","5-10ms","10-20ms","20-50ms",
            "50-100ms","100-200ms","200-500ms","500ms-1s","≥1s"};
        for (int i = 0; i < 17; i++) {
            __u32 key = 0;
            __u64 vals[64] = {};
            if (bpf_map__lookup_percpu_elem(map_hist, &key, i, vals, sizeof(vals)) == 0) {
                __u64 total = 0;
                for (int c = 0; c < 64; c++) total += vals[c];
                if (total > 0)
                    printf("%-12s : %llu\n", labels[i], (unsigned long long)total);
            }
        }
    }

    bpf_object__close(obj);
    return 0;
}

编译与运行

# 编译内核态
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 -c packet_latency_kern.c -o packet_latency_kern.o

# 编译用户态
gcc -O2 -o latency_measure user_space.c -lbpf

# 获取veth对端接口索引(假设容器侧veth为veth0)
ip link show veth0   # 记录ifindex,例如 5

# 运行测量(需要root)
sudo ./latency_measure 5

验证方法

  1. 在容器内持续发送UDP/TCP流量:iperf3 -c 10.0.0.2 -u -b 100M -t 30(或使用ping -f产生ICMP流量)。
  2. 观察用户态程序每2秒打印的直方图,确认有数据包计数分布在各延迟区间。
  3. 通过条件:直方图中出现非零计数,且大部分计数集中在低延迟区间(如<100us),说明测量路径生效。
  4. 停止程序(Ctrl-C)后,检查无eBPF程序残留:tc filter show dev veth0 ingress 应无输出。

常见问题

  • 挂载失败:确认内核支持BPF_TCX(5.15+),或改用tc filter add dev veth0 ingress bpf da obj packet_latency_kern.o sec tc传统方式。
  • 无计数:检查veth对端是否在同一个网络命名空间,且流量确实经过该veth设备;ICMP流量proto为1,同样会被捕获。
  • 哈希冲突:高并发下不同流可能映射到同一key,导致延迟被错误叠加。生产环境建议使用BPF_MAP_TYPE_PERCPU_HASH或增加元组维度(如加TCP序列号)。