GO Plugin 编译问题

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 有一个人,不敢想起,想起就会控制不住流眼泪。这个人就是我的爷爷,六年前永远离开了我们。 永别了,爷爷 爷爷去世的时...
    cutelyd阅读 223评论 0 0
  • 《起风了》这部电影发生在二战期间,那时的德国、日本等国家同盟国联合侵略其他国家,那时人心惶惶,鸡犬不宁,日本当时很...
    崔禹喆阅读 454评论 1 4
  • 之前网络流行语‘积极废人’,一针见血指出我的状态。间歇性的打鸡血般干劲十足,然后就不断重复从入门到放弃的过程。 前...
    明明斋_生活杂货铺阅读 204评论 0 1