共计 5513 个字符,预计需要花费 14 分钟才能阅读完成。
简介:自己在设计和落地基于 Go 原生插件机制的扩大开发产品时踩到了很多坑,因为这方面相干材料很少,因此借此机会做一个十分浅显的总结,心愿能对大家有所帮忙。本文只说问题和解决方案,不读代码。
作者 | 丁飞起源 | 阿里开发者公众号导言自己在设计和落地基于 Go 原生插件机制的扩大开发产品时踩到了很多坑,因为这方面相干材料很少,因此借此机会做一个十分浅显的总结,心愿能对大家有所帮忙。本文只说问题和解决方案,不读代码。一些背景常识 2.1 运行时通常而言,在计算机编程语言畛域,“运行时”的概念和一些须要应用到 vm 的语言相干。程序的运行由两个局部组成:指标代码和“虚拟机”。比方最为典型的 JAVA,即 Java Class + JRE。对于一些看似不须要“虚拟机”的编程语言,就不太会有“运行时”的概念,程序的运行只须要一个局部,即指标代码。但事实上,即便是 C /C++,也有“运行时”,即它所运行平台的 OS/Lib。Go 也是一样,因为运行 Go 程序不须要前置部署相似于 JRE 的“运行时”,所以它看起来仿佛跟“虚拟机”或者“运行时”没啥关系。但事实上,Go 语言的“运行时”被编译器编译成了二进制指标代码的一部分。
图 2 -1. Java 程序、runtime 和 OS 关系
图 2 -2. C/C++ 程序、runtime 和 OS 关系
图 2 -3. Go 程序、runtime 和 OS 关系 2.2 Go 原生插件机制作为一个看起来更贴近 C /C++ 技术栈的 Go 语言来说,反对相似动态链接库的扩大始终是社区中较为强烈的诉求。如图 2 -5,Go 在规范库中专门提供了一个 plugin 包,作为插件的语言级编程界面,src/plugin 包的实质是应用 cgo 机制调用 unix 的标准接口:dlopen() 和 dlsym()。因而,它给 C /C++ 背景的程序员一种“这题我会”的错觉。
图 2 -4. C/C++ 程序加载动态链接库
图 2 -5. Go 程序加载动态链接库 典型问题解决很遗憾,与 C /C++ 技术栈相比,Go 的插件的产出物尽管也是一个动态链接库文件,但它对于插件的开发、应用有一系列很简单的内置束缚。更令人头大的是,Go 语言岂但没有对这些束缚进行系统性的介绍,甚至写了一些比拟差的设计和实现,导致插件相干问题的排错十分反人类。本章节重点跟大家一起看下,在开发、应用 Go 插件,次要是编译、加载插件的时候,最常见、但必须定位到 Go 规范库(次要包含编译器、链接器、打包器和运行时局部)源码能力齐全弄明确的几个问题,及对应的解决办法。简而言之,Go 的主程序在加载 plugin 时,会在“runtime”里对两者进行一堆束缚查看,包含但不限于:go version 统一 go path 统一 go dependency 的交加统一代码统一 path 统一 go build 某些 flag 统一 3.1 不统一的规范库版本主程序加载插件时报错: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 命令按上面的例子组装好:/usr/local/go/bin/go test -c -o /private/var/folders/gy/2zv22t710sd7m0x9bcfzq23r0000gp/T/GoLand/___Test_TaskC_in_github_com_fdingiit_mpl_test.test -gcflags all=-N -l github.com/fdingiit/mpl/test #gosetup 留神,goland 组装的编译命令里蕴含要害的 -gcflags all=-N -l 参数,然而插件编译的命令里没有。此时,你在尝试拉起插件时就会失去一个无关 runtime/internal/sys 的报错。
图 3 -1. 编译 flag 不统一导致的加载失败 解决这一类规范库版本不统一问题的计划比较简单:尽可能对齐主程序和插件编译的 flag。事实上,有一些 flag 是不影响插件加载的,你能够在具体的实际中缓缓摸索。3.2 不统一的第三方库版本如果你应用 vendor 来治理 Go 的依赖库,那么当解决 3.1 的问题之后,你 100% 会立刻遇到以下这个报错:plugin was built with a different version of package xxxxxxxx 其中,xxxxxxxx 指的是某一个具体的三方库,比方 github.com/stretchr/testify。这个报错有几个十分典型的起因,如果没有相干的排查教训,其中几个可能会烧掉开发人员不少工夫。3.2.1 Case 1. 版本不统一如报错所示,仿佛起因很明确,即主程序和插件所独特依赖的某个第三方库版本不统一,报错中会明确通知你哪一个库有问题。此时,你能够比照排查主程序和插件的 go.mod 文件,别离找到问题库的版本,看看他们是否统一。如果这时候你发现主程和插件的确有 commitid 或 tag 的不统一问题,那解决的办法也很简略:对齐它们。然而在很多场景下,你只会用到三方库的一部分:如一个 package,或者只是引了一个 interface。这一部分的代码在不同的版本里基本就没有变更;但其余没用到的代码的变更,同样会导致整个三方库版本号的变更,进而导致你成为那个“版本不统一”的无辜受害者。而且,此时你可能立刻会遇到另一个问题:以谁为基准对齐?主程序?还是插件?从常理上来说,以主程序为基线进行对齐是一个比拟好的策略,毕竟插件是新增加的“附属品”,且主程序与插件通常是 1 对多的关系。然而,如果插件的三方库依赖因为任何起因就是不能和主程序对齐怎么办?在尝试了很久当前,我临时没有找到一个完满解决这个问题的方法。如果版本无奈对齐,就只能从根本上放弃走插件这条路。Go 语言的这种对三方库的、简直无脑的强一致性束缚,从一方面来说,防止了运行时因为版本不统一带来的潜在问题;从另一方面来说,这种刻意不给程序员灵便度的设计,对插件化、定制化、扩大化开发十分的不敌对。
图 3 -2. 独特依赖的三方库版本不统一导致的加载失败 3.2.2 case 2. 版本号统一,代码不统一当你依照 3.2.1 的思路排查 go.mod 文件,然而诧异的发现报错的库版本是统一的时候,事件就会变得复杂起来。你可能会拿出世界上最先进的文本查验工具,并花掉一个上午去 diff 三方库的 commitid,但它们就是截然不同,仿佛陷入了薛定谔的版本。呈现这个问题可能的一个不是起因的起因是:有人间接批改了 vendor 目录下的代码,Go 插件机制会对代码内容的一致性进行校验。这真的是一个十分令人头大,并难以排查的起因。除了批改代码的那个人,和曾经在其余 case 中被“坑”过的那些人,没人会晓得这件事件。如果批改的 vendor 代码呈现在主程序里,你就简直没有任何靠谱的方法让它们失常工作起来。不要间接在 vendor 里改代码,回馈开源社区,或者 fork-replace。好消息是,你不须要解决这个问题。因为即便解决了,也还会有更大的问题等着你。
图 3 -2. 独特依赖的三方库代码被就地批改导致的加载失败 3.2.3 case 3. 门路不统一当依照 3.2.1 和 3.2.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。
图 3 -3. 应用 vendor 机制治理第三方库导致的加载失败 同样的问题也可能会呈现在应用不同机器 / 用户,别离编译主程序、插件的场景下:用户名不同,go 代码的门路应该也会不一样。解决这类问题的办法很暴力间接:删掉主程序和插件的 vendor 目录,或者应用 -mod=readonly 编译 flag。到这里,如果你是应用同一台机器进行主程序和插件的编译,那么常见的问题应该都根本解决了,插件机制理当可能失常工作。另一方面,因为不再应用 vendor 治理依赖,因而 3.2.2 的问题也会在这里被强制解决:要么提 PR 给社区,要么 fork-replace。
图 3 -4. 胜利加载 3.3 不统一的 Go 版本 fatal error: runtime: no plugin module data 除了下面的那些问题以外,还有一个在多机器别离编译主程 / 插件场景下的常见报错。这个报错的一个可能起因是 Go 版本不统一,对齐它们即可。(如果从机器层面就是不能对齐怎么办?)
图 3 -5. Go 版本不统一导致的加载失败 对立解决方案从 3.1 到 3.3,咱们看了一些很难排查,也不是很好解决的问题。除此之外,其实还有一些问题没有被重点介绍进来。作为一个编程语言官网反对的扩大机制,做的如此用户不敌对的确出乎意料。我所在的团队因为重点依赖 Go 的插件机制做定开,因而必须拿出一个系统化的计划把这些问题通通解决掉。在尝试间接批改 Go 源码无果当前(吐槽:Go 插件机制源码写的令人略感遗憾),我重点从以下几个方面动手发展了相干工作:对立编译环境:提供一个规范的 docker image 用来编译主程序和插件,躲避任何 go 版本、gopath 门路、用户名等不统一所带来的问题预制 go/pkg/mod,尽可能减少因为没有应用 vendor 模式导致每次编译都要从新下载依赖的问题对立 Makefile:提供一套主程序和插件的编译 Makefile,躲避任何因为 go build 命令带来的问题对立插件开发脚手架:由脚手架,而不是开发者拉齐插件与主程序的依赖版本。并由脚手架解决其余相干问题 ACI 化:将编译流程 aci 化,进一步避免出现谬误
图 4 -1. 对立解决方案 至此,对于 Go 插件的常见问题及解决办法介绍就暂告段落了,心愿对你有所帮忙。Bonus 如果真的想从根本上搞清楚插件校验的机制,那这里为你提供一些疾速进入源码浏览状态的入口。我应用的 Go 源码为 1.15.2 版本。相干 Go 源码地位:compilergo/src/cmd/compile/linkergo/src/cmd/link/internal/ld/package loadergo/src/cmd/go/internal/load/runtimego/src/runtime/5.1 go build 到底在做啥你能够在 go build 命令里增加 -x 参数,以显式的打印出 Go 程序编译、链接、打包的全流程,例如:go build -x -buildmode=plugin -o ../calc_plugin.so calc_plugin.go5.2 指标代码生成 go/src/cmd/compile/internal/gc/obj.go:55:留神第 67 和第 72 行,这里是两个入口 go/src/cmd/compile/internal/gc/iexport.go:244:留神 280 行,这里会记录 path 相干数据 5.3 库哈希生成算法 go/src/cmd/link/internal/ld/lib.go:967:留神第 995~1025 行,这里计算 pkg 的 hash5.4 库哈希校验 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 赋值点 开发者评测局第五期——函数计算 Serverless 评测征集令 开发者评测局上新啦,第五期评测函数计算 Serverless 江湖征集令来袭,全新玩法,权利降级,价值千元高级版产品乘风者收费限量专享。群雄争霸夺好礼,Beats 耳机、机械键盘、千元天猫超市卡等好礼等你来拿,公布你的评测成为江湖新“一代宗师”。点击这里,查看详情。原文链接:http://click.aliyun.com/m/100… 本文为阿里云原创内容,未经容许不得转载。