您的位置:  首頁 > 技術 > go語言 > 正文

Go 原生插件使用問題全解析

2022-07-13 16:00 https://my.oschina.net/sofastack/blog/5553409 SOFAStack 次閱讀 條評論

圖片

圖片

文|丁飛(花名:路德?)

螞蟻集團高級工程師

圖片

深耕于 SOFAMesh 產品的商業化落地 主要方向為基于服務網格技術的系統架構升級方案設計與落地

本文 4394 字 閱讀?10 分鐘

|前言|

MOSN 作為螞蟻集團在 ServiceMesh 解決方案中的數據面組件,從設計之初就考慮到了第三方的擴展開發需求。目前,MOSN 支持通過 gRPC、WASM、以及 Go 原生插件三種機制對其進行擴展。

我在主導設計和落地基于 Go 原生插件機制的擴展能力時遇到了很多問題,鑒于這方面的相關資料很少,因而就有了這個想法來做一個非常粗淺的總結,希望能對大家有所幫助。

注:本文只說問題和解決方案,不讀代碼,文章最后會給出核心源碼的 checklist。

PART. 1--文章技術背景

一、運行時

通常而言,在計算機編程語言領域,“運行時”的概念和一些需要使用到 VM 的語言相關。程序的運行由兩個部分組成:目標代碼和“虛擬機”。比如最為典型的 JAVA,即 Java Class + JRE。

對于一些看似不需要“虛擬機”的編程語言,就不太會有“運行時”的概念,程序的運行只需要一個部分,即目標代碼。但事實上,即使是 C/C++,也有“運行時”,即它所運行平臺的 OS/Lib。

Go 也是一樣,因為運行 Go 程序不需要前置部署類似于 JRE 的“運行時”,所以它看起來似乎跟“虛擬機”或者“運行時”沒啥關系。但事實上,Go 語言的“運行時”被編譯器編譯成了二進制目標代碼的一部分。

圖片

圖 1-1. Java 程序、runtime 和 OS 關系

圖片

圖 1-2. C/C++ 程序、runtime 和 OS 關系

圖片

圖 1-3. Go 程序、runtime 和 OS 關系

二、Go 原生插件機制

作為一個看起來更貼近 C/C++ 技術棧的 Go 語言來說,支持類似動態鏈接庫的擴展一直是社區中較為強烈的訴求。

如圖 1-5,Go 在標準庫中專門提供了一個?plugin?包,作為插件的語言級編程界面,src/plugin?包的本質是使用 cgo 機制調用 unix 的標準接口:dlopen()?和?dlsym() 。因此,它給 C/C++ 背景的程序員一種“這題我會”的錯覺。

圖片

圖 1-4. C/C++ 程序加載動態鏈接庫

圖片

圖 1-5. Go 程序加載動態鏈接庫

PART. 2--典型問題解決

很遺憾,與 C/C++ 技術棧相比,Go 的插件的產出物雖然也是一個動態鏈接庫文件,但它對于插件的開發、使用有一系列很復雜的內置約束。更令人頭大的是,Go 語言不但沒有對這些約束進行系統性的介紹,甚至寫了一些比較差的設計和實現,導致插件相關問題的排錯非常反人類。

本章節重點跟大家一起看下,在開發、使用 Go 插件,主要是編譯、加載插件的時候,最常見、但必須定位到 Go 標準庫 (主要包括編譯器、鏈接器、打包器和運行時部分) 源碼才能完全弄明白的幾個問題,及對應的解決方法。

簡而言之,Go 的主程序在加載 plugin 時,會在“runtime”里對兩者進行一堆約束檢查,包括但不限于:

-?go version 一致

- go path 一致

- go dependency 的交集一致

  • 代碼一致
  • path 一致

- go build 某些 flag 一致

一、不一致的標準庫版本

主程序加載插件時報錯:

plugin was built with a different version of package runtime/internal/sys

