eBPF技术在内核可观测性中的定位
eBPF(Extended Berkeley Packet Filter)允许在Linux内核中运行沙箱程序,无需修改内核源码或加载内核模块即可动态注入观测和过滤逻辑。传统网络监控工具(如tcpdump、netstat)通过系统调用从用户态读取数据,频繁的内核态-用户态上下文切换在高流量服务器上造成性能开销。eBPF程序直接在内核态执行数据采集和聚合,仅将结果传递到用户态,避免了大量数据拷贝。
服务器运维场景中,eBPF主要用于三个方面:网络流量观测(追踪TCP连接状态、丢包分析)、性能剖析(函数级CPU profiling、IO延迟分析)和安全审计(系统调用监控、文件访问追踪)。bcc(BPF Compiler Collection)是Python编写的eBPF工具链,提供常用观测工具和开发框架。
bcc工具链安装与常用工具
Ubuntu/Debian系统安装bcc工具链:
apt-get install bpfcc-tools linux-headers-$(uname -r)
# 工具以-bpfcc后缀调用
execsnoop-bpfcc
tcplife-bpfcc
biolatency-bpfcc
CentOS/RHEL系统:
yum install bcc-tools
# 工具安装在 /usr/share/bcc/tools/ 目录
/usr/share/bcc/tools/execsnoop
常用bcc观测工具及适用场景:
- execsnoop:追踪新进程创建,排查异常启动的脚本或恶意程序
- tcpconnect:追踪主动出站的TCP连接,定位异常网络访问
- tcpaccept:追踪被动入站的TCP连接,分析服务端连接来源
- biolatency:统计块设备IO延迟分布直方图,识别磁盘性能瓶颈
- funclatency:追踪内核函数执行延迟,定位内核态性能热点
- opensnoop:追踪文件打开操作,排查配置文件加载和日志写入
- runqlat:统计CPU调度延迟分布,定位调度器层面的延迟
使用bcc开发自定义eBPF追踪程序
bcc框架使用Python + C的混合开发模式。C代码定义eBPF程序逻辑,通过Python脚本控制程序加载、事件输出和数据展示。以下示例追踪所有TCP连接的建立过程,输出源IP、目标IP和端口信息:
#!/usr/bin/env python3
from bcc import BPF
from bcc.utils import printb
# eBPF C程序
bpf_text = '''
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <bcc/proto.h>
BPF_HASH(currsock, u32, struct sock *);
// 追踪tcp_v4_connect调用
int trace_connect_v4_entry(struct pt_regs *ctx, struct sock *sk)
{
u32 pid = bpf_get_current_pid_tgid();
// 记录当前进程的sock指针
currsock.update(&pid, &sk);
return 0;
}
int trace_connect_v4_return(struct pt_regs *ctx)
{
int ret = PT_REGS_RC(ctx);
u32 pid = bpf_get_current_pid_tgid();
struct sock **skpp;
skpp = currsock.lookup(&pid);
if (skpp == 0) return 0;
if (ret == 0) {
// 连接成功,读取sock结构中的IP和端口
struct sock *sk = *skpp;
u32 saddr = sk->__sk_common.skc_rcv_saddr;
u32 daddr = sk->__sk_common.skc_daddr;
u16 dport = sk->__sk_common.skc_dport;
bpf_trace_printk("TCP connect: %x -> %x:%d\n", saddr, daddr, ntohs(dport));
}
currsock.delete(&pid);
return 0;
}
'''
b = BPF(text=bpf_text)
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")
print("Tracing TCP connects... Ctrl-C to end.")
b.trace_print()
attach_kprobe在函数入口插入探针,attach_kretprobe在函数返回处插入探针。bpf_trace_printk将输出写入/sys/kernel/debug/tracing/trace_pipe,通过b.trace_print()在用户态读取。生产环境中应使用BPF_PERF_OPEN替代trace_printk,通过ring buffer传递结构化数据,性能更好且支持多事件并发。
XDP高性能数据面编程
XDP(eXpress Data Path)是Linux内核中的高性能网络数据面,在网卡驱动层执行eBPF程序,早于sk_buff分配和协议栈处理。XDP程序可在数据包到达内核协议栈之前执行丢包、重定向或修改操作,单核吞吐量可达24Mpps(百万包/秒),适合DDoS防护和负载均衡场景。
以下XDP程序统计各协议的收包数量并按IP地址限速:
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
// 统计映射
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, __u32);
__type(value, __u64);
__uint(max_entries, 256);
} packet_count SEC(".maps");
// 限速映射
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, __u32); // 源IP
__type(value, __u64); // 上次重置时间(ns)
__uint(max_entries, 100000);
} rate_limit SEC(".maps");
SEC("xdp")
int xdp_stats(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void*)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *iph = (void*)(eth + 1);
if ((void*)(iph + 1) > data_end) return XDP_PASS;
__u32 proto = iph->protocol;
__u64 *count = bpf_map_lookup_elem(&packet_count, &proto);
if (count) __sync_fetch_and_add(count, 1);
// 按源IP限速:每秒超过1000包则丢弃
__u32 src_ip = iph->saddr;
__u64 now = bpf_ktime_get_ns();
__u64 *last = bpf_map_lookup_elem(&rate_limit, &src_ip);
if (last) {
if (now - *last < 1000000000ULL) { // 1秒内
return XDP_DROP; // 限速丢弃
}
*last = now;
} else {
bpf_map_update_elem(&rate_limit, &src_ip, &now, BPF_ANY);
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
编译并加载XDP程序:
# 编译
clang -O2 -g -target bpf -c xdp_stats.c -o xdp_stats.o
# 加载到网卡
ip link set dev eth0 xdpgeneric obj xdp_stats.o sec xdp
# 查看XDP程序状态
ip link show dev eth0
# 卸载
ip link set dev eth0 xdpgeneric off
xdpgeneric使用通用XDP模式,在驱动上层运行,兼容所有网卡。xdpdrv使用驱动原生模式,性能更高但需要网卡驱动支持。xdpoffload将程序卸载到智能网卡上执行,延迟最低但需要特定硬件支持。
UPROBE与KPROBE性能对比
动态追踪中,KPROBE用于内核函数,UPROBE用于用户态函数。两者在性能开销上有显著差异:
# 测试KPROBE开销
funclatency-bpfcc -d 10 tcp_v4_connect
# 典型结果:usec级延迟分布
# 测试UPROBE开销
funclatency-bpfcc -d 10 c:malloc
# 典型结果:usec级,但UPROBE在高频函数上开销更大
KPROBE的每次触发约增加1-3微秒开销,UPROBE约3-10微秒。在每秒调用数万次的热点函数上,UPROBE可能影响系统性能。生产环境中应在确认性能影响可控后启用追踪,长时间运行时使用采样模式替代全量追踪。通过bpf_get_current_pid_tgid()和bpf_get_current_uid_gid()在eBPF程序中获取进程上下文,过滤特定进程或用户的流量,减少无关数据的采集开销。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/linuxebpf-wang-luo-guan-ce-shi-zhan-bcc-gong-ju-lian-kai-fa/