在Golang中利用BPF进行动态追踪

Part 1 背景

Golang作为云原生领域中使用最广泛的编程技术,是我们MatrixOrigin数据库主力开发语言。Golang本身提供了pprof性能剖析工具,可以让我们快速,粗略的分析性能瓶颈,在日常开发中广泛使用。然而,随着性能调优要求不断升高,我们需要更精准的性能指标,比如某个特定golang函数的执行时间,此时pprof就无能为力了,我们必须另想办法。
第一反应,我们可以考虑人工加入计时函数,然后重新编译代码执行。该方案当然可行,但是缺点也很明显,对每个需要分析的函数都要修改代码,重新编译执行。该方案工作量比较大,也不易维护。如果是线上系统,往往无法修改代码。
那么,是否有更好的方法来测量特定函数的时延?最好是不用修改代码并重新编译。答案是:Yes。


Part 2 历史

01 Uprobe

如果想在不修改代码的前提下,实现修改代码测量的功能,那么自然而然的选择,则在程序运行时动态的修改程序的代码段,加入我们自定义逻辑。
Linux开发者们,提供了在程序运行时,动态修改程序代码的功能——uprobe。它可以修改指定汇编代码为int3指令,并把原指令保存起来。当应用程序运行到int3指令时,便会触发异常,切换到Linux内核态,这里可以执行提前好的测量逻辑。随后返回用户空间,执行最开始被int3替换并保存的指令。在应用程序看来,一切都毫无感觉。而我们的测量逻辑,记录了当前时间。如果我们在函数开始和结束,各记录一次,则可以通过计算差值,得到函数的执行时间。

02 BPF

Uprobe机制,提供了动态追踪应用程序的基础框架。然而,它依旧很难使用,测量逻辑需要写Linux内核模块,这对大多数应用程序开发者来说,使用门槛依旧很高。
随着BPF技术横空出世,Linux内核开发者们,将其整合进入了uprobe框架。从而可以使用类似C的代码,编写动态追踪逻辑。这比Linux内核模块实现,要简单多了。

03 Bpftrace

为了进一步降低uprobe + BPF 的使用门槛,开发者们设计并实现了bpftrace工具,它作为BPF的前端,利用类似于脚本语法的简单方式,编译生成BPF程序并自动加载。这在我们分析应用程序时,可以非常快速的实现动态测量程序。也可以提前准备好脚本,直接拿来使用。
Bpftrace原本设计主要是针对C语言开发的应用场景,那么它可以用在golang环境中吗?答案是可以,但是需要调整。接下来的篇幅,将介绍如何让bpftrace在golang环境中正确的使用。


Part 3 当BPF遇上Golang

01 问题

