摘要
- 原文:Brendan Gregg’s Blog :《Golang bcc/BPF Function Tracing》,31 Jan 2017
- 引子:gdb、go execution tracer、GODEBUG、gctrace、schedtrace
- 一、gccgo Function Counting
- 二、Go gc Function Counting
- 三、Per-event invocations of a function
- 四、Interface Arguments
- 五、Function Latency
- 六、总结
- 七、Tips:构建 LLVM 和 Clang 开发工具库
在这篇文章中,我将迅速调研一种跟踪的 Go 程序的新方法:基于 Linux 4.x eBPF 实现动态跟踪。如果你去搜索 Go 和 BPF,你会发现使用 BPF 接口的 Go 语言接口(例如,gobpf)。这不是我所探索的东西:我将使用 BPF 工具实现 Go 应用程序的性能分析和调试。
目前已经有多种调试和追踪 Go 程序的方法,包括但不限于:
- gdb
go execution tracer :用于高层异常和阻塞事件
Go execution tracer. (import “runtime/trace”)
The tracer captures a wide range of execution events like goroutine creation/blocking/unblocking, syscall enter/exit/block, GC-related events, changes of heap size, processor start/stop, etc and writes them to an io.Writer in a compact form. A precise nanosecond-precision timestamp and a stack trace is captured for most events. A trace can be analyzed later with ‘go tool trace’ command.GODEBUG (一个跨平台的Go程序调试工具)、 gctrace 和 schedtrace
BPF 追踪以做很多事,但都有自己的优点和缺点,接下来将详细说明。首先我从一个简单的 Go 程序开始( hello.go)
1 | package main |
一、gccgo Function Counting
我开始会使用 gccgo 编译,然后使用 Go gc 编译器 。(区别:gccgo 可以生成优化后的二进制文件,但是基于老版本的 Go。)
1 | ## 编译 |
现在我将使用 bcc 工具的 funccount 来动态跟踪和计数所有以 “fmt.” 开头的 Go 库函数调用,在另一个终端重新运行 Hello 程序效果如下:
1 | # funccount 'go:fmt.*' |
Neat! 输出结果中包含该程序的 fmt.Println() 函数调用。
我不需要进入任何特殊的模式才能实现这个效果,对于一个已经在运行的 Go 应用我可以直接开始测量而不需要重启进程。 So how does it even work? 这要归功于 uprobes ,Linux 3.5 新增的特性,详见Linux uprobes: User-Level Dynamic Tracing 。
It overwrites instructions with a soft interrupt to kernel instrumentation, and reverses the process when tracing has ended.
gccgo 编译的输出提供一个标准的符号表用于函数查找。在这种情况下,我利用 libgo 当测量工具(假定“lib”在“go:”之前),作为 gccgo 发出的一个二进制动态链接库(libgo 包含 fmt 包)。uprobes 可以连接到已经运行的进程,或者像我现在一样作为一个二进制库,捕捉所有调用自己的进程。
为了提高效率,我在内核上下文中进行函数调用计数,只将计数发送到用户空间。例如:
1 | $ file hello |
这些内容看起来非常像一个编译过的 C 语言二进制程序,因此可以使用包括 bcc/BPF在内的许多现有的调试工具和追踪器观测。相对于测量即时编译的运行时要简单得多(例如 Java 和 Node.js)。到目前为止,这个例子唯一的麻烦事函数名称中可能包含非标准的字符,例如“.”。
funccount also has options like -p to match a PID, and -i to emit output every interval. It currently can only handle up to 1000 probes at a time, so “fmt.*“ was ok, but matching everything in libgo:
funccount 提供 -p 选项来匹配进程号(PID),-i 选项来控制输出频率。它目前能够同时处理 1000 个探测点,匹配 “fmt.*” 时运行正常,但是匹配 libgo 的所有函数就出现异常。诸如此类的问题在 bcc/BPF 中还有不少,我们需要寻找其它的方法来处理。
1 | # funccount 'go:*' |
二、Go gc Function Counting
使用 Go 语言的 gc 编译器实现 fmt 函数调用计数:
1 | $ go build hello.go |
1 | # funccount '/home/bgregg/hello:fmt.*' |
你依然能够追踪到 fmt.Println() ,这个二进制程序与 libgo 有所不同:包含该函数的是一个 2M 的静态库(而非动态库的 29K )。另一个区别就是函数名称包含更多特殊字符,例如 “*“, “(“,等等,我怀疑如果不能修正处理的haul将影响其它调试器(例如 bcc 追踪器)。
1 | $ file hello |
三、Per-event invocations of a function
3.1 gccgo Function Tracing
现在我将尝试使用 Sasha Goldshtein 的追踪工具,也是基于 bcc,用来查看每一个函数调用事件。我将回到 gccgo,使用一个非常简单的示例程序( from the go tour ),functions.go:
1 | package main |
追踪 add() 函数。所有参数都输出在右侧,trace 还有其他选项(帮助 -h ),例如输出时间戳和堆栈。
1 | \# trace '/home/bgregg/functions:main.add' |
3.2 Go gc Function Tracing
同样的程序,如果使用 go build 就没有 main.add() ? 禁用代码嵌入( Disabling inlining)即可。
1 | $ go build functions.go |
1 | $ go build -gcflags '-l' functions.go |
That’s wrong. 参数应该是 42 和 13 而不是 536912504 和 16。
使用 gdb 查看结果如下:
1 | $ gdb ./functions |
启动信息中包含一个关于 runtime-gdb.py 的警告,它非常有用:如果需要进一步深入挖掘 Go 上下文,我希望能够修复并找出告警原因。即使没有该信息,gdb 依然可以输出参数变量的值是 “x=42, y=13”。我也将它们从寄存器导出与 x86_64 ABI(Application Binary Interface,应用程序二进制接口)对比,which is how bcc’s trace reads them. From the syscall(2) man page:
1 | arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes |
The reason is that Go’s gc compiler is not following the standard AMD64 ABI function calling convention, which causes problems with this and other debuggers.
42 和 13 没有出现在 rdi , rsi 或者其它任何一个寄存器。原因是 Go 的 gc 编译器不遵循标准的 AMD64 ABI 函数调用约定,其它调试器也会存在这个问题。这很烦人。我猜 Go 语言的返回值使用的是另外一种 ABI,因为它可以返回多个值,所以即使入口参数是标准的,我们仍然会遇到差异。我浏览了指南(Quick Guide to Go’s Assembler and the Plan 9 assembly manual),看起来函数在堆栈上传递。这些是我们的 42 和 13:
1 | (gdb) x/3dg $rsp |
BPF can dig these out too. As a proof of concept, I just hacked in a couple of new aliases, “go1” and “go2” for those entry arguments:
BPF 也可以挖掘这些信息。为了验证这一个概念,我为入口参数声明一对新的别名“go1”和“go2” 。希望您阅读本文的时候,我(或者其他人)已经将它加入到 bcc 追踪工具中,最好是 “goarg1”, “goarg2”, 等等。
1 | # trace '/home/bgregg/functions:main.add "%d %d" go1, go2' |
四、Interface Arguments
你可以写一个自定义的 bcc/BPF 程序来挖掘,为了处理接口参数我们可以给 bcc 的跟踪程序添加多个别名。输入参数是接口的示例:
1 | func Println(a ...interface{}) (n int, err error) { |
1 | $ gdb ./hello |
五、Function Latency
- 示例:循环调用 fmt.Println() 函数的时延直方图(纳秒)
WARNING: Go 函数调用过程中,如果从一个进程(goroutine)切换到另外一个系统进程,funclatency 无法匹配入口-返回。这种场景需要一个新的工具 —— gofunclatency ,它基于 Go 内建的 GOID 替代系统的 TID 追踪时延,在某些情况下, uretprobes 修改 Go 程序可能出现崩溃的问题,因此在调试之前需要准备周全的计划。
1 | # funclatency 'go:fmt.Println' |
六、总结
原作者总结很简洁,不再赘述。
I took a quick look at Golang with dynamic tracing and Linux enhanced BPF, via bcc’s funccount and trace tools, with some successes and some challenges. Counting function calls works already. Tracing function arguments when compiled with gccgo also works, whereas Go’s gc compiler doesn’t follow the standard ABI calling convention, so the tools need to be updated to support this. As a proof of concept I modified the bcc trace tool to show it could be done, but that feature needs to be coded properly and integrated. Processing interface objects will also be a challenge, and multi-return values, again, areas where we can improve the tools to make this easier, as well as add macros to C for writing other custom Go observability tools as well.
七、Tips
6.1 安装和编译 BCC
1 | git clone https://github.com/iovisor/bcc.git |
6.2 构建 LLVM 和 Clang 开发工具库
1 | yum install gcc |
6.3 LLVM 与 Clang
LLVM (Low Level Virtual Machine)是一种编译器基础设施,以C++写成。起源于2000年伊利诺伊大学厄巴纳-香槟分校维克拉姆·艾夫(Vikram Adve)与克里斯·拉特纳(Chris Lattner)的研究,他们想要为所有静态及动态语言创造出动态的编译技术。2005年,Apple直接雇用了克里斯·拉特纳及他的团队,为了苹果电脑开发应用程序,期间克里斯·拉特纳设计发明了 Swift 语言,LLVM 成为 Mac OS X 及 iOS 开发工具的一部分。LLVM 的范围早已经不局限于创建一个虚拟机,成为了众多编译工具及低级工具技术的统称,适用于LLVM下的所有项目,包含LLVM中介码(LLVM IR)、LLVM除错工具、LLVM C++标准库等。
目前 LLVM 已支持包括ActionScript、Ada、D语言、Fortran、GLSL、Haskell、Java字节码、Objective-C、Swift、Python、Ruby、Rust、Scala1以及C#等语言。
Clang 是LLVM编译器工具集的前端(front-end),目的是输出代码对应的抽象语法树(Abstract Syntax Tree, AST),并将代码编译成LLVM Bitcode。接着在后端(back-end)使用 LLVM 编译成平台相关的机器语言 。Clang支持C、C++、Objective C。它的目标是提供一个 GCC 的替代品。作者是克里斯·拉特纳(Chris Lattner),在苹果公司的赞助支持下进行开发。Clang项目包括Clang前端和Clang静态分析器等。
6.4 ABI
应用二进制接口(Application Binary Interface, ABI)描述了应用程序和操作系统之间或其他应用程序的低级接口。ABI涵盖了各种细节,如:
- 数据类型的大小、布局;
- 调用约定(控制着函数的参数如何传送以及如何接受返回值),例如,是所有的参数都通过栈传递,还是部分参数通过寄存器传递;哪个寄存器用于哪个函数参数;通过栈传递的第一个函数参数是最先push到栈上还是最后;
目标文件的二进制格式、程序库等等。
ABI vs API
应用程序接口 (API)定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译,然而ABI允许编译好的目标代码在使用兼容 ABI 的系统中无需改动就能运行。
扩展阅读
动态追踪技术
- 操作系统原理 | How Linux Works(一):How the Linux Kernel Boots
- 操作系统原理 | How Linux Works(二):User Space & RAM
- 操作系统原理 | How Linux Works(三):Memory
- 动态追踪技术(一):DTrace 导论
- 动态追踪技术(二):strace+gdb 溯源 Nginx 内存溢出异常
- 动态追踪技术(三):Tracing Your Kernel Function!
- 动态追踪技术(四):基于 Linux bcc/BPF 实现 Go 程序动态追踪
- 动态追踪技术(五):Welcome DTrace for Linux