BPF技术:从原理到实际应用
1. BPF LSM Hooks
为了实现对系统事件与架构无关的控制,Linux Security Modules(LSM)引入了钩子(hooks)的概念。从技术层面讲,钩子调用类似于系统调用,但它独立于系统且与LSM框架集成,这种抽象层既方便使用,又能避免在不同架构下使用系统调用时可能出现的问题。
目前,内核中有七个与BPF程序相关的钩子,且只有SELinux这个LSM实现了它们。这些钩子定义在
include/linux/security.h
文件中,具体如下:
extern int security_bpf(int cmd, union bpf_attr *attr, unsigned int size);
extern int security_bpf_map(struct bpf_map *map, fmode_t fmode);
extern int security_bpf_prog(struct bpf_prog *prog);
extern int security_bpf_map_alloc(struct bpf_map *map);
extern void security_bpf_map_free(struct bpf_map *map);
extern int security_bpf_prog_alloc(struct bpf_prog_aux *aux);
extern void security_bpf_prog_free(struct bpf_prog_aux *aux);
这些钩子在执行的不同阶段被调用:
| 钩子函数 | 功能 |
| — | — |
|
security_bpf
| 对执行的BPF系统调用进行初始检查 |
|
security_bpf_map
| 当内核为映射返回文件描述符时进行检查 |
|
security_bpf_prog
| 当内核为eBPF程序返回文件描述符时进行检查 |
|
security_bpf_map_alloc
| 对BPF映射内的安全字段进行初始化 |
|
security_bpf_map_free
| 清理BPF映射内的安全字段 |
|
security_bpf_prog_alloc
| 对BPF程序内的安全字段进行初始化 |
|
security_bpf_prog_free
| 清理BPF程序内的安全字段 |
LSM BPF钩子的核心思想是为eBPF对象提供逐个对象的保护,确保只有具有适当权限的用户才能对映射和程序进行操作。
2. 系统安全的分层与BPF的作用
安全并非能以通用方式应用于所有需要保护的事物。在不同层面和以不同方式保障系统安全至关重要。最佳的系统安全方案是堆叠不同视角的安全层,这样即使某一层被攻破,也不会导致整个系统被访问。内核开发者为我们提供了一系列不同的安全层和交互点,我们可以利用BPF程序与这些层进行交互。
3. Sysdig eBPF God Mode
Sysdig是一家开发同名开源Linux故障排除工具的公司,于2017年内核4.11版本时开始使用eBPF。此前,该公司一直使用内核模块来完成内核端的工作,但随着用户数量增加以及更多公司开始尝试使用,他们意识到内核模块存在诸多限制:
-
加载限制
:越来越多的用户无法在其机器上加载内核模块,云原生平台对运行时程序的限制也越来越多。
-
贡献者理解困难
:新老贡献者对内核模块的架构理解不足,导致贡献者数量减少,限制了项目的发展。
-
维护难度大
:内核模块的维护不仅涉及代码编写,还需要投入大量精力确保其安全性和良好的组织性。
基于这些原因,Sysdig决定尝试使用eBPF程序来实现与内核模块相同的功能。采用eBPF还带来了其他好处,例如可以利用eBPF的跟踪功能。通过用户探针,能够轻松地将eBPF程序附加到用户空间应用程序的特定执行点。此外,项目还可以使用eBPF程序中的原生辅助功能来捕获运行进程的堆栈跟踪,为用户提供更多的故障排除信息。
Sysdig在开始使用eBPF时,由于eBPF虚拟机的限制遇到了一些挑战。项目的首席架构师Gianluca Borello通过向上游内核贡献补丁来改进,这些补丁包括:
- 使eBPF程序能够原生处理字符串。
- 多个用于改进eBPF程序参数语义的补丁。
Sysdig的eBPF模式核心是一组自定义的eBPF程序,这些程序用C语言的一个子集编写,并使用Clang和LLVM的最新版本编译成eBPF字节码。目前,eBPF程序被附加到以下静态跟踪点:
- 系统调用入口路径
- 系统调用出口路径
- 进程上下文切换
- 进程终止
- 小页面和大页面错误
- 进程信号传递
每个程序接收执行点的数据并进行处理,处理方式取决于系统调用的类型。对于简单的系统调用,参数直接复制到eBPF映射中临时存储,直到整个事件帧形成;对于更复杂的调用,eBPF程序会包含翻译或增强参数的逻辑,以便用户空间的Sysdig应用程序能够充分利用这些数据。
此外,Sysdig使用特殊的原生BPF函数将捕获的数据推送到一组CPU环形缓冲区,用户空间应用程序可以以非常高的吞吐量读取这些数据。从性能上看,Sysdig的eBPF模式的开销仅略高于传统的内核模块模式。
graph TD
A[系统调用执行点] --> B[eBPF程序]
B --> C{系统调用类型}
C -- 简单系统调用 --> D[eBPF映射临时存储]
C -- 复杂系统调用 --> E[eBPF程序处理逻辑]
D --> F[用户空间Sysdig应用程序]
E --> F
B --> G[原生BPF函数]
G --> H[CPU环形缓冲区]
H --> F
4. Flowmill
Flowmill是一家可观测性初创公司,源于其创始人Jonathan Perry的学术研究项目Flowtune。Flowtune研究如何在拥塞的数据中心网络中高效调度单个数据包,其中关键技术之一是能够以极低的开销收集网络遥测数据。Flowmill最终将这项技术应用于观察、聚合和分析分布式应用程序中各组件之间的连接,以实现以下目标:
- 提供分布式系统中服务交互的准确视图。
- 识别流量速率、错误或延迟发生显著变化的区域。
Flowmill使用eBPF内核探针来跟踪每个打开的套接字,并定期捕获操作系统指标。这一过程较为复杂,原因如下:
| 复杂原因 | 具体说明 |
| — | — |
| 连接覆盖 | 需要同时检测新连接和在eBPF探针建立时已打开的现有连接,并且要考虑内核中TCP、UDP以及IPv4、IPv6的代码路径。 |
| 容器关联 | 对于基于容器的系统,每个套接字必须关联到适当的cgroup,并与Kubernetes或Docker等平台的编排器元数据相结合。 |
| 网络地址转换 | 必须对通过conntrack执行的网络地址转换进行检测,以建立套接字与其外部可见IP地址之间的映射。例如,在Docker中,常用的网络模型使用源NAT将容器隐藏在主机IP地址后面;在Kubernetes中,使用服务虚拟IP地址来表示一组容器。 |
| 数据后处理 | eBPF程序收集的数据必须进行后处理,以按服务进行聚合,并匹配连接两侧收集的数据。 |
添加eBPF内核探针提供了一种更高效、更可靠的方式来收集这些数据,它完全消除了遗漏连接的风险,并且可以在亚秒级间隔内以低开销对每个套接字进行检测。Flowmill的方法依赖于一个代理,该代理结合了一组eBPF kprobes、用户空间指标收集以及离线聚合和后处理。其实现大量使用了Perf环将每个套接字上收集的指标传递到用户空间进行进一步处理,还使用哈希映射来跟踪打开的TCP和UDP套接字。
在设计eBPF检测方案时,Flowmill发现通常有两种策略:
-
“简单”策略
:找到在每个检测事件上调用的一到两个内核函数,但这需要BPF代码维护更多状态,并在非常频繁调用的检测点上执行更多工作。
-
Flowmill采用的策略
:检测调用频率较低但表示重要事件的更具体函数。这种方法开销显著降低,但需要更多精力来覆盖所有重要的代码路径,尤其是随着内核代码的演变。例如,
tcp_v4_do_rcv
函数可以捕获所有已建立的TCP RX流量并访问
sock
结构体,但调用量极高。用户可以检测处理ACK、乱序数据包处理、RTT估计等的函数,以处理影响已知指标的特定事件。
通过在TCP、UDP、进程、容器、conntrack等子系统中采用这种方法,系统实现了极高的性能,开销低到在大多数系统中难以测量。CPU开销通常为每个核心0.1% - 0.25%,主要取决于新套接字的创建速率。
graph TD
A[网络连接] --> B[eBPF内核探针]
B --> C{数据处理}
C -- 收集数据 --> D[Perf环]
C -- 记录套接字 --> E[哈希映射]
D --> F[用户空间代理]
E --> F
F --> G[离线聚合和后处理]
G --> H[分析结果]
5. 总结
Sysdig和Flowmill是使用BPF构建监控和可观测性工具的先驱,但并非仅有它们。许多公司如Cillium和Facebook等已将BPF作为首选框架,以提供高度安全和高性能的网络基础设施。BPF及其社区的未来充满希望,我们期待看到更多基于BPF的创新应用。
超级会员免费看
164

被折叠的 条评论
为什么被折叠?



