Tubi 赞助的第八期 Elixir Meetup 于五月底顺利结束,三位 Elixir 资深使用者 Horvo、Scott 和杨淼,与线上线下的 660+ 函数式编程爱好者分享了 Elixir 的相关应用与实践。本文回顾了杨淼带来的分享“了解 eBPF 从这些例子开始”。
欢迎关注比图科技公众号,了解 Elixir 最新资讯及活动。
eBPF是 Linux 内核中的一项技术,其全称为 Extended Berkeley Packet Filter。经典的 BPF(Berkeley Packet Filter)最初是为了网络流量过滤而设计的(例如,tcpdump 命令就使用了 BPF 做数据包的过滤)。经典 BPF 出现于1992 年,在 1997 年合并到 Linux 系统,此后的多年都没有重大进展。
2014 年初,Alexei Starovoitov 实现了 eBPF。与经典 BPF 不同的是,eBPF 是更高级的拓展,比如拓展了指令集,引入了高级语言的支持,引入了安全机制,JIT,Maps 等。因此,eBPF 有着更广泛的应用场景,如性能分析、安全审计、应用程序跟踪等。
eBPF 的工作原理是在内核中插入一个虚拟机,该虚拟机可以执行安全的、受限制的代码。这些代码可以用来过滤和修改网络数据包、监控和分析系统性能、控制应用程序行为等。
eBPF 程序是事件驱动的,在内核或应用程序通过某个钩点时运行,即可以通过用户空间的工具和 API 来加载和执行,或者通过内核事件、网络流量等触发器自动执行。
如果前面对 eBPF 的定义还不够直观,我们可以这样简单地理解:给你提供了一个切入点,你可以在这个切入点放入一段代码,这些代码可以在内核中安全地运行,执行一些处理(当然是有限的处理)。学过 Java 的朋友,看到这里应该会想到 AOP(即 Aspect Oriented Programming 面向切面编程),eBPF 的原理和 Java AOP 在思想上类似。Java 中的 AOP 允许我们在正常的业务代码中加入“切入点”(通常是一个方法,用注解标注),然后我们可以在“切入点”加入“切面逻辑”。
USDT 全称为 Userland Statically Defined Tracing,即用户空间静态定义跟踪。它是一种在用户空间程序中定义的静态跟踪点,用于在程序运行时收集和分析程序的性能数据和行为信息。很多程序都添加了对 USDT 的支持,如 Erlang、Ruby、Java、MySql 等。下面我们看一个监测 Erlang 垃圾回收的代码例子(该程序需要使用 bpftrace 执行,bpftrace 是基于 eBPF 的高级语言,让 eBPF 程序写起来容易):
usdt:beam.smp:gcminorstart
{
@start[srt(arg0)] = nsecs;
}
usdt:beam.smp:gcminorend
{
@usecs= hist((nsecs -@start[str(arg0)]) / 1000);
delete(@start[str(arg0)]);
}
END
{
clear(@start);
}
解释一下这些代码:
· usdt:beam.smp:gc_minor__start:表示在垃圾回收开始时触发的探测点,用于记录当前时间戳(以纳秒为单位)
· usdt:beam.smp:gc_minor__end:表示在垃圾回收结束时触发的探测点,用于计算垃圾回收过程的耗时。其中,@start 和 @usecs 都是 BPF Maps,用于保存和统计数据。@start 是一个关联数组,以字符串类型的 arg0 作为索引,保存每个垃圾回收开始时的时间戳。@usecs 是一个柱状图(Histogram),用于记录垃圾回收过程的耗时(以微秒为单位),并将其分成多个区间,进行统计和聚合
· end 函数中计算了时间戳之差,得到垃圾回收过程耗时,并将其记录到 @usecs 中,delete 调用是从 @start 数组中删除当前垃圾回收的开始时间戳
· end 块中,使用 clear 函数清空 @start 数组,以便下次垃圾回收监控。
在前面的 USDT 例子中,我们了解到用户程序需要有 Hook,我们才能做事情,即探测点是预定义的。如果用户没提供,我们也可以用 uprobes 来做追踪和检测。
uprobes 是用户级别的动态跟踪,允许开发人员在用户空间程序的任意函数入口和出口处插入探针(probe),以便在运行时收集有关函数调用和返回的信息。这些探针可以用于调试、分析性能和安全审计等方面。其原理就是在代码指令出插入一个 breakpoint,当执行到这个指令的时候,可以调到我们指定的 handler 来执行,然后就可以在 handler 中做读改参数之类的操作。在整个 eBPF 跟踪架构中,uprobes 的位置如下:
Auto-Instrumentation for Go 是一个基于 uprobes 的项目,可以用来追踪 Go 应用。下面的代码演示了读取 Go 的请求:
Traffic Control(TC)是 Linux 内核中的一个网络流量控制机制,用于实现对网络流量的限速、延迟、丢包等控制。TC Hooks 顾名思义就是 TC 钩子,我们可以在网络数据经过 TC 链时插入 eBPF 程序,用来实现类似监控这样的目的。
TC 框架中包含了许多组件,其中包括分类器(Classifier)、队列(Queue)、队列调度器(Queueing Discipline)等,TC Hooks 可以作用于这些组件中的任意一个节点,最常用的是队列调度器(Queueing Discipline),如下图所示:
通常,我们的软件应用会有很多模块,用户的一个请求会经历多个服务调用,如果我们想构建一个 Request Flow,该怎么做呢?最普遍的做法是 TraceId,把一个 TraceId 不断传下去得到一个 Request Flow。但这种做法需要修改代码,如果应用较多,甚至是多语言,修改成本及维护成本都会很高。如果我们潜到下一层,利用 TC Hook 做手脚,可以极大地降低成本(大约是 N 乘 M 到 M),其逻辑如下图所示:
下面的代码演示了如何使用 eBPF Hook HTTP 请求(该程序需要使用 bcc 执行,bcc 是一个工具集,提供了更容易使用的 API,让 eBPF 程序写起来容易):
如上面代码所示,将 eBPF 代码通过 TC 挂到内核中执行(上面 sniff.c 在内核中执行),在内核读取数据,将数据传到用户程序,用户程序便可以解析原始数据了。上面的程序得到的结果输出如下:
关于 eBPF 的安全问题
在 Elixir Meetup 现场,我们看到了 eBPF 的强大,可以在内核执行代码;自然而言,我们因此也会关心其安全问题。eBPF 如何保证安全,Panic 了怎么办?
首先,大部分 eBPF 程序需要相应的权限才能加载,比如 root、CAP_EBPF。其次,Kernel 会使用 verifier 进行检查,verifer 会确保 eBPF 的内存安全和程序会会终止(termination),以及只能调用制定的方法,如下图所示:
关于 eBPF 的一些开源项目
eBPF 曾被称为操作系统在过去几十年的最大变革,事实上有很多公司都有 eBPF 的实践,社区也有很多关于 eBPF 的开源项目。下面列举一些:
· Katran [网络- 4 层负载均衡] :Facebook 开源的高性能 4 层负载均衡器。Katran 是一个 Cpp 库和 eBPF 程序,用于建立一个高性能的第四层负载平衡转发平面
· Cilium [网络/安全/观测] :Isovalent 开源项目,提供由 eBPF 驱动的网络、安全和可观察性。它是专门为 Kubernetes 世界带来 BPF 的优势而设计的,以解决容器工作负载的新的可扩展性、安全性和可视性要求
· BCC [开发工具] :是一个建立在 eBPF 基础上的高效内核跟踪和操作程序的工具包,它包括几个有用的命令行工具和例子。BCC 简化了用 C 语言编写 BPF 程序的过程,包括一个围绕 LLVM 包装器,以及 Python 和 Lua 的前端绑定
· bmc-cache(memcache+eBPF):通过 eBPF 使 Memcached 吞吐量提升到原有的 18 倍。简单来说,bmc-cache 通过 eBPF 做 kernel-bypass,同时使 Memcached 性能可以更好地随着 CPU 的增长而增长。
如果你对 eBPF 感兴趣,想深入学习和研究,也推荐你读一读这个技术博客www.ebpf.top/。
以上分享来自现场观众德明,感谢为 Elixir 社区做的贡献!
使用 Elixir/OTP 构建多媒体 E2E 处理平台
Ruby 思想在 Elixir 项目中的应用
一个潜藏在 Elixir 代码库里 7 年的性能问题
热招岗位:https://tubi.tech/careers/
微信公众号:【活动回顾】了解 eBPF 从这些例子开始