专栏首页Serverless+[译] 第 1 部分: 在生产环境中使用 eBPF 调试 Go 程序

[译] 第 1 部分: 在生产环境中使用 eBPF 调试 Go 程序

这是本系列文章的第一篇, 讲述了我们如何在生产环境中使用 eBPF 调试应用程序而无需重新编译/重新部署. 这篇文章介绍了如何使用 gobpf 和 uprobe 来为 Go 程序构建函数参数跟踪程序. 这项技术也可以扩展应用于其他编译型语言, 例如 C++, Rust 等. 本系列的后续文章将讨论如何使用 eBPF 来跟踪 HTTP/gRPC/SSL 等.

简介

在调试时, 我们通常对了解程序的状态感兴趣. 这使我们能够检查程序正在做什么, 并确定缺陷在代码中的位置. 观察状态的一种简单方法是使用调试器来捕获函数的参数. 对于 Go 程序来说, 我们经常使用 Delve 或者 GDB.

在开发环境中, Delve 和 GDB 工作得很好, 但是在生产环境中并不经常使用它们. 那些使调试器强大的特性也让它们不适合在生产环境中使用. 调试器会导致程序中断, 甚至允许修改状态, 这可能会导致软件产生意外故障.

为了更好地捕获函数参数, 我们将探索使用 eBPF(在 Linux 4.x+ 中可用) 以及高级的 Go 程序库 gobpf.

eBPF 是什么 ?

扩展的 BPF(eBPF) 是 Linux 4.x+ 里的一项内核技术. 你可以把它想像成一个运行在 Linux 内核中的轻量级的沙箱虚拟机, 可以提供对内核内存的经过验证的访问.

如下概述所示, eBPF 允许内核运行 BPF 字节码. 尽管使用的前端语言可能会有所不同, 但它通常是 C 的受限子集. 一般情况下, 使用 Clang 将 C 代码编译为 BPF 字节码, 然后验证这些字节码, 确保可以安全运行. 这些严格的验证确保了机器码不会有意或无意地破坏 Linux 内核, 并且 BPF 探针每次被触发时, 都只会执行有限的指令. 这些保证使 eBPF 可以用于性能关键的工作负载, 例如数据包过滤, 网络监控等.

从功能上讲, eBPF 允许你在某些事件(例如定时器, 网络事件或函数调用)触发时运行受限的 C 代码. 当在函数调用上触发时, 我们称这些函数为探针, 它们既可以用于内核里的函数调用(kprobe) 也可以用于用户态程序中的函数调用(uprobe). 本文重点介绍使用 uprobe 来动态跟踪函数参数.

Uprobe

uprobe 可以通过插入触发软中断的调试陷阱指令(x86 上的 int3)来拦截用户态程序. 这也是调试器的工作方式. uprobe 的流程与任何其他 BPF 程序基本相同, 如下图所示. 经过编译和验证的 BPF 程序将作为 uprobe 的一部分执行, 并且可以将结果写入缓冲区.

BPF for tracing (from Brendan Gregg)

让我们看看 uprobe 是如何工作的. 要部署 uprobe 并捕获函数参数, 我们将使用这个简单的示例程序. 这个 Go 程序的相关部分如下所示.

main() 是一个简单的 HTTP 服务器, 在路径 /e 上公开单个GET 端点, 该端点使用迭代逼近来计算欧拉数(e). computeE接受单个查询参数(iterations), 该参数指定计算近似值要运行的迭代次数. 迭代次数越多, 近似值越准确, 但会消耗指令周期. 理解函数背后的数学并不是必需的. 我们只是想跟踪对computeE 的任何调用的参数.

// computeE computes the approximation of e by running a fixed number of iterations.
func computeE(iterations int64) float64 {
  res := 2.0
  fact := 1.0

  for i := int64(2); i < iterations; i++ {
    fact *= float64(i)
    res += 1 / fact
  }
  return res
}

func main() {
  http.HandleFunc("/e", func(w http.ResponseWriter, r *http.Request) {
    // Parse iters argument from get request, use default if not available.
    // ... removed for brevity ...
    w.Write([]byte(fmt.Sprintf("e = %0.4f\n", computeE(iters))))
  })
  // Start server...
}

要了解 uprobe 的工作原理, 让我们看一下二进制文件中如何跟踪符号. 由于 uprobe 通过插入调试陷阱指令来工作, 因此我们需要获取函数所在的地址. Linux 上的 Go 二进制文件使用 ELF 存储调试信息. 除非删除了调试数据, 否则即使在优化过的二进制文件中也可以找到这些信息. 我们可以使用 objdump 命令检查二进制文件中的符号:

[0] % objdump --syms app|grep computeE
00000000006609a0 g     F .text    000000000000004b              main.computeE

