Go 原生插件使用问题全解析

文|丁飞(花名:路德)

蚂蚁集团高级工程师

Go 原生插件使用问题全解析

深耕于 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 言语的“运转时”被编译器编译成了二进制方针代码的一部分。

Go 原生插件使用问题全解析

图 1-1. Java 程序、runtime 和 OS 联系

Go 原生插件使用问题全解析

图 1-2. C/C++ 程序、runtime 和 OS 联系

Go 原生插件使用问题全解析

图 1-3. Go 程序、runtime 和 OS 联系

二、Go 原生插件机制

作为一个看起来更贴近 C/C++ 技能栈的 Go 言语来说,支撑相似动态链接库的扩展一直是社区中较为激烈的诉求。

如图 1-5,Go 在规范库中专门供给了一个plugin包,作为插件的言语级编程界面,src/plugin包的本质是运用 cgo 机制调用 unix 的规范接口:dlopen()和dlsym() 。因而,它给 C/C++ 背景的程序员一种“这题我会”的错觉。

Go 原生插件使用问题全解析

图 1-4. C/C++ 程序加载动态链接库

Go 原生插件使用问题全解析

图 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 指令按下面的比方拼装好:

Go 原生插件使用问题全解析

留意,goland 拼装的编译指令里包括要害的

-gcflags all=-N -l参数,可是插件编译的指令里没有。此刻,你在测验拉起插件时就会得到一个有关runtime/internal/sys的报错。

Go 原生插件使用问题全解析

图 2-1. 编译 flag 不一同导致的加载失利

处理这一类规范库版别不一同问题的计划比较简单:尽或许对齐主程序和插件编译的 flag。事实上,有一些 flag 是不影响插件加载的,你能够在详细的实践中渐渐摸索。

二、不一同的第三方库版别

假如运用 vendor 来办理 Go 的依靠库,那么当处理上一节的问题之后,你 100% 会立即遇到以下这个报错:

plugin was built with a different version of package xxxxxxxx

其间,xxxxxxxx指的是某一个详细的三方库,比方github.com/stretchr/te…。这个报错有几个十分典型的原因,假如没有相关的排查经历,其间几个或许会烧掉开发人员不少时刻。

Case 1. 版别不一同

如报错所示,似乎原因很清晰,即主程序和插件所一同依靠的某个第三方库版别不一同,报错中会清晰告诉你哪一个库有问题。此刻,你能够比照排查主程序和插件的go.mod文件,别离找到问题库的版别,看看他们是否一同。假如这时分你发现主程和插件的确有commitid或tag的不一同问题,那处理的办法也很简单:对齐它们

可是在很多场景下,你只会用到三方库的一部分:如一个 package,或许仅仅引了某些 interface。这一部分的代码在不同的版别里或许根本就没有改变,但其他没用到的代码的改变,同样会导致整个三方库版别的改变,进而导致你成为那个“版别不一同”的无辜受害者。

并且,此刻你或许立即会遇到另一个问题:以谁为基准对齐?主程序?还是插件?

从常理上来说,以主程序为基线进行对齐是一个比较好的战略,毕竟插件是新添加的“附属品”,且主程序与插件一般是“一对多”的联系。可是,假如插件的三方库依靠由于任何原因便是不能和主程序对齐怎么办?在测验了很久今后,我暂时没有找到一个完美处理这个问题的办法。

假如版别无法对齐,就只能从根本上放弃走插件这条路。

Go 言语的这种对三方库的、简直无脑的强一同性束缚,从一方面来说,防止了运转时由于版别不一同带来的潜在问题;从另一方面来说,这种故意不给程序员灵敏度的规划,对插件化、定制化、扩展化开发十分的不友好。

Go 原生插件使用问题全解析

图 2-2. 一同依靠的三方库版别不一同导致的加载失利

Case 2. 版别号一同,代码不一同

当你依照 case 1 的思路排查go.mod文件,可是惊讶的发现报错的库版别是一同的时分,工作就会变得杂乱起来。你或许会拿出世界上最先进的文本查验工具,并花掉一个上午去diff三方库的commitid,但它们便是如出一辙,似乎陷入了薛定谔的版别。