Golang作为一种编译型语言,理论上可以直接使用uprobe + BPF进行动态追踪。然而,golang与C语言相比,又有微妙的不同之处,这里用go 1.19 + x86环境为例子:

  1. Golang ABI与C语言相比,C语言函数只使用寄存器传递参数。然而golang不确定使用寄存器还是使用栈来传递函数参数(要么用寄存器,要么用栈传递,不会混合),编译器有相关算法来决定,具体算法规则可以参考golang源码的src/cmd/compile/internal-abi.md文档。golang函数传递参数的方式的不同,这意味着使用bpftrace动态追踪时,获取被追踪函数的参数,需要先确认它是利用寄存器传参,还是通过栈传参,然后调用不同的bpftace函数来获取。我个人的方法是查看该golang函数的汇编语言,人工判断。目前还没想出更智能的办法。
  2. Golang原生支持协程,它的函数栈与C语言相比,初始非常小,并且随着栈深度增加,就会发生栈扩张,这会把旧的栈复制到更大的新栈内存中。golang这一特性,意味着在C语言中,稳定使用的对函数返回的uretprobe动态追踪技术(需要修改函数栈),直接应用在golang中,当发生栈扩张时,就会导致程序错误。因此,想要正确追踪golang函数的返回,只能遍历函数的汇编代码,跟踪所有ret汇编指令。
  3. Golang的函数参数个数和顺序,不能从字面来判断。编译器会可能会进行改写。比如字符串引用,golang会把它扩张成地址和长度两个参数。目前,笔者还没有找到很好的方法可以找到改写后的参数列表。
  4. Golang所有函数都在协程中运行,这意味着函数一次执行过程中,可能会从A线程切换到B线程。而C语言的函数,在一次执行中,只会在一个线程中,直到执行完成。这意味着golang函数执行的上下文,不能用线程号来唯一的确定。幸运的是,我们依旧有办法唯一的标识golang协程上线文。每个golang协程,都有type gobuf struct {...} 实例表示。追踪 rumtime/proc.go:execute(gp g, inheritTime bool) 函数,记录下g,这就可以唯一的标记当前协程上下文。
  5. Golang函数符号,可能会带有(,*之类符号。bpftrace对这类情况支持并不太好,或许之后会解决。目前我们可以用地址来替换函数名,绕过这个问题。

02 实现

开源项目
https://github.com/stevenjohn...
很好的解决了BPF在golang中的适配问题。我们聚焦在latency.bt 脚本上,它可以测量任意golang函数的调用时延的直方图。
编译
go build
生成对特定函数的测量脚本

./go-bpf-gen templates/latency.bt ../../matrixone/mo-service symbol="github.com/matrixorigin/matrixone/pkg/logservice.(*managedClient).Append"
BEGIN {                                                                                                                                                                                                                                                                                                                    
  printf("Hit CTRL+C to end profiling\n");                                                                                                                                                                                                                                                                                 
}                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
uprobe:/home/jinqinghui/matrixone/mo-service:runtime.execute {                                                                                                                                                                                                                                                             
        // map thread id to goroutine id                                                                                                                                                                                                                                                                                   
        @gids[tid] = reg("ax")                                                                                                                                                                                                                                                                                             
}                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                           
tracepoint:sched:sched_process_exit {                                                                                                                                                                                                                                                                                      
  delete(@gids[tid]);                                                                                                                                                                                                                                                                                                      
}                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
uprobe:/home/jinqinghui/matrixone/mo-service:"github.com/matrixorigin/matrixone/pkg/logservice.(*managedClient).Append" {                                                                                                                                                                                                  
        $gid = @gids[tid];                                                                                                                                                                                                                                                                                                 
        @start0[$gid, pid] = nsecs;                                                                                                                                                                                                                                                                                        
}                                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
uprobe:/home/jinqinghui/matrixone/mo-service:"github.com/matrixorigin/matrixone/pkg/logservice.(*managedClient).Append" + 273,                                                                                                                                                                                             
uprobe:/home/jinqinghui/matrixone/mo-service:"github.com/matrixorigin/matrixone/pkg/logservice.(*managedClient).Append" + 291 {                                                                                                                                                                                            
        $gid = @gids[tid];                                                                                                                                                                                                                                                                                                 
        @durations["github.com/matrixorigin/matrixone/pkg/logservice.(*managedClient).Append"] = hist((nsecs - @start0[$gid, pid])/1000000);                                                                                                                                                                               
        delete(@start0[$gid, pid]);                                                                                                                                                                                                                                                                                        
}

该脚本的关键点:
函数调用上线文的获取:trace runtime.execute()。该函数是golang运行时,用于调度协程的分派函数。通过跟踪参数,可以知道当前线程上正在调度的golang协程标识。
函数参数的获取:runtime.execute()在golang 1.19中是通过寄存器传递。可以通过查看汇编确定。
Golang函数的返回点:遍历二进制,找到该函数的ret指令偏移量,然后和函数起始位置相加。有多少ret指令,就需要跟踪多少次。
通过记录函数进入时间和返回时间,计算出时延,最后保存在直方图树结构里。
BUG
遗憾的是,该脚本在最新bpftrace中,并不能正确执行。原因在于被追踪函数含有"(" 目前bpftrace支持不了。替代方案,人工把函数名,用二进制中函数的具体地址替换即可。需要人工计算。

MO将基于go-bpf-gen项目进行改进——生成脚本时自动用函数地址替换函数名。避免人工查看汇编而带来的不便。

0 条评论
请不要发布违法违规有害信息,如发现请及时举报或反馈
还没有人评论呢,速度抢占沙发!
相关文章
  • 服务端 package main import ( "errors" "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" "os" ) // ...

  • 往期回顾: Go语言开发小技巧&易错点100例(一) 本期看点(技巧类用【技】表示,易错点用【易】表示): (1)Go Module中对依赖库版本的升级与降级【技】 (2...

  • 1. 前言 所谓的逃逸分析(Escape analysis)是指由编译器决定内存分配的位置吗不需要程序员指定。 函数中申请一个新的对象 如果分配在栈中, 则函数执行结束后可自动将内存回收 如果分配...

  • 一 JWT介绍 JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。 JWT 以 JSON 对象的形式安全传...

  • 概述调用 sync/atomic 包即可。错误的并发操作先来看一个错误的示例。通过启动 1000 个 goroutine 来模拟并发调用,在函数内部对变量 number 进行自增操作,那么可能存在的一...

  • 今日记录一下 学习 golang 这门语言遇到的一些比较特殊的细节,供大家参考。    所以,在我们输出内容的时候,可以包含很多的非 ASCII 码字符。实际上,Go 是天生支持 UTF...

  • 文章字数大约1.95万字,阅读大概需要65分钟,建议收藏后慢慢阅读!!! 1. GoLang语言 1.1 Slice Slice底层实现原理 切片是基于数组实现的,它的底层是数组,它自己本身非常小...

  • 最近做的一个项目是采用前后端分离模式写前端,后端是fabric区块链,提供接口,需要使用post方法进行访问。如上一章注册用户,就是需要把用户名、账户信息转换成json形式 使用post方法传给后端区...

  • 我的客服系统使用的Golang+ Gin作为后端服务,所以下面的代码是演示demo 在 Go 语言中使用 Gin 框架实现 WebSocket 的方法如下: 安装 gin-gonic/websoc...

  • 概述建议先阅读 函数 和 接口 小节。例子errors.New() 创建错误package main import ( "errors" "fmt" ) // 自定义除法函数 fu...

  • 本文参与了思否技术征文,欢迎正在阅读的你也加入。前言这是Go常见错误系列的第15篇:interface使用的常见错误和最佳实践。素材来源于Go布道者,现Docker公司资深工程师Teiva Harsa...

  • msf生成的裸马现在已经不行了,加壳也只能加冷门壳了,VMP,Shielden,upx不是失效就是效果很差,所以当下,得用shellcode来免杀了 msfvenom -a x86 --platfo...

  • 可见性包通过 导出 机制控制 变量、结构体、函数 等数据可见性。只有 1 个简单的规则: 首字母大写,可导出,首字母小写,不可导出。 也就是说,Go 的访问控制只有两种模式:包内可见全局可见例子pac...

  • 对于无类型常量,可能大家是第一次听说,但这篇我就不放进拾遗系列里了。 因为虽然名字很陌生,但我们每天都在用,每天都有无数潜在的坑被埋下。包括我本人也犯过同样的错误,当时代码已经合并并发布了,当我意识到...

  • hello 大家好呀,我是小楼,这是系列文《Go底层原理剖析》的第三篇,依旧分析 Http 模块。我们今天来看 Go内置的 RPC。说起 RPC 大家想到的一般是框架,Go 作为编程语言竟然还内置了 ...

  • 一 jaeger链路追踪介绍 什么是链路追踪: 分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的...

  • 所有人都听过这样一个歌谣:从前有座山,山里有座庙,庙里有个和尚在讲故事:从前有座山。。。。,虽然这个歌谣并没有一个递归边界条件跳出循环,但无疑地,这是递归算法最朴素的落地实现,本次我们使用Golang...

  • 概述Map 是一种键值对的无序集合,在其他编程语言中也被称为 字典, Hash, 关联数组。重要的一点是: Map 键 的数据类型必须是可以比较的,例如 string, int, float64,类型...

  • 官方资料 官方解释: https://pkg.go.dev/cmd/go#hdr-Build_constraints ,go help buildconstraint 也能看到描述 根据官方描述,go...

  • 概述_ 是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被丢弃,因此这些值不能在后续的代码中使用,也不可以使用这...