GO Plugin 編譯問題

GO Plugin 編譯問題

初始問題

現(xiàn)在用go mod和docker multi-stage生成的plugin在workflow中加載的時候萎馅,會遇到plugin與workflow用到的共用package(如:github.com/pkg/errors)版本不一致導(dǎo)致plugin加載失敗。


image.png

問題追蹤

運行時

  1. plugin.open(): https://golang.org/src/plugin/plugin_dlopen.go
func open(name string) (*Plugin, error) {
  // ...
    // 調(diào)用運行時方法
    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()
}

結(jié)論

main程序與plugin對【同一個第三方包】(即:import包名相同)的依賴抛人,需要保證如下兩點弛姜,才能讓main程序成功加載plugin:

  • Toolchain version & any GOEXPERIMENT flags(主要是GOPATH) 完全一致;
    • GOPATH的問題妖枚,等go 1.13加入-trimpath這個tag之后解決
  • 第三方依賴包版本完全一致廷臼。

編譯問題解決思路

Main程序與Plugin在相同環(huán)境編譯

由于Main程序代碼只能在我們這邊,而plugin的代碼在用戶側(cè)绝页,若想要編譯環(huán)境一致:

  • 用戶擁有Main程序代碼荠商;
  • Main程序所有引用的包go.mod全固定下來,不進(jìn)行g(shù)o mod的更新续誉,并寫到文檔中莱没,用戶開發(fā)so前check文檔,如果引用了平臺引用過的包酷鸦,必須手動更新為跟平臺同樣的版本饰躲。
  • Main程序使用go.mod交給用戶側(cè),用戶基于我們的go.mod文件進(jìn)行編譯臼隔。

Main程序自定義import path

既然用戶側(cè)的plugin我們無法控制嘹裂,可以嘗試控制Main程序?qū)τ诘谌桨囊蕾嚕?/p>

  • 盡可能減少Main程序?qū)τ诘谌桨囊蕾嚕?/li>
  • 自定義第三方包的import路徑,這樣即使plugin引用相同的第三方包摔握,但由于import路徑不一樣寄狼,它們不再是同一個包。

自定義import路徑有兩種方式:

  1. 搭建Go-Get Proxy氨淌,參考:http://www.reibang.com/p/449345975453
    • 只能更改直接依賴的第三方包的import path
  2. 本地Fork第三方依賴
  3. 通過go.mod的replace功能也能實現(xiàn)

修改GO源碼編譯

即將 https://golang.org/src/runtime/plugin.go中的檢查注釋后重新編譯GO例嘱,可能引入新的問題狡逢,暫不采用。

編譯可行性驗證

Main程序與Plugin在相同環(huán)境下編譯

步驟:

  • 將Main程序依賴的某個第三方包改為老版本(plugin默認(rèn)在go mod tidy時去獲取新版本)拼卵,編譯獲取新的Main程序以及go.mod文件;
  • Plugin基于上面的go.mod編譯蛮艰;
  • 結(jié)果:執(zhí)行成功腋腮,預(yù)期一致

Main程序自定義import path

Main程序引用自定義第三方包,驗證是否依然報錯

步驟:

  • Main程序與Plugin引用同一個第三方包的不同版本壤蚜,import路徑一致
  • 結(jié)果:報錯即寡,與預(yù)期一致
  • Main程序與Plugin引用同一個第三方包的不同版本,Main程序使用自定義路徑袜刷,plugin使用原路徑
  • 結(jié)果:正確聪富,與預(yù)期一致

Main程序引用自定義第三方包,驗證是否確定被認(rèn)為不同的包

步驟:

  • Main程序與Plugin引用同一個第三方包的相同版本著蟹,import路徑一致墩蔓,同時在main和plugin中獲取第三方包中的全局變量地址
  • 結(jié)果:地址相同,與預(yù)期一致
  • Main程序與Plugin引用同一個第三方包的不同版本萧豆,Main程序使用自定義路徑奸披,plugin使用原路徑,同時在main和plugin中獲取第三方包中的全局變量地址
  • 結(jié)果:地址不同涮雷,與預(yù)期一致