從這個報錯的文本可以得知,具體有問題的庫是?runtime/internal/sys?,很顯然這是一個 go 的內置標準庫??吹竭@里,你可能會有很大的疑惑:我明明用的是同一個本地環境編譯主程序和插件,為什么報標準庫不是一個版本?

答案是,Go 的 error 日志描述不準確。而這個報錯出現的根本原因可以歸結為:主程序和插件的某些關鍵編譯 flag 不一致,跟“版本”沒啥關系。

比如,你使用下面的命令編譯插件:

GO111MODULE=on go build --buildmode=plugin -mod readonly -o ./codec.so ./codec.go

但是你使用 goland 的 debug 模式調試主程序,此時,goland 會幫你把 go build 命令按下面的例子組裝好:

圖片

注意,goland 組裝的編譯命令里包含關鍵的?

-gcflags all=-N -l?參數,但是插件編譯的命令里沒有。此時,你在嘗試拉起插件時就會得到一個有關?runtime/internal/sys?的報錯。

圖片

圖 2-1. 編譯 flag 不一致導致的加載失敗

解決這一類標準庫版本不一致問題的方案比較簡單:盡可能對齊主程序和插件編譯的 flag。事實上,有一些 flag 是不影響插件加載的,你可以在具體的實踐中慢慢摸索。

二、不一致的第三方庫版本

如果使用 vendor 來管理 Go 的依賴庫,那么當解決上一節的問題之后,你 100% 會立即遇到以下這個報錯:

plugin was built with a different version of package xxxxxxxx

其中,xxxxxxxx?指的是某一個具體的三方庫,比如?github.com/stretchr/testify。這個報錯有幾個非常典型的原因,如果沒有相關的排查經驗,其中幾個可能會燒掉開發人員不少時間。

Case 1. 版本不一致

如報錯所示,似乎原因很明確,即主程序和插件所共同依賴的某個第三方庫版本不一致,報錯中會明確告訴你哪一個庫有問題。此時,你可以對比排查主程序和插件的?go.mod?文件,分別找到問題庫的版本,看看他們是否一致。如果這時候你發現主程和插件確實有?commitid?或?tag?的不一致問題,那解決的方法也很簡單:對齊它們。

但是在很多場景下,你只會用到三方庫的一部分:如一個 package,或者只是引了某些 interface。這一部分的代碼在不同的版本里可能根本就沒有變更,但其他沒用到的代碼的變更,同樣會導致整個三方庫版本的變更,進而導致你成為那個“版本不一致”的無辜受害者。

而且,此時你可能立即會遇到另一個問題:以誰為基準對齊?主程序?還是插件?

從常理上來說,以主程序為基線進行對齊是一個比較好的策略,畢竟插件是新添加的“附屬品”,且主程序與插件通常是“一對多”的關系。但是,如果插件的三方庫依賴因為任何原因就是不能和主程序對齊怎么辦?在嘗試了很久以后,我暫時沒有找到一個完美解決這個問題的辦法。

如果版本無法對齊,就只能從根本上放棄走插件這條路。

Go 語言的這種對三方庫的、幾乎無腦的強一致性約束,從一方面來說,避免了運行時因為版本不一致帶來的潛在問題;從另一方面來說,這種刻意不給程序員靈活度的設計,對插件化、定制化、擴展化開發非常的不友好。

圖片

圖 2-2. 共同依賴的三方庫版本不一致導致的加載失敗

Case 2. 版本號一致,代碼不一致

當你按照 case 1 的思路排查?go.mod?文件,但是驚訝的發現報錯的庫版本是一致的時候,事情就會變得復雜起來。你可能會拿出世界上最先進的文本查驗工具,并花掉一個上午去?diff?三方庫的?commitid,但它們就是一模一樣,似乎陷入了薛定諤的版本。

出現這個問題可能的一個不是原因的原因是:有人直接修改了 vendor 目錄下的代碼,Go 插件機制會對代碼內容的一致性進行校驗。

