2

apache/skywalking-go 源码分析

 3 months ago
source link: https://vearne.cc/archives/40093
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc

apache/skywalking-go 源码分析

1.劫持 Golang 编译

2.SkyWalking Go Agent 快速开始指南

3.support-plugins

4.Hybrid Compilation

5.Key Principle

6.通过SkyWalking上报Go应用数据

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

RPC框架

它是如何做到代码几乎无入侵埋点的呢?本文试图解答这个问题

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所在的目录下,被增加了大量新文件

778084c2-9338-11ee-810b-5626e1cdcfe2.jpg

微信公众号

2023年12月6日,萌叔打算利用skywalking-go的编译劫持功能,在关键代码位置,自动化的添加日志和metrics

阅读数: 404

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK