eBPF技术入门与实战:Linux内核黑科技

前言

三年前第一次听说eBPF,觉得这玩意离我太远------内核开发?那是神仙干的事。

直到前段时间排查一个诡异的网络延迟问题,传统工具都定位不到根因,同事甩给我一个bpftrace脚本,几秒钟就抓到了问题。那一刻我才意识到,eBPF已经不是什么高深莫测的东西,而是实打实能解决问题的生产力工具。

这篇文章是我学习和使用eBPF的一些记录,不讲太多底层原理,主要聊聊怎么用它解决实际问题。

eBPF是什么

简单说,eBPF(extended Berkeley Packet Filter)让你可以在内核里安全地运行自定义程序,而不需要修改内核源码或加载内核模块。

传统的做法要观测内核行为,要么改内核重新编译,要么写个内核模块。两个方案都很重,风险也大。eBPF相当于在内核里开了个"沙盒",你的程序在里面跑,既能访问内核数据,又不会把系统搞崩。

能干什么

  • 网络:高性能负载均衡、DDoS防护、流量过滤
  • 安全:系统调用审计、入侵检测、容器安全
  • 观测:性能分析、延迟追踪、资源监控
  • 调试:内核函数追踪、用户态程序分析

Cloudflare用eBPF扛DDoS,Facebook用它做负载均衡,Cilium用它搞容器网络。这东西已经在生产环境大规模使用了。

环境准备

eBPF需要内核版本支持,最低4.x,建议5.x以上。Ubuntu 20.04/22.04都没问题。

bash 复制代码
# 检查内核版本
uname -r
# 5.15.0-91-generic

# 安装BCC工具集(最常用的eBPF工具集)
apt update
apt install -y bpfcc-tools linux-headers-$(uname -r)

# 安装bpftrace(高级追踪语言)
apt install -y bpftrace

# 验证安装
bpftrace --version
# bpftrace v0.14.0

实战一:追踪系统调用延迟

前几天遇到一个问题:某个Java服务响应变慢,但CPU、内存、IO看着都正常。

用bpftrace追踪一下read系统调用的延迟分布:

bash 复制代码
# 追踪read调用延迟(按进程名过滤)
bpftrace -e '
tracepoint:syscalls:sys_enter_read /comm == "java"/ {
    @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read /comm == "java" && @start[tid]/ {
    @usecs = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
'

输出:

less 复制代码
@usecs:
[0]                  156 |@@@@@@@@@@@@@@@                                     |
[1]                  489 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2, 4)               234 |@@@@@@@@@@@@@@@@@@@@@@@                             |
[4, 8)                67 |@@@@@@                                              |
[8, 16)               23 |@@                                                  |
[16, 32)              12 |@                                                   |
[32, 64)               8 |                                                    |
[64, 128)              3 |                                                    |
[128, 256)           156 |@@@@@@@@@@@@@@@                                     |  <-- 异常

128-256微秒这个区间的调用数量异常偏多,正常应该是单峰分布。进一步追踪发现是读取某个配置文件时,文件系统有锁竞争。

这种问题用传统工具(strace会拖慢进程太多)很难快速定位,eBPF几乎零开销。

实战二:网络延迟分析

生产环境有台机器TCP延迟偶发飙高,网络组说线路没问题。用tcpretrans追踪重传:

bash 复制代码
# BCC工具:追踪TCP重传
/usr/share/bcc/tools/tcpretrans

# 输出
TIME     PID    IP LADDR:LPORT          T> RADDR:RPORT          STATE
14:23:15 0      4  10.0.1.5:443         R> 10.0.2.8:52341       ESTABLISHED
14:23:15 0      4  10.0.1.5:443         R> 10.0.2.8:52341       ESTABLISHED
14:23:16 0      4  10.0.1.5:443         R> 10.0.2.8:52341       ESTABLISHED

同一个连接连续重传,问题缩小到10.0.2.x这个网段。最后查出来是那个机房的交换机有问题。

更进一步,看TCP连接延迟分布:

bash 复制代码
# 追踪TCP连接建立延迟
/usr/share/bcc/tools/tcpconnlat

# 输出
PID    COMM         IP SADDR            DADDR            DPORT LAT(ms)
1892   curl         4  10.0.1.5         10.0.2.8         443   245.12
1893   curl         4  10.0.1.5         10.0.2.8         443   312.45
1894   curl         4  10.0.1.5         10.0.3.9         443   1.23

对比很明显,连10.0.2网段延迟高了两个数量级。

实战三:进程级资源监控

有个容器CPU用量一直很高,但top里看不出哪个函数在消耗。用profile工具:

bash 复制代码
# CPU采样火焰图数据
/usr/share/bcc/tools/profile -p $(pgrep -f myapp) -f 30 > profile.out

# 生成火焰图(需要安装FlameGraph)
git clone https://github.com/brendangregg/FlameGraph
./FlameGraph/flamegraph.pl profile.out > cpu.svg

火焰图一目了然,发现某个JSON解析函数占了40%的CPU。原来是每次请求都在重复解析同一个大配置文件,加个缓存解决。

实战四:自定义追踪点

有时候需要追踪特定的内核函数。比如想知道文件打开操作的分布:

bash 复制代码
bpftrace -e '
kprobe:do_sys_openat2 {
    @files[str(arg1)] = count();
}
interval:s:5 {
    print(@files);
    clear(@files);
}
'

输出每5秒打印一次文件打开统计:

less 复制代码
@files[/etc/ld.so.cache]: 234
@files[/lib/x86_64-linux-gnu/libc.so.6]: 156
@files[/proc/self/status]: 89
@files[/app/config.json]: 67
...

这种方式对排查"到底谁在频繁读写某个文件"特别有用。

写个简单的eBPF程序

BCC提供Python接口,写起来比较方便。追踪所有的execve调用(新进程启动):

python 复制代码
#!/usr/bin/env python3
from bcc import BPF

# eBPF程序(C语言)
prog = """
#include <linux/sched.h>

struct data_t {
    u32 pid;
    char comm[TASK_COMM_LEN];
};

BPF_PERF_OUTPUT(events);

int trace_execve(struct pt_regs *ctx) {
    struct data_t data = {};
    
    data.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    
    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}
"""

b = BPF(text=prog)
b.attach_kprobe(event="__x64_sys_execve", fn_name="trace_execve")

print("追踪新进程启动,Ctrl+C退出...")

def print_event(cpu, data, size):
    event = b["events"].event(data)
    print(f"PID: {event.pid}, COMM: {event.comm.decode()}")

b["events"].open_perf_buffer(print_event)
while True:
    b.perf_buffer_poll()

运行效果:

yaml 复制代码
追踪新进程启动,Ctrl+C退出...
PID: 12345, COMM: bash
PID: 12346, COMM: ls
PID: 12347, COMM: grep

这对安全审计很有价值------谁在服务器上执行了什么命令,一清二楚。

生产环境使用建议

性能开销

eBPF的开销很低,但不是零。几个原则:

  1. 过滤要前置:在eBPF程序里做过滤,而不是全量采集再在用户态过滤
  2. 采样而非全量:profile用采样,不要每个事件都追踪
  3. 控制输出频率:用interval聚合,不要每个事件都输出

常用工具速查

bash 复制代码
# 性能分析
profile      # CPU火焰图
offcputime   # Off-CPU时间分析
runqlat      # 运行队列延迟

# 网络
tcplife      # TCP连接生命周期
tcpretrans   # TCP重传追踪
tcpconnlat   # TCP连接延迟

# 磁盘IO
biolatency   # 块设备IO延迟
biosnoop     # 块设备IO追踪
ext4slower   # ext4慢操作

# 内存
memleak      # 内存泄漏检测
cachestat    # 缓存命中率

# 系统调用
execsnoop    # 进程启动追踪
opensnoop    # 文件打开追踪

多机器批量排查

遇到跨多台服务器的问题时,需要同时在多个节点运行eBPF工具做对比分析。这种场景下我会用星空组网先把各个网段的机器串起来,统一管理后再批量跑诊断脚本,比逐台SSH效率高很多。

进阶学习资源

总结

eBPF不再是内核黑客的专属玩具。对于运维和后端开发来说,它是一个威力巨大的问题排查工具:

  1. 低开销:生产环境可用
  2. 高精度:内核级别的观测能力
  3. 灵活:可以自定义追踪逻辑
  4. 安全:内核保证程序不会搞崩系统

从BCC工具集入手,遇到问题先试试现成工具,熟练后再尝试写自定义追踪程序。这个学习路径比较平滑。

说实话,用惯了eBPF再回头看传统的排查手段,会觉得效率差太多。推荐每个做服务端的同学都了解一下。

相关推荐
风好衣轻2 小时前
Ubuntu单卡5090部署VeRL:从安装到运行
linux·运维·ubuntu
火柴棍mcu2 小时前
Ubuntu设备屏幕旋转、竖屏改横屏
linux·ubuntu·旋转·屏幕
amao99882 小时前
MITos2022--Lab2: system calls
linux
小码吃趴菜2 小时前
地址空间详解-fork复制进程
linux
Xの哲學2 小时前
Linux IPsec 深度解析: 架构, 原理与实战指南
linux·服务器·网络·算法·边缘计算
hqzing2 小时前
C语言程序调用syscall的几种方式
linux·c++
老王熬夜敲代码2 小时前
TCP相关问题的解决
linux·网络·笔记·网络协议
泽君学长3 小时前
CentOS 7 安装 Docker 完整教程
linux·docker·centos
wheeldown3 小时前
【Linux网络编程】网络基础之MAC地址与IP地址的区别
linux·运维·网络·macos