Main程序間接引用阵面,Plugin直接引用,驗證是否會存在問題

步驟:

  • Main程序間接引用第三方包洪鸭,plugin直接引用第三方包样刷,觀察是否會加載失敗
  • 結(jié)果:成功加載,說明僅對直接依賴的包進(jìn)行檢測

編譯最終解決方案

Main程序與Plugin在相同環(huán)境下編譯

  • 由于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司蔬,可能會有函數(shù)參數(shù)類型不一致問題(主要是包不一致)


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

    • 問題:由于遠(yuǎn)端源碼并不由我們控制姨蝴,go.mod文件中的module無法更改俊啼,go mod tidy獲取源碼過程中做了檢測,即改了本地go.mod中的module仍然無法import


      image.png
  • 本地Fork左医,下載到gitlab.alipay/workflow
    a. 問題:原本間接依賴的包會變成直接依賴授帕,需要遞歸依次去改所有依賴包同木,成本很高。

  • GOPATH + Go-Get Proxy
    a. 問題:無法明確使用的版本跛十,若后續(xù)有變動彤路,需要修改;需要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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市奈偏,隨后出現(xiàn)的幾起案子坞嘀,更是在濱河造成了極大的恐慌,老刑警劉巖惊来,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丽涩,死亡現(xiàn)場離奇詭異,居然都是意外死亡裁蚁,警方通過查閱死者的電腦和手機矢渊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厘擂,“玉大人昆淡,你說我怎么就攤上這事」粞希” “怎么了昂灵?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長舞萄。 經(jīng)常有香客問我眨补,道長,這世上最難降的妖魔是什么倒脓? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任撑螺,我火速辦了婚禮,結(jié)果婚禮上崎弃,老公的妹妹穿的比我還像新娘甘晤。我一直安慰自己,他們只是感情好饲做,可當(dāng)我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布线婚。 她就那樣靜靜地躺著,像睡著了一般盆均。 火紅的嫁衣襯著肌膚如雪塞弊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機與錄音游沿,去河邊找鬼饰抒。 笑死,一個胖子當(dāng)著我的面吹牛诀黍,可吹牛的內(nèi)容都是我干的袋坑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼眯勾,長吁一口氣:“原來是場噩夢啊……” “哼咒彤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咒精,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎旷档,沒想到半個月后模叙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡鞋屈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年范咨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厂庇。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡渠啊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出权旷,到底是詐尸還是另有隱情替蛉,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布拄氯,位于F島的核電站躲查,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏译柏。R本人自食惡果不足惜镣煮,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鄙麦。 院中可真熱鬧典唇,春花似錦、人聲如沸胯府。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盟劫。三九已至夜牡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背塘装。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工急迂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蹦肴。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓僚碎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親阴幌。 傳聞我的和親對象是個殘疾皇子勺阐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 有一個人霍狰,不敢想起藐吮,想起就會控制不住流眼淚浅役。這個人就是我的爺爺屹篓,六年前永遠(yuǎn)離開了我們橘忱。 永別了恃鞋,爺爺 爺爺去世的時...
    cutelyd閱讀 229評論 0 0
  • 《起風(fēng)了》這部電影發(fā)生在二戰(zhàn)期間宪赶,那時的德國告抄、日本等國家同盟國聯(lián)合侵略其他國家栈幸,那時人心惶惶愤估,雞犬不寧,日本當(dāng)時很...
    崔禹喆閱讀 462評論 1 4
  • 之前網(wǎng)絡(luò)流行語‘積極廢人’速址,一針見血指出我的狀態(tài)玩焰。間歇性的打雞血般干勁十足,然后就不斷重復(fù)從入門到放棄的過程芍锚。 前...
    明明齋_生活雜貨鋪閱讀 206評論 0 1