← 返回新闻列表

深入解析eBPF技术:Linux可观测性的强大引擎

eBPF(extended Berkeley Packet Filter)作为一项革新的Linux内核技术,允许开发者在不修改内核代码的前提下,进行网络监控、安全过滤和性能分析等多种操作,极大增强了Linux系统的可观测性。借助BPF编译器集合(BCC)等工具,eBPF能够实现对系统事件的深度追踪和数据收集,但其学习和掌握需要扎实的底层系统知识与开发能力。

文 / 编辑部 · 2022/12/10 · 阅读约 14 分钟

分享:
深入解析eBPF技术:Linux可观测性的强大引擎

eBPF(extended Berkeley Packet Filter),是Linux内核中一项强大的技术,它使得开发者能够在不直接修改内核代码的情况下,动态加载并执行自定义程序。这项技术脱胎于早期的Berkeley Packet Filter(BPF),后者主要用于网络数据包的捕获与过滤。

eBPF的灵感源于对更优Linux追踪工具的渴望,特别借鉴了DTrace的理念。DTrace是一款广泛应用于Solaris和BSD系统的动态追踪工具。与DTrace不同,Linux在提供系统运行态全面视图方面有所限制,主要局限于系统调用、库调用和特定框架内的函数。为此,一小部分工程师开始扩展BPF后端,以期实现与DTrace相似的功能集,eBPF由此应运而生。eBPF首次亮相于2014年的Linux 3.18版本,但要充分利用其功能,通常需要Linux 4.4及以上版本。

相较于传统的BPF,其主要限制于网络过滤,eBPF的应用场景更为广泛,涵盖了网络监控、安全策略实施及性能深度分析等多个领域。eBPF的核心优势在于,它允许用户空间应用程序将预设的逻辑打包为字节码,并在特定的内核事件(即“挂钩”)发生时由内核执行。这些挂钩可以是系统调用、网络事件等。目前,编写和调试eBPF程序最流行的工具链是基于LLVM和Clang构建的BPF编译器集合(BCC)。

除了eBPF,业界还有一些功能相似的工具。例如,SystemTap是一种开源工具,通过动态加载内核模块来收集Linux内核的运行时数据。DTrace则是一种动态追踪和分析工具,同样能够用于收集系统运行时信息,与eBPF和SystemTap异曲同工。

下表简要对比了eBPF、SystemTap和DTrace这三类工具的特点:

| 工具 | eBPF | SystemTap | DTrace | |:----------|:-------------------------|:------------------------------|:---------------------------------| | 定位 | 内核技术,多应用场景 | 内核模块 | 动态追踪与分析工具 | | 工作原理 | 动态加载与执行无损编译代码 | 动态加载内核模块 | 动态插接分析器,探针获取数据分析 | | 常见用途 | 网络监控、安全过滤、性能分析 | 系统性能分析、故障诊断 | 系统性能分析、故障诊断 | | 优点 | 灵活、安全、多应用场景 | 功能强大、可视化界面 | 功能强大、高性能、支持多语言 | | 缺点 | 学习曲线陡峭,安全性依赖编译器 | 学习曲线陡峭,安全性依赖内核模块 | 配置复杂,对系统性能影响较大 |

从上述对比可以看出,eBPF、SystemTap和DTrace都是功能强大的工具,各自在系统运行状况的收集与分析方面占有一席之地。

eBPF作为一项高度灵活且强大的内核技术,其应用范围极为广泛,例如:

网络监控:eBPF可以捕获网络数据包,执行特定的逻辑来深度分析网络流量。例如,可以使用eBPF程序来实时监控网络传输情况,并在检测到异常流量时发出警报。

安全过滤:eBPF可用于实施高效的网络数据包安全过滤。例如,通过eBPF程序,可以有效阻止恶意流量的传播,或在检测到潜在威胁时立即进行拦截。

性能分析:eBPF在内核性能分析方面也表现出色。利用eBPF程序,可以收集详细的内核性能指标,并通过特定接口进行可视化展示,从而帮助识别并优化性能瓶颈。

虚拟化:eBPF同样适用于虚拟化环境。例如,它可以收集虚拟机的性能数据,进一步优化负载均衡策略,从而更高效地利用虚拟化资源,提升系统整体性能和稳定性。

总而言之,eBPF的常见用途非常广泛,贯穿网络监控、安全过滤、性能分析和虚拟化等多个关键领域。

eBPF的工作原理主要包含加载、编译以及执行这三个核心步骤。

eBPF程序的运行需要在内核环境中进行。通常,用户空间应用程序通过系统调用将eBPF程序加载到内核。在加载过程中,eBPF程序的代码会被复制到内核空间。

eBPF程序需经过编译后才能执行。该过程通常由Clang/LLVM编译器完成,生成字节码。这些字节码被装载到内核之后,会先由一个验证器(Verifier)进行安全检查。这一步骤至关重要,它确保eBPF程序不会损害内核的稳定性与安全性。验证器会对代码进行深度分析,以防止恶意操作,例如非法的系统调用或内存访问。一旦通过安全检查,eBPF程序便可在内核中正常运行,并通过即时编译(JIT)将通用字节码转换为机器特定的指令集,从而优化执行速度。

eBPF程序的架构图通常展示了其在内核中的运行机制,包括与用户空间的交互流程。

eBPF程序在内核中运行时,通常会绑定到一个或多个内核钩子(hook)上,从而在特定事件发生时被触发执行。这些钩子可以包括:

系统调用:当用户空间函数将执行转移到内核时介入。

函数进入和退出:拦截对已存在函数的调用。

网络事件:在接收到数据包时执行。

Kprobes和Uprobes:附着于内核或用户空间函数的探测器。

此外,eBPF Maps允许eBPF程序在不同调用之间保持状态,用于数据统计或与用户空间应用程序共享数据。eBPF Map本质上是一种键值存储机制,其中的值通常被视为任意数据的二进制块。它们通过bpf_cmd系统调用(带BPF_MAP_CREATE参数)创建,并通过文件描述符进行寻址。对Map的操作包括查找、更新和删除等系统调用。

总的来说,eBPF的工作原理是通过动态加载、执行以及严格的验证机制,实现对编译后的代码在内核中的安全高效运行。

以下是一个基于eBPF的性能分析的逐步示例:

第一步:准备工作:首先,需要确保Linux内核已启用eBPF相关功能。这通常涉及在内核配置文件中启用eBPF选项并重新编译内核。您可以通过执行`ls /sys/fs/bpf`和`lsmod | grep bpf`命令来检查eBPF的支持情况。

第二步:编写eBPF程序:接下来,需要编写eBPF程序以收集内核性能指标。eBPF程序可以使用C或Python编写,需要通过特定接口访问内核数据结构,并将收集到的数据保存到指定位置。

以下是一个Python示例(其中包含了 embedded C 代码,由Python加载到Linux内核):

```python #!/usr/bin/python3

from bcc import BPF from time import sleep

bpf_text = """ #include <uapi/linux/ptrace.h> BPF_HASH(stats, u32);

int count(struct pt_regs *ctx) { u32 key = 0; u64 *val, zero=0; val = stats.lookup_or_init(&key, &zero); (*val)++; return 0; } """

b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"]) b.attach_kprobe(event="tcp_sendmsg", fn_name="count")

name = { 0: "tcp_sendmsg" }

while True: try: for k, v in b["stats"].items(): print("{}: {}".format(name[k.value], v.value)) sleep(1) except KeyboardInterrupt: exit() ```

这个eBPF程序旨在统计网络中传输的数据包数量。它利用`BPF_HASH`数据结构存储统计结果,并通过捕获`tcp_sendmsg`事件实现实时计数。程序每秒输出一次统计结果。此示例较为基础,实际应用中可能需要更复杂的统计和分析逻辑。

第三步:运行eBPF程序:使用eBPF编译器将eBPF程序编译成内核可执行格式。这一过程通常借助于BPF Compiler Collection(BCC)工具完成。BCC工具能够通过命令行将eBPF程序编译为内核可执行格式,并加载到内核中。(在上述Python程序中,bcc库负责将C语言程序编译成字节码并加载到内核,同时将某个函数挂载到特定事件上。)

要运行上述Python3程序,请遵循以下步骤:

安装依赖:`sudo apt install python3-bpfcc`

注意:在Python3环境下,不建议使用`pip3 install bcc`。

对于Ubuntu 20.10及以上版本,通过源码安装通常能避免编译问题:

```bash apt purge bpfcc-tools libbpfcc python3-bpfcc wget <下载地址,通常在bcc的github release页面查找> tar xf bcc-src-with-submodule.tar.gz cd bcc/ apt install -y python-is-python3 apt install -y bison build-essential cmake flex git libedit-dev libllvm11 llvm-11-dev libclang-11-dev zlib1g-dev libelf-dev libfl-dev python3-distutils apt install -y checkinstall mkdir build cd build/ cmake -DCMAKE_INSTALL_PREFIX=/usr -DPYTHON_CMD=python3 .. make checkinstall ```