从这个输出中, 我们知道函数 computeE 位于地址 0x6609a0. 要看到它前后的指令, 我们可以使用 objdump 来反汇编二进制文件(通过添加 -d 选项实现). 反汇编后的代码如下:

[0] % objdump -d app | less
00000000006609a0 <main.computeE>:
  6609a0:       48 8b 44 24 08          mov    0x8(%rsp),%rax
  6609a5:       b9 02 00 00 00          mov    $0x2,%ecx
  6609aa:       f2 0f 10 05 16 a6 0f    movsd  0xfa616(%rip),%xmm0
  6609b1:       00
  6609b2:       f2 0f 10 0d 36 a6 0f    movsd  0xfa636(%rip),%xmm1

由此可见, 当 computeE 被调用时会发生什么. 第一条指令是 mov 0x8(%rsp), %rax. 它把 rsp 寄存器偏移 0x8 的内容移动到 rax 寄存器. 这实际上就是上面的输入参数 iterations. Go 的参数在栈上传递.

有了这些信息, 我们现在就可以继续深入, 编写代码来跟踪 computeE 的参数了.

构建跟踪程序

要捕获事件, 我们需要注册一个 uprobe 函数, 还需要一个可以读取输出的用户空间函数. 如下图所示. 我们将编写一个称为跟踪程序的二进制文件, 它负责注册 BPF 代码并读取 BPF 代码的结果. 如图所示, uprobe 简单地写入 perf buffer, 这是用于 perf 事件的 Linux 内核数据结构.

High-level overview showing the Tracer binary listening to perf events generated from the App

现在, 我们已了解了涉及到的各个部分, 下面让我们详细研究添加 uprobe 时发生的情况. 下图显示了 Linux 内核如何使用uprobe 修改二进制文件. 软中断指令(int3)作为第一条指令被插入 main.computeE 中. 这将导致软中断, 从而允许 Linux 内核执行我们的 BPF 函数. 然后我们将参数写入 perf buffer, 该缓冲区由跟踪程序异步读取.

Details of how a debug trap instruction is used call a BPF program

BPF 函数相对简单, C代码如下所示. 我们注册这个函数, 每次调用 main.computeE 时都将调用它. 一旦调用, 我们只需读取函数参数并写入 perf buffer. 设置缓冲区需要很多样板代码, 可以在完整的示例中找到.

#include <uapi/linux/ptrace.h>

BPF_PERF_OUTPUT(trace);

inline int computeECalled(struct pt_regs *ctx) {
  // The input argument is stored in ax.
  long val = ctx->ax;
  trace.perf_submit(ctx, &val, sizeof(val));
  return 0;
}

现在我们有了一个用于 main.computeE 函数的功能完善的端到端的参数跟踪程序! 下面的视频片段展示了这一结果.

End-to-End demo

另一个很棒的事情是, 我们可以使用 GDB 来查看对二进制文件所做的修改. 在运行我们的跟踪程序之前, 我们输出地址 0x6609a0 的指令.

(gdb) display /4i 0x6609a0
10: x/4i 0x6609a0
   0x6609a0 <main.computeE>:    mov    0x8(%rsp),%rax
   0x6609a5 <main.computeE+5>:  mov    $0x2,%ecx
   0x6609aa <main.computeE+10>: movsd  0xfa616(%rip),%xmm0
   0x6609b2 <main.computeE+18>: movsd  0xfa636(%rip),%xmm1

而这是在我们运行跟踪程序之后. 我们可以清楚地看到, 第一个指令现在变成 int3 了.

(gdb) display /4i 0x6609a0
7: x/4i 0x6609a0
   0x6609a0 <main.computeE>:    int3
   0x6609a1 <main.computeE+1>:  mov    0x8(%rsp),%eax
   0x6609a5 <main.computeE+5>:  mov    $0x2,%ecx
   0x6609aa <main.computeE+10>: movsd  0xfa616(%rip),%xmm0

尽管我们为该特定示例对跟踪程序进行了硬编码, 但是这个过程是可以通用化的. Go 的许多方面(例如嵌套指针, 接口, 通道等)让这个过程变得有挑战性, 但是解决这些问题可以使用现有系统中不存在的另一种检测模式. 另外, 因为这一过程工作在二进制层面, 它也可以用于其他语言(C++, Rust 等)编译的二进制文件. 我们只需考虑它们各自 ABI 的差异.

下一步是什么 ?

使用 uprobe 进行 BPF 跟踪有其自身的优缺点. 当我们需要观察二进制程序的状态时, BPF 很有用, 甚至在连接调试器会产生问题或者坏处的环境(例如生产环境二进制程序). 最大的缺点是, 即使是最简单的程序状态的观测性, 也需要编写代码来实现. 编写和维护 BPF 代码很复杂. 没有大量高级工具, 不太可能把它当作一般的调试手段.

原文链接:https://blog.pixielabs.ai/ebpf-function-tracing/post/

原文作者:Zain Asgar

