美文网首页
GO Plugin 编译问题

GO Plugin 编译问题

作者: 唐西铭 | 来源:发表于2019-08-17 22:56 被阅读0次

    GO Plugin 编译问题

    初始问题

    现在用go mod和docker multi-stage生成的plugin在workflow中加载的时候,会遇到plugin与workflow用到的共用package(如:github.com/pkg/errors)版本不一致导致plugin加载失败。


    image.png

    问题追踪

    运行时

    1. plugin.open(): https://golang.org/src/plugin/plugin_dlopen.go
    func open(name string) (*Plugin, error) {
      // ...
        // 调用运行时方法
        pluginpath, syms, errstr := lastmoduleinit()
      if errstr != "" {
        plugins[filepath] = &Plugin{
          pluginpath: pluginpath,
          err:        errstr,
        }
        pluginsMu.Unlock()
        return nil, errors.New(`plugin.Open("` + name + `"): ` + errstr)
      }
        // ...
    }
    
    // lastmoduleinit is defined in package runtime
    func lastmoduleinit() (pluginpath string, syms map[string]interface{}, errstr string)
    
    1. lastmoduleinit(): https://golang.org/src/runtime/plugin.go
    //go:linkname plugin_lastmoduleinit plugin.lastmoduleinit
    func plugin_lastmoduleinit() (path string, syms map[string]interface{}, errstr string) {
      // ...
        for _, pkghash := range md.pkghashes {
            // 对比pkg链接与运行时的hash是否一致
        if pkghash.linktimehash != *pkghash.runtimehash {
          md.bad = true
          return "", nil, "plugin was built with a different version of package " + pkghash.modulename
        }
      }
        // ...
    }
    
    1. modulehash: https://golang.org/src/runtime/symtab.go
      a. cmd/internal/ld/symtab.go:symtab
    // moduledata records information about the layout of the executable
    // image. It is written by the linker. Any changes here must be
    // matched changes to the code in cmd/internal/ld/symtab.go:symtab.
    // moduledata is stored in statically allocated non-pointer memory;
    // none of the pointers here are visible to the garbage collector.
    type moduledata struct {
        // ...
        pkghashes  []modulehash
        // ...
    }
    
    // A modulehash is used to compare the ABI of a new module or a
    // package in a new module with the loaded program.
    //
    // For each shared library a module links against, the linker creates an entry in the
    // moduledata.modulehashes slice containing the name of the module, the abi hash seen
    // at link time and a pointer to the runtime abi hash. These are checked in
    // moduledataverify1 below.
    //
    // For each loaded plugin, the pkghashes slice has a modulehash of the
    // newly loaded package that can be used to check the plugin's version of
    // a package against any previously loaded version of the package.
    // This is done in plugin.lastmoduleinit.
    type modulehash struct {
        modulename   string
        linktimehash string
        runtimehash  *string
    }
    

    编译时

    1. symtab(): https://golang.org/src/cmd/link/internal/ld/symtab.go
    func (ctxt *Link) symtab() {
        // ...
        // Information about the layout of the executable image for the
        // runtime to use. Any changes here must be matched by changes to
        // the definition of moduledata in runtime/symtab.go.
        // This code uses several global variables that are set by pcln.go:pclntab.
        moduledata := ctxt.Moduledata
        // ...
        if ctxt.BuildMode == BuildModePlugin {
            // ...
            for i, l := range ctxt.Library {
                // pkghashes[i].name
                addgostring(ctxt, pkghashes, fmt.Sprintf("go.link.pkgname.%d", i), l.Pkg)
                // pkghashes[i].linktimehash
                addgostring(ctxt, pkghashes, fmt.Sprintf("go.link.pkglinkhash.%d", i), l.Hash)
                // pkghashes[i].runtimehash
                hash := ctxt.Syms.ROLookup("go.link.pkghash."+l.Pkg, 0)
                pkghashes.AddAddr(ctxt.Arch, hash)
            }
            // ...
        }
    }
    
    1. addlibpath(): https://golang.org/src/cmd/link/internal/ld/ld.go
      a. l.pkg: package import path, e.g. container/vector
    /*
     * add library to library list, return added library.
     *  srcref: src file referring to package
     *  objref: object file referring to package
     *  file: object file, e.g., /home/rsc/go/pkg/container/vector.a
     *  pkg: package import path, e.g. container/vector
     *  shlib: path to shared library, or .shlibname file holding path
     */
    func addlibpath(ctxt *Link, srcref string, objref string, file string, pkg string, shlib string) *sym.Library {
        if l := ctxt.LibraryByPkg[pkg]; l != nil {
            return l
        }
    
        if ctxt.Debugvlog > 1 {
            ctxt.Logf("%5.2f addlibpath: srcref: %s objref: %s file: %s pkg: %s shlib: %s\n", Cputime(), srcref, objref, file, pkg, shlib)
        }
    
        l := &sym.Library{}
        ctxt.LibraryByPkg[pkg] = l
        ctxt.Library = append(ctxt.Library, l)
        l.Objref = objref
        l.Srcref = srcref
        l.File = file
        l.Pkg = pkg
        // ...
        return l
    }
    
    1. loadlib():https://golang.org/src/cmd/link/internal/ld/lib.go
      a. l.hash: toolchain version and any GOEXPERIMENT flags
    func (ctxt *Link) loadlib() {
        // ctxt.Library grows during the loop, so not a range loop.
        for i := 0; i < len(ctxt.Library); i++ {
            lib := ctxt.Library[i]
            if lib.Shlib == "" {
                if ctxt.Debugvlog > 1 {
                    ctxt.Logf("%5.2f autolib: %s (from %s)\n", Cputime(), lib.File, lib.Objref)
                }
                loadobjfile(ctxt, lib)
            }
        }
        
        // ...
        
        // If package versioning is required, generate a hash of the
        // packages used in the link.
        if ctxt.BuildMode == BuildModeShared || ctxt.BuildMode == BuildModePlugin || ctxt.CanUsePlugins() {
            for _, lib := range ctxt.Library {
                if lib.Shlib == "" {
                    genhash(ctxt, lib)
                }
            }
        }
    }
    
    func genhash(ctxt *Link, lib *sym.Library) {
        // ...
        h := sha1.New()
    
        // To compute the hash of a package, we hash the first line of
        // __.PKGDEF (which contains the toolchain version and any
        // GOEXPERIMENT flags) and the export data (which is between
        // the first two occurrences of "\n$$").
        lib.Hash = hex.EncodeToString(h.Sum(nil))
    }
    
    1. main(): https://golang.org/src/cmd/link/internal/ld/main.go
    // Main is the main entry point for the linker code.
    func Main(arch *sys.Arch, theArch Arch) {
        // ...
        switch ctxt.BuildMode {
        case BuildModePlugin:
            addlibpath(ctxt, "command line", "command line", flag.Arg(0), *flagPluginPath, "")
        default:
            addlibpath(ctxt, "command line", "command line", flag.Arg(0), "main", "")
        }
        ctxt.loadlib()
    }
    

    结论

    main程序与plugin对【同一个第三方包】(即:import包名相同)的依赖,需要保证如下两点,才能让main程序成功加载plugin:

    • Toolchain version & any GOEXPERIMENT flags(主要是GOPATH) 完全一致;
      • GOPATH的问题,等go 1.13加入-trimpath这个tag之后解决
    • 第三方依赖包版本完全一致。

    编译问题解决思路

    Main程序与Plugin在相同环境编译

    由于Main程序代码只能在我们这边,而plugin的代码在用户侧,若想要编译环境一致:

    • 用户拥有Main程序代码;
    • Main程序所有引用的包go.mod全固定下来,不进行go mod的更新,并写到文档中,用户开发so前check文档,如果引用了平台引用过的包,必须手动更新为跟平台同样的版本。
    • Main程序使用go.mod交给用户侧,用户基于我们的go.mod文件进行编译。

    Main程序自定义import path

    既然用户侧的plugin我们无法控制,可以尝试控制Main程序对于第三方包的依赖:

    • 尽可能减少Main程序对于第三方包的依赖;
    • 自定义第三方包的import路径,这样即使plugin引用相同的第三方包,但由于import路径不一样,它们不再是同一个包。

    自定义import路径有两种方式:

    1. 搭建Go-Get Proxy,参考:https://www.jianshu.com/p/449345975453
      • 只能更改直接依赖的第三方包的import path
    2. 本地Fork第三方依赖
    3. 通过go.mod的replace功能也能实现

    修改GO源码编译

    即将 https://golang.org/src/runtime/plugin.go中的检查注释后重新编译GO,可能引入新的问题,暂不采用。

    编译可行性验证

    Main程序与Plugin在相同环境下编译

    步骤:

    • 将Main程序依赖的某个第三方包改为老版本(plugin默认在go mod tidy时去获取新版本),编译获取新的Main程序以及go.mod文件;
    • Plugin基于上面的go.mod编译;
    • 结果:执行成功,预期一致

    Main程序自定义import path

    Main程序引用自定义第三方包,验证是否依然报错

    步骤:

    • Main程序与Plugin引用同一个第三方包的不同版本,import路径一致
    • 结果:报错,与预期一致
    • Main程序与Plugin引用同一个第三方包的不同版本,Main程序使用自定义路径,plugin使用原路径
    • 结果:正确,与预期一致

    Main程序引用自定义第三方包,验证是否确定被认为不同的包

    步骤:

    • Main程序与Plugin引用同一个第三方包的相同版本,import路径一致,同时在main和plugin中获取第三方包中的全局变量地址
    • 结果:地址相同,与预期一致
    • Main程序与Plugin引用同一个第三方包的不同版本,Main程序使用自定义路径,plugin使用原路径,同时在main和plugin中获取第三方包中的全局变量地址
    • 结果:地址不同,与预期一致

    Main程序间接引用,Plugin直接引用,验证是否会存在问题

    步骤:

    • Main程序间接引用第三方包,plugin直接引用第三方包,观察是否会加载失败
    • 结果:成功加载,说明仅对直接依赖的包进行检测

    编译最终解决方案

    Main程序与Plugin在相同环境下编译

    • 由于go.mod中有很多不是plugin需要的第三方包,go mod tidy虽然最后会将它们从go.mod中移除,但是还是会先去find,这个过程耗时有点久(这个问题通过加入GOPROXY=https://goproxy.io后得到有效改善)

    Main程序自定义import路径

    • 共性问题:这两个问题的解决最好的方式就是本地fork修改了
      a. 若第三方包强制指定了import路径,改为自定义import 路径,会失败(参考:https://jiajunhuang.com/articles/2018_09_07-go_custom_import_path.md.html

      image.png
      image.png

      b. 部分程序需要修改,因为仅改变了第一层import path,可能会有函数参数类型不一致问题(主要是包不一致)


      image.png
    • Go Mod + Go-Get Proxy,

      • 问题:由于远端源码并不由我们控制,go.mod文件中的module无法更改,go mod tidy获取源码过程中做了检测,即改了本地go.mod中的module仍然无法import


        image.png
    • 本地Fork,下载到gitlab.alipay/workflow
      a. 问题:原本间接依赖的包会变成直接依赖,需要递归依次去改所有依赖包,成本很高。

    • GOPATH + Go-Get Proxy
      a. 问题:无法明确使用的版本,若后续有变动,需要修改;需要RD通过go get下载,目前与goland集成有问题;

    参考文档

    https://github.com/golang/go/issues/26759
    https://github.com/golang/go/issues/16860
    https://www.atatech.org/articles/116635#modules
    https://supereagle.github.io/2018/06/17/multiple-dep-versions/
    http://www.cppblog.com/sunicdavy/archive/2017/07/06/215057.html
    http://razil.cc/post/2018/08/go-plugin-package-version-error/
    https://groups.google.com/forum/#!topic/golang-codereviews/_kALgmWInGQ

    相关文章

      网友评论

          本文标题:GO Plugin 编译问题

          本文链接:https://www.haomeiwen.com/subject/zoklsctx.html