将Python程序保存为`netstat.py`,然后执行:

```bash $ chmod +x ./netstat.py $ sudo ./netstat.py ```

程序运行后,控制台将持续输出网络数据包的统计信息,按`Ctrl+C`可终止程序。

接下来看一个更复杂的示例,它计算TCP数据包的发送时间(参考自GitHub上的一个issue中的程序):

```python #!/usr/bin/python3

from bcc import BPF import time

bpf_text = """ #include <uapi/linux/ptrace.h> #include <net/sock.h> #include <net/inet_sock.h> #include <bcc/proto.h>

struct packet_t { u64 ts, size; u32 pid; u32 saddr, daddr; u16 sport, dport; };

BPF_HASH(packets, u64, struct packet_t);

int on_send(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) { u64 id = bpf_get_current_pid_tgid(); u32 pid = id;

struct packet_t pkt = {}; pkt.ts = bpf_ktime_get_ns(); pkt.size = size; pkt.pid = pid; pkt.saddr = sk->__sk_common.skc_rcv_saddr; pkt.daddr = sk->__sk_common.skc_daddr; struct inet_sock *sockp = (struct inet_sock *)sk; pkt.sport = sockp->inet_sport; pkt.dport = sk->__sk_common.skc_dport;

packets.update(&id, &pkt); return 0; }

int on_recv(struct pt_regs *ctx, struct sock *sk) { u64 id = bpf_get_current_pid_tgid(); u32 pid = id;

struct packet_t *pkt = packets.lookup(&id); if (!pkt) { return 0; }

u64 delta = bpf_ktime_get_ns() - pkt->ts;

bpf_trace_printk("tcp_time: %llu.%llums, size: %llu\\n", delta/1000, delta%1000%100, pkt->size);

packets.delete(&id); return 0; } """

b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"]) b.attach_kprobe(event="tcp_sendmsg", fn_name="on_send") b.attach_kprobe(event="tcp_v4_do_rcv", fn_name="on_recv")

print("Tracing TCP latency... Hit Ctrl-C to end.") while True: try: (task, pid, cpu, flags, ts, msg) = b.trace_fields() print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg)) except KeyboardInterrupt: exit() ```

上述程序通过捕获每个数据包的发送和接收时间戳来计算传输延迟。当`tcp_sendmsg`事件发生时记录发送时间,当`tcp_v4_do_rcv`事件发生时记录接收时间,然后二者之差即为传输时间。这两个示例展现了eBPF的基本编程模式:在Python中将“C语言”片段挂载到内核的特定事件上。坦白说,eBPF编程具有相当的复杂性和一些不常见的细节,对于一般开发者而言,掌握起来颇具挑战。幸运的是,许多常用eBPF工具已由社区开发者完成,例如Github上bcc库的`tools`目录下提供了大量现成的示例。

BCC(BPF Compiler Collection)是一套开源工具集,专门用于在Linux系统上利用BPF程序进行系统级性能分析与监测。BCC包含了众多实用的工具,例如:

bcc-tools:一个集成了多种常用BCC工具的软件包。

bpftrace:一种高级语言,用于便捷地编写和执行BPF程序。

tcptop:实时监控和分析TCP流量的实用工具。

execsnoop:用于追踪进程执行活动。

filetop:实时监控文件系统I/O活动。

trace:跟踪和分析函数调用。

funccount:统计函数调用次数。

opensnoop:监控文件打开操作。

pidstat:监控进程性能。

profile:分析系统CPU使用情况。

一张流传甚广的图表形象展示了eBPF及其丰富工具集所能实现的功能,揭示了内核内部发生的各种事件,使得一切变得一览无余。

延伸阅读推荐:

Brendan Gregg撰写的《BPF Performance Tools: Linux System and Application Observability》是一本全面介绍eBPF基础知识和实践应用的指南。

由Cilium维护的eBPF官方网站,提供了丰富的资源和文档。

Cilium's BPF and XDP参考指南。

BPF官方文档。

BPF设计问答集。

以及GitHub上的Awesome eBPF项目,汇集了众多eBPF相关的优秀资源。

最后,我想分享一个“彩蛋”。近期ChatGPT异常火爆,我尝试Natiive借助于它来完成这篇文章的撰写。我首先让ChatGPT帮我生成文章提纲,然后根据提纲生成内容并查找相关信息,这确实提供了一些便利。

广告位 · 文末横幅