XDP 与 TC:Linux 内核网络可观测性的双引擎
在上一篇文章中,我们从 strace 出发,系统梳理了 Linux 系统调用追踪的技术栈,并最终引入了 eBPF 作为现代性能分析的利器。本文将继续深入 eBPF 生态,将焦点从系统调用追踪转向网络数据包处理——具体而言,是 XDP(eXpress Data Path)和 TC(Traffic Control)这两大 eBPF 挂载点在实际网络可观测性工程中的应用。
XDP:数据包到达后的第一个战场
XDP 允许 eBPF 程序在数据包进入 Linux 内核网络栈的最早阶段介入——甚至在 socket buffer 分配之前。这意味着 XDP 程序运行在网卡驱动层的接收路径上,具有极高的处理效率。一个典型的 XDP 程序可以直接读取数据包的原始字节,并根据预定义的逻辑决定是丢弃、重定向还是放行该数据包。
XDP 的三种工作模式
- Native XDP:eBPF 程序直接在网卡驱动内部执行,需要将 XDP 程序加载到支持该模式的驱动中。性能最优,但依赖硬件驱动支持。
- Offloaded XDP:eBPF 程序被卸载到网卡硬件上执行(SmartNIC 场景),完全绕过 CPU,适合超大规模流量处理。
- Generic XDP:作为 fallback 模式,在 Linux 通用网络层执行 eBPF 程序。性能略低于 Native 模式但不要求驱动支持。
对于大多数可观测性场景,Generic XDP 已足够使用。当需要监控大量流量但不希望分配完整 socket buffer 时,XDP 提供了一个理想的观测点:你可以在数据包进入内核协议栈之前就完成统计、采样和异常检测。
TC:流量控制层的精细观测
与 XDP 不同,TC eBPF 程序运行在内核流量控制子系统层,此时数据包已经封装为 sk_buff。TC 支持ingress 和 egress 两个方向,能够在协议栈的多个关键点介入。
TC 的主要优势在于:
- 可以访问完整的
sk_buff结构,获取更多协议元数据(如 socket、cgroup 信息) - 支持更复杂的分类和排队规则(与 qdisc 集成)
- 能够修改数据包内容或添加/移除包头
- 对容器场景友好——可以直接关联到 cgroup 级别的网络观测
实战:构建实时网络异常检测系统
下面展示一个基于 XDP 的 SYN Flood 检测与缓解系统。该系统在驱动层统计每个源 IP 的 SYN 包速率,当超过阈值时自动丢弃后续 SYN 包,从而实现内核态的 DDoS 防护。
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
// 哈希表:记录每个源IP的SYN计数
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, __u32); // 源IP
__type(value, __u64); // SYN计数
__uint(max_entries, 10000);
} syn_count SEC(".maps");
SEC("xdp")
int syn_flood_detect(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_TCP)
return XDP_PASS;
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
// 只关注SYN包(非ACK)
if (!tcp->syn || tcp->ack)
return XDP_PASS;
__u32 src_ip = ip->saddr;
__u64 *count = bpf_map_lookup_elem(&syn_count, &src_ip);
__u64 now = bpf_ktime_get_ns();
if (count) {
// 速率检测:1秒内超过1000个SYN则丢弃
if (*count > 1000 && (now - *count) < 1000000000ULL) {
return XDP_DROP;
}
*count = now; // 更新时间戳和计数
} else {
bpf_map_update_elem(&syn_count, &src_ip, &now, BPF_ANY);
}
return XDP_PASS;
}
TC 实战:容器网络流量分类
在容器化部署场景中,我们经常需要按 Pod/容器维度统计网络流量。TC eBPF 可以直接获取发送方的 cgroup 信息,配合 bpf_skb_cgroup_id() 辅助函数实现容器级别的网络可观测性。
// TC eBPF 程序:按cgroup统计流量
SEC("tc")
int cgroup_traffic_count(struct __sk_buff *skb) {
__u64 cgroup_id = bpf_skb_cgroup_id(skb);
__u64 pkt_len = skb->len;
// 按cgroup聚合流量统计
__u64 *bytes = bpf_map_lookup_elem(&cgroup_stats, &cgroup_id);
if (bytes) {
__sync_fetch_and_add(bytes, pkt_len);
} else {
bpf_map_update_elem(&stats, &cgroup_id, &pkt_len, BPF_ANY);
}
return TC_ACT_OK;
}
性能对比:选择 XDP 还是 TC
| 维度 | XDP | TC |
|---|---|---|
| 运行层级 | 网卡驱动层(最早) | 流量控制子系统(较晚) |
| 数据访问 | 原始以太网帧 | 完整 sk_buff |
| 可观测信息 | 包头(L2-L4) | 全部协议元数据 + cgroup + socket |
| 处理吞吐 | 最高(~24M pps/核) | 高(~5M pps/核) |
| 修改能力 | 可重定向/丢弃/放行 | 可修改包头/排队/重写 |
| 驱动依赖 | 需要驱动支持(Offloaded) | 无特殊要求 |
生产部署建议
在实际生产环境中,我们通常会组合使用 XDP 和 TC:
- 第一层(XDP):用于高速流量采样、DDoS 防护、负载均衡重定向
- 第二层(TC):用于精细流量分类、QoS 策略、容器网络监控
- 第三层(用户态):将 BPF Map 中的统计数据导出到 Prometheus/Grafana 进行可视化
通过 libbpf 提供的 CO-RE(Compile Once, Run Everywhere)机制,可以编写与内核版本无关的 eBPF 程序,避免在生产环境中重新编译的痛苦。
结语
XDP 和 TC 代表了 Linux 网络可观测性的两个不同层级:XDP 追求极致性能,TC 提供丰富语义。理解两者的差异和协同方式,是构建现代云原生网络观测体系的关键一步。结合前一篇文章讨论的系统调用追踪技术,我们现在已经有了从系统调用到网络数据包的完整观测能力——这正是 eBPF 最初的设计愿景:一个统一的内核可编程观测平台。

发表评论 取消回复