登录 后参与评论
0 条评论

相关文章

  • 在生产环境中使用 eBPF 调试 GO 程序

    这是本系列文章的第一篇,讲述了我们如何在生产环境中使用 eBPF 调试应用程序而无需重新编译/重新部署。这篇文章介绍了如何使用 gobpf 和 uprobe 来...

    Linux阅码场
  • 使用 eBPF 在生产环境调试 Go 应用

    本文是描述我们如何在生产中使用 eBPF 调试应用程序的系列文章中的第一篇,无需重新编译/重新部署,这篇文章介绍了如何使用 gobpf[1] 和uprobes ...

    我是阳明
  • eBPF 概述:第 3 部分:软件开发生态

    在本系列的第 1 部分和第 2 部分中,我们对 eBPF 虚拟机进行了简洁的深入研究。阅读上述部分并不是理解第 3 部分的必修课,尽管很好地掌握了低级别的基础知...

    用户7686797
  • 观察HTTP/2流量是困难的,但eBPF可以帮助

    在当今充满微服务的世界中,获取服务之间发送的消息的可观察性对于理解和排除问题至关重要。

    CNCF
  • 大规模微服务利器:eBPF + Kubernetes 介绍

    本文翻译自 2020 年 Daniel Borkmann 在 KubeCon 的一篇分享: eBPF and Kubernetes: Little Helper...

    CNCF
  • 基于 eBPF 的 Linux 可观测性

    最近发布的 Linux 内核带了一个针对内核的能力强大的 Linux 监控框架。它起源于历史上人们所说的的 BPF。

    黑光技术
  • 为容器时代设计的高级 eBPF 内核特性(FOSDEM, 2021)

    本文翻译自 2021 年 Daniel Borkmann 在 FOSDEM 的一篇分享:Advanced eBPF kernel features for th...

    米开朗基杨
  • eBPF 入门教程

    有兴趣了解更多关于 eBPF 技术的底层细节?那么请继续移步,我们将深入研究 eBPF 的底层细节,从其虚拟机机制和工具,到在远程资源受限的嵌入式设备上运行跟踪...

    米开朗基杨
  • eBPF 概述:第 1 部分:介绍

    有兴趣了解更多关于 eBPF 技术的底层细节?那么请继续移步,我们将深入研究 eBPF 的底层细节,从其虚拟机机制和工具,到在远程资源受限的嵌入式设备上运行跟踪...

    用户7686797
  • 区块链开发语言之go语言学习线路指导

    问题导读 1.为什么学习go语言? 2.你认为该如何入门go语言? 3.你认为go语言需要哪些学习过程?

    用户1410343
  • 基于eBPF的微服务网络安全(Cilium 1)

    翻译自:Network security for microservices with eBPF

    charlieroro
  • 【eBPF笔记前篇】介绍、开发环境搭建、原理简介、case

    之前一个老板说“xxx组的同学是一定要把eBPF用到得心应手”,因为之前是做性能压测相关工作,个人感觉压测其实并不复杂,复杂的是压测后的问题定位,而eBPF则是...

    历久尝新
  • 使用 eBPF 调试 Python 容器

    随着 Docker/Kubernetes 等容器技术的盛行,越来越多的 Python 应用已经运行在容器中了。在带来便利性的同时,也让生产环境中的 debug ...

    Jintao Zhang
  • eBPF 如何简化服务网格

    本文译自 How eBPF Streamlines the Service Mesh[1]。

    CNCF
  • P99 Conf Talk 汇总 | Rust 在高性能低延迟系统中的应用

    P99 Conf[1] 是一个由 Scylladb[2] 组织的新的跨行业的线上Conf,为工程师而设。该活动以低延迟、高性能设计为中心,范围包括操作系统(内核...

    张汉东
  • eBPF 的发展历史和核心设计

    本文翻译自 2016 年 Daniel Borkman 在 NetdevConf 大会上的一篇文章:On getting tc classifier fully...

    米开朗基杨
  • 【eBPF笔记中篇】运行原理、交互、event触发 解析(未完)

    从之前的分析已经得知,.c的eBPF程序会通过BCC等工具编译并加载到内核中,但是具体在内核中,ebpf是如何工作的呢?

    历久尝新
  • 开源持续性能剖析平台 Pyroscope

    性能剖析是动态代码分析的一种形式,你可以在应用运行时捕获应用的特征,然后使用这些特征信息确定如何使应用更快、更高效。但是对于线上生产环境来说很难捕获到现场,所以...

    我是阳明
  • 【玩转腾讯云】ebpf 学习梳理和测试使用

    周五下午在公司的服务网格月度讨论会上,一位同事为大家分享了在服务网格中使用 ebpf 来优化提升 istio 中 sidecar 和 RS 间的通信效率。听过之...

    黑光技术

扫码关注腾讯云开发者

领取腾讯云代金券