呈现这个问题或许的一个不是原因的原因是:有人直接修正了 vendor 目录下的代码,Go 插件机制会对代码内容的一同性进行校验。

这真的是一个十分令人头大,并难以排查的原因。除了修正代码的那个人,和现已在其他 case 中被“坑”过的那些人,没人会知道这件工作。假如修正的 vendor 代码呈现在主程序里,你就简直没有任何靠谱的办法让它们正常工作起来。

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

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

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

回馈开源社区,或许 fork-replace!!!

好消息是,你不需求处理这个问题。由于即便处理了,也还会有更大的问题等着你。

Go 原生插件使用问题全解析

图 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。

Go 原生插件使用问题全解析

图 2-4. 运用 vendor 机制办理第三方库导致的加载失利

同样的问题也或许会呈现在运用不同机器/用户,别离编译主程序、插件的场景下:用户名不同,go 代码的途径应该也会不一样。

处理这类问题的办法很暴力直接:删掉主程序和插件的 vendor 目录,或许运用-mod=readonly编译 flag

到这儿,假如你是运用同一台机器进行主程序和插件的编译,那么常见的问题应该都基本处理了,插件机制理应能够正常工作。另一方面,由于不再运用 vendor 办理依靠,因而 case 2 的问题也会在这儿被强制处理:要么提 PR 给社区,要么 fork-replace。

Go 原生插件使用问题全解析

图 2-5. 成功加载

三、不一同的 Go 版别

fatal error: runtime: no plugin module data

除了上面的那些问题以外,还有一个在多机器别离编译主程/插件场景下的常见报错。这个报错的一个或许原因是 Go 版别不一同,对齐它们即可。(假如从机器层面便是不能对齐怎么办?……)

Go 原生插件使用问题全解析

图 2-6. Go 版别不一同导致的加载失利

PART. 3–一致处理计划

从第二 Part 中,咱们看了一些既很难排查,也不是很好处理的问题。除此之外,其实还有一些问题没有被要点介绍进来。作为一个编程言语官方支撑的扩展机制,做的如此用户不友好的确出人意料。

由于「专有云 MOSN」要点依靠 Go 的插件机制做定开,因而有必要拿出一个体系化的计划把这些问题通通处理掉。在测验直接修正 Go 源码无果今后 (吐槽:Go 插件机制源码写的令人略感惋惜) ,咱们要点从“产品层”及外围基础设施入手开展了相关工作:

一致编译环境:

  • 供给一个规范的 docker image 用来编译主程序和插件,躲避任何 go 版别、gopath 途径、用户名等不一同所带来的问题;
  • 预制go/pkg/mod,尽或许减少由于没有运用 vendor 形式导致每次编译都要重新下载依靠的问题。

一致 Makefile:

  • 供给一套主程序和插件的编译 Makefile,躲避任何由于 go build 指令带来的问题。

一致插件开发脚手架:

  • 由脚手架,而不是开发者拉齐插件与主程序的依靠版别。并由脚手架处理其他相关问题。

流水线化:

  • 将编译布置流水线化,进一步防止呈现错误。

Go 原生插件使用问题全解析

图 3-1. 一致处理计划

PART. 4–要害源码方位

假如真的想从根本上搞清楚插件校验的机制,那这儿为你供给一些快速进入源码阅览状况的进口。我运用的 Go 源码为 1.15.2 版别。相关 Go 源码方位:

compiler:go/src/cmd/compile/*

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

pkgloader: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 逻辑、注册中心/装备中心对接等在内的多方面才干进行插件化支撑。

了解更多……

MOSNStar 一下✨: github.com/mosn/mosn

和咱们一同共建吧

本周引荐阅览

MOSN 构建 Subset 优化思路共享

Go 原生插件使用问题全解析

MOSN 文档运用指南

Go 原生插件使用问题全解析

MOSN 1.0 发布,敞开新架构演进

Go 原生插件使用问题全解析

MOSN Contributor 采访|开源能够是做量力而行的事

Go 原生插件使用问题全解析