Fork me on GitHub

版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://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

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

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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注