版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | http://vearne.cc

apache/skywalking-go 源码分析

参考资料

1.劫持 Golang 编译

2.SkyWalking Go Agent 快速开始指南

3.support-plugins

4.Hybrid Compilation

5.Key Principle

6.通过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

/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 示例

redis.go

代码增强前

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