一、go-plugin 简介
1.1 go-plugin 是什么?
我们知道 Go 语言缺乏动态加载代码的机制,Go 程序通常是独立的二进制文件,因此难以实现类似于 C++ 的插件系统。即使go的最新标准引入了 go plugin 机制,但是由于限制性条件比较多导致在生产环境中不是很好用,比如插件的编写环境和插件的使用环境要保持一致,如 gopath、go sdk 版本等。
HashiCorp 公司开源的 go-plugin 库解决了上述问题,允许应用程序通过本地网络(本机)的 gRPC 调用插件,规避了 Go 无法动态加载代码的缺点。go-plugin 是一个通过RPC 实现的 Go 插件系统,并在 Packer、Terraform, Nomad、Vault 等由 HashiCorp 主导的项目中均有应用。
顺便说一句,Vault 开源代码,我这几天看了下,代码写的很不错,感兴趣的小伙伴可以看看vault是怎么使用 go-plugin,很值得借鉴,后续会针对 vault 的源代码的插件部分进行剖析。
1.2 特性
go-plugin 的特性包括:
插件是 Go 接口的实现: 这让插件的编写、使用非常自然。对于插件编写者来说,他只需要实现一个 Go 接口即可;对于插件的用户来说,就像在同一个进程中使用和调用函数即可。go-plugin 会处理好本地调用转换为 gRPC 调用的所有细节
跨语言支持:插件可以被任何主流语言编写(和使用),该库支持通过 gRPC 提供服务插件,而基于 gRPC 的插件是允许被任何语言编写的。
支持复杂的参数、返回值: go-plugin 可以处理接口、io.Reader/Writer 等复杂类型,我们为您提供了一个库(MuxBroker),用于在客户端/服务器之间创建新连接,以服务于附加接口或传输原始数据。
双向通信: 为了支持复杂参数,宿主进程能够将接口实现发送给插件,插件也能够回调到宿主进程(这点还需要看官网的双向通信的例子好好理解下)
内置日志系统: 任何使用 log 标准库的的插件,都会自动将日志信息传回宿主机进程。宿主进程会镜像日志输出,并在这些日志前面加上插件二进制文件的路径。这会使插件的调试变简单。如果宿主机使用 hclog,日志数据将被结构化。如果插件同样使用 hclog,插件的日志会发往宿主机并被结构化。
协议版本化: 支持一个简单的协议版本化,可增加版本号使之前插件无效。当接口签名变化、协议版本改变等情况时,协议版本话是很有用的。当协议版本不兼容时,会发送错误消息给终端用户。
标准输出/错误同步: 插件以子进程的方式运行,这些插件可以自由的使用标准输出/错误,并且输出会被镜像回到宿主进程。
TTY Preservation: 插件子进程可以链接到宿主进程的 stdin 标准输入文件描述符,允许以TTY方式运行的软件。
插件运行状态中,宿主进程升级: 插件可以"reattached",所以可以在插件运行状态中升级宿主机进程。NewClient 函数使用 ReattachConfig 选项来确定是否 Reattach 以及如何 Reattach。
加密通信: gRPC信道可以加密
1.3 架构优势
插件不影响宿主机进程:插件崩溃了,不会导致宿主进程崩溃
插件容易编写:仅仅写个 go 应用程序并执行 go build。或者使用其他语言来编写 gRPC 服务 ,加上少量的模板来支持 go-plugin。
易于安装:只需要将插件放到宿主进程能够访问的目录即可,剩下的事情由宿主进程来处理。
完整性校验:支持对插件的二进制文件进行 Checksum
插件是相对安全的:插件只能访问传递给它的接口和参数,而不是进程的整个内存空间。另外,go-plugin 可以基于 TLS 和插件进行通信。
1.4 适用场景
go-plugin 目前仅设计为在本地[可靠]网络上工作,不支持 go-plugin 在真实网络,并可能会导致未知的行为。
即不能将 go-plugin 用于在两台服务器之间的远程过程调用,这点和传统的RPC有很大区别,望谨记。
二、核心数据结构
2.1 Plugin接口
Plugin 是一个接口,是插件进程和宿主进程进行通信的桥梁。
不管是插件编写者还是插件使用者,都需要实现 plugin.Plugin 接口,只是各自的实现不同。
type Plugin interface {
// Server should return the RPC server compatible struct to serve
// the methods that the Client calls over net/rpc.
Server(*MuxBroker) (interface{}, error)
// Client returns an interface implementation for the plugin you're
// serving that communicates to the server end of the plugin.
Client(*MuxBroker, *rpc.Client) (interface{}, error)
}
Server 接口:Server 接口应返回与 RPC server 兼容的结构以提供方法,客户端可以通过net/rpc来调用此方法。
Client 接口:Client 接口返回你提供服务的插件的接口实现,该接口实现将与该插件的服务器端进行通信。
2.2 GRPCPlugin 接口
type GRPCPlugin interface {
// 由于gRPC插件以单例方式服务,因此该方法仅调用一次
GRPCServer(*GRPCBroker, *grpc.Server) error
// 插件进程退出时,context会被go-plugin关闭
GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error)
}
GRPCPlugin的接口实现,在grpc的例子中我们再详细解释。
2.3 plugin.client接口
这个接口负责管理一个插件进程的完整生命周期,包括创建插件进程、连接到插件进程、分配接口实现、处理杀死进程。
对于每个插件,宿主机进程需要创建一个plugin.Client实例。
type Client struct {
// 插件客户端配置
config *ClientConfig
// 插件进程是否已经退出
exited bool
l sync.Mutex
// 插件进程的RPC监听地址
address net.Addr
// 插件进程对象
process *os.Process
// 协议客户端,宿主进程需要调用其Dispense方法来获得业务接口的Stub
client ClientProtocol
//