文|丁飞(花名:路德)
蚂蚁集团高级工程师
深耕于 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/te…。这个报错有几个十分典型的原因,假如没有相关的排查经历,其间几个或许会烧掉开发人员不少时刻。
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/*
– 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 优化思路共享
MOSN 文档运用指南
MOSN 1.0 发布,敞开新架构演进
MOSN Contributor 采访|开源能够是做量力而行的事