這真的是一個非常令人頭大,并難以排查的原因。除了修改代碼的那個人,和已經在其他 case 中被“坑”過的那些人,沒人會知道這件事情。如果修改的 vendor 代碼出現在主程序里,你就幾乎沒有任何靠譜的辦法讓它們正常工作起來。

不要直接在 vendor 里改代碼!!!

不要直接在 vendor 里改代碼!!!

不要直接在 vendor 里改代碼!!!

回饋開源社區,或者 fork-replace!!!

好消息是,你不需要解決這個問題。因為即使解決了,也還會有更大的問題等著你。

圖片

圖 2-3. 共同依賴的三方庫代碼被就地修改導致的加載失敗

Case 3. 路徑不一致

當按照 case 1 和 case 2 的思路都把問題排查、解決完,但它還是報?different version of package?的時候,可能你就會開始對 Go 的插件機制失去耐心了:版本真的“一毛一樣”,代碼真的一行沒動,為什么還報不同版本???

原因是:插件機制會校驗依賴庫源碼的「路徑」,因此不能使用 vendor 管理依賴。

舉個例子:你的主程序源碼放在?/path/to/main?目錄下,因此,你的某個三方庫依賴的目錄應該是:/path/to/main/vendor/some/thrid/part/lib;

同理,你的插件源碼放在?/path/to/plugin?目錄下,因此,同一個三方庫依賴的目錄應該是:/path/to/plugin/vendor/some/thrid/part/lib。

這些「文件路徑」數據會被打包到二進制可執行文件里并用于校驗,當主程序加載插件時,Go 的“運行時”“聰明的”通過「文件路徑」的差異認定它和插件用的不是同一份代碼,然后報了個?different version of package。

圖片

圖 2-4. 使用 vendor 機制管理第三方庫導致的加載失敗

同樣的問題也可能會出現在使用不同機器/用戶,分別編譯主程序、插件的場景下:用戶名不同,go 代碼的路徑應該也會不一樣。

解決這類問題的方法很暴力直接:刪掉主程序和插件的 vendor 目錄,或者使用?-mod=readonly?編譯 flag。

到這里,如果你是使用同一臺機器進行主程序和插件的編譯,那么常見的問題應該都基本解決了,插件機制理應能夠正常工作。另一方面,由于不再使用 vendor 管理依賴,因此 case 2 的問題也會在這里被強制解決:要么提 PR 給社區,要么 fork-replace。

圖片

圖 2-5. 成功加載

三、不一致的 Go 版本

fatal error: runtime: no plugin module data

除了上面的那些問題以外,還有一個在多機器分別編譯主程/插件場景下的常見報錯。這個報錯的一個可能原因是 Go 版本不一致,對齊它們即可。(如果從機器層面就是不能對齊怎么辦?……)

圖片

圖 2-6. Go 版本不一致導致的加載失敗

PART. 3--統一解決方案

從第二 Part 中,我們看了一些既很難排查,也不是很好處理的問題。除此之外,其實還有一些問題沒有被重點介紹進來。作為一個編程語言官方支持的擴展機制,做的如此用戶不友好確實出人意料。

由于「專有云 MOSN」重點依賴 Go 的插件機制做定開,因此必須拿出一個系統化的方案把這些問題統統解決掉。在嘗試直接修改 Go 源碼無果以后 (吐槽:Go 插件機制源碼寫的令人略感遺憾) ,我們重點從“產品層”及外圍基礎設施入手開展了相關工作:

- 統一編譯環境:

  • 提供一個標準的 docker image 用來編譯主程序和插件,規避任何 go 版本、gopath 路徑、用戶名等不一致所帶來的問題;
  • 預制?go/pkg/mod,盡可能減少因為沒有使用 vendor 模式導致每次編譯都要重新下載依賴的問題。

- 統一 Makefile:

  • 提供一套主程序和插件的編譯 Makefile,規避任何因為 go build 命令帶來的問題。

