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 指的是某一个具体的三方库,比如http://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源码位置:

  • compiler
    • go/src/cmd/compile/*
  • linker
    • go/src/cmd/link/internal/ld/*
  • package loader
    • go/src/cmd/go/internal/load/*
  • runtime
    • go/src/runtime/*

5.1 go build到底在做啥

你可以在go build 命令里添加-x 参数,以显式的打印出Go程序编译、链接、打包的全流程,例如:

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

5.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的hash

5.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赋值点

作者 | 丁飞

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/510609.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从云计算到函数计算

从云计算到函数计算 函数计算,你的名字 云计算,是一种基于互联网的计算方式,通过这种方式,共享的软硬件资源和信息可以按需求提供给计算机各种终端和其他设备,使用服务商提供的电脑基建作计算资源,因此用…

基于 OPLG 从 0 到 1 构建统一可观测平台实践

应用架构与可观测技术演进历程 在软件开发早期,单体应用架构因其结构简单,便于测试和部署,得到了广泛的应用,对应的监控诊断技术主要是基于日志和日志关键词的指标监控。随着软件复杂度的不断提升,单体应用架构逐步向分…

从运维到运维大神,只需要一个正确的选择

马上就是7月24日了,听群里的朋友说,7和24这两个数字是运维工作的最佳体现——7X24小时待命,所以咱们IT人将这一天自定义为“运维日”。 对于运维工作来说,想要在黑天鹅横飞,灰犀牛直撞的当下,既能独善其身…

主流定时任务解决方案全横评

定时任务作为一种按照约定时间执行预期逻辑的通用模式,在企业级开发中承载着丰富的业务场景,诸如后台定时同步数据生成报表,定时清理磁盘日志文件,定时扫描超时订单进行补偿回调等。 程序开发人员在定时任务领域有着诸多框架和方…

基于阿里云 Serverless 函数计算开发的疫情数据统计推送机器人

一、Serverless函数计算 什么是Serverless? 在《Serverless Architectures》中对 Serverless 是这样子定义的: Serverless was first used to describe applications that significantly or fully incorporate third-party, cloud-hosted applications…

看 Serverless Task 如何解决任务调度可观测性中的问题

在上篇文章《解密函数计算异步任务能力之「任务的状态及生命周期管理」》中,我们介绍了任务系统的状态管理,并介绍了用户应如何根据需求,对任务状态信息进行实时的查询等操作。在本篇中我们将会进一步走进函数计算异步任务,介绍异…

B站每日自动签到传统单节点网站的 Serverless 上云

什么是函数?刚刚考完数学没多久的我,脑力里立马想到的是自变量、因变量、函数值,也就是yf(x)。当然,在计算机里,函数function往往指的是一段被定义好的代码程序,我们可以通过传参调用这个定义好的函数&…

通过部署流行 Web 框架掌握 Serverless 技术

大家好,我是霍大侠,这个系列课程我们通过部署流行web框架,来学习掌握serverless的技术和架构。课程主要从实践介绍,实践演示,分析详解三个大的章节来一步一步学习。 前言 进入实验室-动手实践 点击下面链接进入阿里云…

一首歌的时间,手把手搭建基于FC的网站

部署网站 说好不哭 在接触serverless架构之前,我们如果想实现上线一个Web网站,就要在开发前期经过操作很多冗杂但又必须的步骤,不少小白可谓是快速的从入门到退坑。 编写代码,部署应用,部署数据库,申请域…

PolarDB-X 源码解读:事务的一生

概述 本文将主要解读 PolarDB-X 中事务部分的相关代码,着重解读事务的一生在计算节点(CN)中的关键代码:从开始、执行、到最后提交这一整个生命周期。 在阅读本文前,强烈推荐先阅读与 PolarDB-X 事务系统相关的文章&a…

阿里云云原生一体化数仓 — 湖仓一体新能力解读

一、基于 MaxCompute 的湖仓一体架构更新 基于MaxCompute 云数据仓库的湖仓一体架构近期进行架构升级。了解 MaxCompute 的同学可能比较清楚,MaxCompute 有两层结构,需要先创建 Project ,在 Project 里面创建表、资源等。传统数据库&#xf…

DM8168 DVRRDK软件框架研究

DM8168 DVRRDK软件框架研究 2016-07-26 11:39 72人阅读 评论(0) 收藏 举报分类:DM8168(18) Netra(DM8168)处理器是个多核处理器,每个核之间相互独立却又相互关联,如何高效简洁地利用每个核完成一…

基于函数计算自定义运行时快速部署一个 Springboot 项目

什么是函数计算? 函数计算是事件驱动的全托管计算服务。使用函数计算,您无需采购与管理服务器等基础设施,只需编写并上传代码。函数计算为您准备好计算资源,弹性地可靠地运行任务,并提供日志查询、性能监控和报警等功…

FFmpeg源代码简单分析:avformat_open_input()

登录 | 注册 收藏成功 确定收藏失败,请重新收藏 确定标题 标题不能为空网址 标签 摘要 公开 取消收藏 查看所有私信查看所有通知 暂没有新通知返回通知列表 下一条 上一条 分享资讯传PPT/文档提问题写博客传资源创建项目创建代码片baidu_34732018编辑自我介绍&…

硬之城携手阿里云 Serverless 应用引擎(SAE)打造低代码平台

硬之城成立于 2015 年,是一家以电子元器件 BOM 整体供应为核心,为中小科技型硬件企业提供 BOM 标准化、BOM 报价、BOM 采购、BOM 交付和 SMT 一站式 PCBA 服务的电子产业数字供应链与智能制造平台。 电子产业互联网的需求是离散和复杂多变的&#xff0c…

阿里云 Serverless 异步任务处理系统在数据分析领域的应用

异步任务处理系统中的数据分析 数据处理、机器学习训练、数据统计分析是最为常见的一类离线任务。这类任务往往都是经过了一系列的预处理后,由上游统一发送到任务平台进行批量训练及分析。在处理语言方面,Python 由于其所提供的丰富的数据处理库&#x…

代码重构:面向单元测试

重构代码时,我们常常纠结于这样的问题: 需要进一步抽象吗?会不会导致过度设计?如果需要进一步抽象的话,如何进行抽象呢?有什么通用的步骤或者法则吗? 单元测试是我们常用的验证代码正确性的工具…

如何把 thinkphp5 的项目迁移到阿里云函数计算来应对流量洪峰?

1. 为什么要迁移到阿里云函数? 我的项目是一个节日礼品领取项目,过节的时候会有短时间的流量洪峰。平时访问量很低。之前的架构是购买的阿里云alb多台ecs云msyql云redis。最大的问题就是成本问题。平时流量低的时候ecs成本也无法缩减。 阿里云函数计算…

[总结]视音频编解码技术零基础学习方法

0. 生活中的视音频技术 平时我们打开电脑中自己存电影的目录的话,一般都会如下图所示,一大堆五花八门的电影。(其实专业的影视爱好者一概会把影视文件分门别类的,但我比较懒,一股脑把电影放在了一起) 因…

Helm Chart 多环境、多集群交付实践,透视资源拓扑和差异

Helm Charts[1] 如今已是一种非常流行的软件打包方式,在其应用市场中你可以找到接近一万款适用于云原生环境的软件。然后在如今的混合云多集群环境中,业务越来越依赖部署到不同的集群、不同的环境、同时指定不同的配置。再这样的环境下,单纯依…