apache/skywalking-go 源码分析
版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc
apache/skywalking-go 源码分析
参考资料
1. 前言
Skywalking是什么?
Skywalking 是一个开源的应用性能监控工具,它专注于分布式系统架构中的性能监控和故障排查。
它能够跟踪分布式系统中的请求流,并提供实时的性能指标、调用链追踪、错误分析等功能。
使用 Skywalking 可以帮助开发人员和运维团队更好地理解应用程序的性能特征, 并快速定位和解决潜在的性能问题和故障。
几年前,当我最开始知道Skywalking的时候,它似乎只支持Java,它给我的最大的惊喜时,是直接使用字节码注入的方式来实现埋点,
减少开发人员埋点的工作量,且代码几乎无侵入。
2023年上半年,Skywalking推出一个全新的Go Agent skywalking-go。
它基于Golang build提供的-toolexec参数,实现了编译期劫持,也达到了在对代码几乎无侵入的情况下,实现埋点。
截止2023年12月1日,已经有大量的库得到了支持。参看support-plugins
日志库
zap、logrus
数据库Client
gorm、sql
HTTP Server
gin、http
HTTP Client
http
RPC框架
gRPC
它是如何做到代码几乎无入侵埋点的呢?本文试图解答这个问题
2. skywalking-go实现埋点
关于skywalking-go的使用参看SkyWalking Go Agent 快速开始指南
skywalking-go实现埋点主要依靠在golang build编译阶段的编译劫持。
go build -toolexec="/myopt/bin/skywalking-go-agent" -x -work -a -o test .
这里我们增加了2个额外参数
-x
:这个标志告诉 Go 打印它执行的命令。
-work
:这个标志告诉 Go 打印临时工作目录的名称,并在退出时不删除它。
2.1 编译要点回顾
编译主要分成2个阶段
注意: 为了展示核心要点,萌叔删除了大量非关键参数
Step1 compile
...
/myopt/bin/skywalking-go-agent /usr/local/go/pkg/tool/darwin_arm64/compile -o $WORK/b001/_pkg_.a -p main ./main.go
...
第1阶段生成目标文件 $WORK/b001/pkg.a
Step2 link
/myopt/bin/skywalking-go-agent /usr/local/go/pkg/tool/darwin_arm64/link -o $WORK/b001/exe/a.out $WORK/b001/_pkg_.a
第2阶段将目标文件链接成可执行文件a.out
/myopt/bin/skywalking-go-agent的作用相当于装饰器
,且只干预第1阶段
在下面的命令中
/myopt/bin/skywalking-go-agent /usr/local/go/pkg/tool/darwin_arm64/compile -o $WORK/b001/_pkg_.a -p main ./main.go
"/myopt/bin/skywalking-go-agent" 是 command
["/usr/local/go/pkg/tool/darwin_arm64/compile", "-o", "$WORK/b001/pkg.a", "-p", "main", "./main.go"] 是传递给command的参数
这里的-p参数后面跟的就是包名
既然command
是skywalking-go-agent,那么从main()开始
main.go
func main() {
args := os.Args[1:]
var err error
var firstNonOptionIndex int
...
// only enhance the "compile" phase
cmdName := tools.ParseProxyCommandName(args, firstNonOptionIndex)
if cmdName != "compile" { // 如果不是compile命令,就不做代码增强,直接执行原有的命令
executeDelegateCommand(args[firstNonOptionIndex:])
return
}
...
// parse the args
compileOptions := &api.CompileOptions{}
if _, err = tools.ParseFlags(compileOptions, args); err != nil {
executeDelegateCommand(args[firstNonOptionIndex:])
return
}
// 给原有的代码添加tracing逻辑
// execute the enhancement
args, err = instrument.Execute(compileOptions, args)
if err != nil {
log.Fatal(err)
}
// execute the delegate command with updated args
executeDelegateCommand(args[firstNonOptionIndex:])
}
var instruments = []api.Instrument{
runtime.NewInstrument(),
agentcore.NewInstrument(),
reporter.NewGRPCInstrument(),
entry.NewInstrument(),
logger.NewInstrument(),
plugins.NewInstrument(),
}
逻辑都在这里
func execute0(opts *api.CompileOptions, args []string) ([]string, error) {
// find the instrument
var inst api.Instrument
for _, ins := range instruments {
// 判断是否能执行代码增强
// 1)检查包名 2)检查包的版本
if ins.CouldHandle(opts) {
inst = ins
break
}
}
if inst == nil {
return args, nil
}
var buildDir = filepath.Dir(opts.Output)
// 修改原文件
// instrument existing files
if err := instrumentFiles(buildDir, inst, args); err != nil {
return nil, err
}
// 增加额外的文件
// write extra files if exist
files, err := inst.WriteExtraFiles(buildDir)
if err != nil {
return nil, err
}
// 把添加的额外文件加入到命令行的参数列表中
if len(files) > 0 {
args = append(args, files...)
}
return args, nil
}
func instrumentFiles(buildDir string, inst api.Instrument, args []string) error {
// 将每个go文件解析成语法树
// parse files
parsedFiles, err := parseFilesInArgs(args)
if err != nil {
return err
}
allFiles := make([]*dst.File, 0)
for _, f := range parsedFiles {
allFiles = append(allFiles, f.dstFile)
}
// filter and edit the files
instrumentedFiles := make([]string, 0)
for path, info := range parsedFiles {
hasInstruted := false
// 遍历语法树,对语法树上的每一个节点进行检查,如果匹配,则对此节点进行扩展
// 一般为function 或者 struct
dstutil.Apply(info.dstFile, func(cursor *dstutil.Cursor) bool {
if inst.FilterAndEdit(path, info.dstFile, cursor, allFiles) {
hasInstruted = true
}
return true
}, func(cursor *dstutil.Cursor) bool {
return true
})
if hasInstruted {
instrumentedFiles = append(instrumentedFiles, path)
}
}
// write instrumented files to the build directory
for _, updateFileSrc := range instrumentedFiles {
info := parsedFiles[updateFileSrc]
filename := filepath.Base(updateFileSrc)
dest := filepath.Join(buildDir, filename)
debugInfo, err := tools.BuildDSTDebugInfo(updateFileSrc, nil)
if err != nil {
return err
}
// 将语法树转换为文本,写回原文件
if err := tools.WriteDSTFile(dest, info.dstFile, debugInfo); err != nil {
return err
}
if err := inst.AfterEnhanceFile(updateFileSrc, dest); err != nil {
return err
}
args[info.argsIndex] = dest
}
return nil
}
把go文件解析成语法树,用到了dave/dst
注意: skywalking-go
对包名和包的版本都有严格的检查,所以需要注意对应关系,否则可能无法被处理
golang build的临时目录中,有skywalking-go
执行的日志文件"instrument.log"。
某个扩展点被成功匹配并处理,可以看到形如下面的日志。
time="2023-12-05T10:21:42+08:00" level=info msg="adding enhanced method" method=NewClient package=github.com/redis/go-redis/v9 receiver= type=enhance_method
time="2023-12-05T10:21:42+08:00" level=info msg="adding enhanced method" method=NewFailoverClient package=github.com/redis/go-redis/v9 receiver= type=enhance_method
2.2 plugin/go-redisv9 示例
代码增强前
func NewClient(opt *Options) *Client {
opt.init()
c := Client{
baseClient: &baseClient{
opt: opt,
},
}
c.init()
c.connPool = newConnPool(opt, c.dialHook)
return &c
}
代码增强后(可在golang编译临时目录中自行查找)
// NewClient returns a client to the Redis Server specified by Options.
func NewClient(opt *Options) (skywalking_result_0 *Client) {
if _sw_inv_res_0,_sw_invocation, _sw_skip := skywalking_enhance_github_com_redis_go_redis_v9_NewClient(&opt); _sw_skip { return _sw_inv_res_0} else { defer func() { skywalking_enhance_github_com_redis_go_redis_v9_NewClient_ret(_sw_invocation,&skywalking_result_0)}() }; opt.init()
c := Client{
baseClient: &baseClient{
opt: opt,
},
}
c.init()
c.connPool = newConnPool(opt, c.dialHook)
return &c
}
另外可以看到,在redis.go所在的目录下,被增加了大量新文件
后记
2023年12月6日,萌叔打算利用skywalking-go的编译劫持功能,在关键代码位置,自动化的添加日志和metrics