- 統一插件開發腳手架:

  • 由腳手架,而不是開發者拉齊插件與主程序的依賴版本。并由腳手架解決其他相關問題。

- 流水線化:

  • 將編譯部署流水線化,進一步避免出現錯誤。

圖片

圖 3-1. 統一解決方案

PART. 4--關鍵源碼位置

如果真的想從根本上搞清楚插件校驗的機制,那這里為你提供一些快速進入源碼閱讀狀態的入口。我使用的 Go 源碼為 1.15.2 版本。相關 Go 源碼位置:

- compiler:go/src/cmd/compile/*

- linker:go/src/cmd/link/internal/ld/*

- pkg?loader:go/src/cmd/go/internal/load/*

- runtime:go/src/runtime/*

一、go build 到底在做啥

你可以在?go build?命令里添加?-x?參數,以顯式的打印出 Go 程序編譯、鏈接、打包的全流程,例如:

go build -x -buildmode=plugin -o ../calc_plugin.so calc_plugin.go

二、目標代碼生成

go/src/cmd/compile/internal/gc/obj.go:55?:注意第 67 和第 72 行,這里是兩個入口;

go/src/cmd/compile/internal/gc/iexport.go:244?:注意 280 行,這里會記錄 path 相關數據。

三、庫哈希生成算法

go/src/cmd/link/internal/ld/lib.go:967:注意第 995-1025 行,這里計算 pkg 的 hash。

四、庫哈希校驗

go/src/runtime/symtab.go:392?:關鍵數據結構;

go/src/runtime/plugin.go:52?:鏈接期 hash 與運行時 hash 值校驗點;

go/src/cmd/link/internal/ld/symtab.go:621?:鏈接期 hash 賦值點;

go/src/cmd/link/internal/ld/symtab.go:521?:運行時 hash 賦值點。

PART. 5--總結

可以看到,即使 Go 的原生插件機制有各種各樣令人頭痛的問題,SOFAStack 團隊依舊秉持“開源、開放、可擴展”的初衷,通過各種手段解決問題,并最終將此能力做到生產可用。

目前,專有云 MOSN 的協議編解碼器和 logger 的定制化開發已經實現全面的插件化。接下來,我們將持續對 MOSN 架構進行升級,目標對包括路由邏輯、LB 邏輯、注冊中心/配置中心對接等在內的多方面能力進行插件化支持。

了解更多……

MOSN?Star 一下?: https://github.com/mosn/mosn

和我們一起共建吧🧸

本周推薦閱讀

MOSN 構建 Subset 優化思路分享

圖片

MOSN 文檔使用指南

圖片

MOSN 1.0 發布,開啟新架構演進

圖片

MOSN Contributor 采訪|開源可以是做力所能及的事

圖片

歡迎關注:

圖片

展開閱讀全文 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
  • 0
    感動
  • 0
    路過
  • 0
    高興
  • 0
    難過
  • 0
    搞笑
  • 0
    無聊
  • 0
    憤怒
  • 0
    同情
熱度排行
友情鏈接
18禁高潮出水呻吟娇喘mp3,日本熟妇乱人伦A片免费高清,成人午夜精品无码区,狠狠色噜噜色狠狠狠综合久久,麻豆一区二区99久久久久,年轻的妈妈4,少妇被又大又粗又爽毛片,护士张开腿让我爽了一夜,男男互攻互受h啪肉np文,你好神枪手电视剧免费观看啊,97人妻一区二区精品免费,久久久婷婷五月亚洲97号色,freegaysexvideos男男中国,国产精品国产三级国av麻豆,国产精品又黄又爽又色无遮挡网站,亚洲av无码一区二区三区网站,亚洲国产精品久久久久蜜桃,国产真人无码作爱视频免费,国产成人精品亚洲一区二区三区,亚洲欧洲日产最新,老司机带带我精彩免费,国产成人久久精品激情,日本最新av免费一区二区三区,边摸边吃奶又黄又激烈视频
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>