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
验证方法
- 在容器内持续发送UDP/TCP流量:
iperf3 -c 10.0.0.2 -u -b 100M -t 30(或使用ping -f产生ICMP流量)。 - 观察用户态程序每2秒打印的直方图,确认有数据包计数分布在各延迟区间。
- 通过条件:直方图中出现非零计数,且大部分计数集中在低延迟区间(如<100us),说明测量路径生效。
- 停止程序(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序列号)。