go-plugin 插件系统详解
基于 HashiCorp go-plugin 库的插件系统,通过 RPC 实现主程序(Host)与独立进程(Plugin)之间的通信。
一、架构概览
1 2 3 4 5 6
| ┌─────────────────┐ RPC ┌─────────────────┐ │ Host (main) │ ◄─────────────────► │ Plugin (子进程) │ │ │ │ │ │ - 加载插件进程 │ │ - 实现接口 │ │ - 调用 RPC │ │ - 提供服务 │ └─────────────────┘ └─────────────────┘
|
核心特点:
- 进程隔离:Plugin 崩溃不影响 Host
- 语言无关:Plugin 可以用任何语言实现(只要支持 RPC)
- 动态加载:运行时加载/替换插件
二、核心组件
1. 定义接口 (shared/xxx_interface.go)
1 2 3
| type Provider interface { Provide() string }
|
该接口是 Plugin 端和 Host 端都需实现的。
2. 四个关键结构体的协作
在示例中有4个结构体,一同协作实现了插件系统最核心的工作流;默认情况下, Host 作为 RPC 客户端,Plugin 作为 RPC 服务端提供服务。
| 结构体 |
位置 |
角色 |
JsonProvider |
Plugin 端 |
真实业务实现 |
ProvideRPCServer |
Plugin 端 |
RPC 服务端,接收请求 |
ProvideClientRPC |
Host 端 |
RPC 客户端代理,转发调用 |
ProvidePlugin |
胶水层 |
连接两端的桥梁 |
协作流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| ┌─────────────────────────── HOST 进程 ───────────────────────────┐ │ │ │ ProvideClientRPC │ │ ┌─────────────────────────────────┐ │ │ │ 实现 Provider 接口 │ │ │ │ Provide() 方法 │ │ │ │ 内部持有 rpc.Client │ ──┐ │ │ └─────────────────────────────────┘ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ 网络 RPC 调用 │ │ │ └──────────────────┘ │ └────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────── PLUGIN 进程 ───────────────────────┐ │ │ │ ┌──────────────────┐ │ │ │ 网络 RPC 服务 │ │ │ └──────────────────┘ │ │ │ │ │ ProvideRPCServer ▼ │ │ ┌─────────────────────────────────┐ │ │ │ │ 实现 net/rpc.Server 接口 │ │ │ │ │ Provide() 方法 │◄──┘ │ │ │ 内部持有 JsonProvider │ │ │ └─────────────────────────────────┘ │ │ │ │ │ ▼ │ │ JsonProvider (真实业务实现) │ │ ┌─────────────────────────────────┐ │ │ │ 实现 Provider 接口 │ │ │ │ Provide() 返回实际结果 │ │ │ └─────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────┘
ProvidePlugin 是"胶水": • Server() 方法 → 返回 ProvideRPCServer • Client() 方法 → 返回 ProvideClientRPC
|
调用流程:
- Host 调用
ProvideClientRPC.Provide()
ProvideClientRPC 通过 rpc.Client 发送网络请求
- Plugin 端的
ProvideRPCServer.Provide() 接收请求
ProvideRPCServer 调用 JsonProvider.Provide() 获取真实结果
- 结果沿原路返回给 Host
3. Plugin 实现 (shared/xxx_interface.go)
1 2 3 4 5 6 7 8 9 10 11
| type ProvidePlugin struct { Impl Provider }
func (p *ProvidePlugin) Server(broker *plugin.MuxBroker) (interface{}, error) { return &ProvideRPCServer{Impl: p.Impl}, nil }
func (p *ProvidePlugin) Client(broker *plugin.MuxBroker, client *rpc.Client) (interface{}, error) { return &ProvideClientRPC{client: client}, nil }
|
4. Host 端 (main.go)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: handshakeConfig, Plugins: pluginMap, Cmd: exec.Command("./plugin/json"), Logger: logger, }) defer client.Kill()
rpcClient, err := client.Client()
raw, err := rpcClient.Dispense("provider") greeter := raw.(shared.Provider)
greeter.Provide()
|
5. Plugin 端 (plugin/main.go)
1 2 3 4 5 6 7 8 9 10 11 12
| func main() { jsonA := &JsonProvider{logger: logger}
pluginMap := map[string]plugin.Plugin{ "provider": &shared.ProvidePlugin{Impl: jsonA}, }
plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: handshakeConfig, Plugins: pluginMap, }) }
|
三、Dispense 的工作原理
rpcClient.Dispense("provider") 中的 “provider” 是 key:
| 位置 |
代码 |
说明 |
| Host 端 |
rpcClient.Dispense("provider") |
请求 key 为 “provider” 的插件 |
| Plugin 端 |
pluginMap = {"provider": &ProvidePlugin{...}} |
用 “provider” 注册 |
流程:
- Host 调用
Dispense("provider")
- go-plugin 框架向 Plugin 发送请求,携带 key
- Plugin 查找
pluginMap[key],调用 Server() 返回 RPCServer
- Host 端调用
Client() 获取代理对象
- 代理对象类型断言为接口使用
四、Handshake 机制
HandshakeConfig 结构
1 2 3 4 5
| var handshakeConfig = plugin.HandshakeConfig{ ProtocolVersion: 1, MagicCookieKey: "BASIC_PLUGIN", MagicCookieValue: "hello", }
|
三个属性的作用
| 属性 |
作用 |
| ProtocolVersion |
校验 Host 和 Plugin 的协议版本是否一致,不一致则拒绝连接 |
| MagicCookieKey |
用于验证的 key(相当于校验字段名) |
| MagicCookieValue |
用于验证的 value(相当于校验密码) |
工作流程
1 2 3 4 5 6 7 8 9 10
| Host 启动 Plugin 进程 ↓ 发送 HandshakeConfig 信息(包含 version、cookie) ↓ Plugin 验证: - version 是否匹配? - cookie key/value 是否正确? ↓ 验证失败 → 显示友好错误信息 → 进程退出 验证成功 → 建立 RPC 连接
|
验证失败的场景
- 版本不匹配 - Host 用 v1,Plugin 用 v2
- 误执行插件目录 - 用户直接运行
./plugin/ 而不是可执行文件
- 错误的插件 - 加载了不相关的可执行文件
示例错误信息
1 2 3 4 5 6
| 错误的 Plugin: This plugin is not a HashiCorp go-plugin plugin. It appears to be a directory or other invalid plugin.
版本不匹配: Incompatible api version. Plugin: 2, Host: 1
|
总结
- 不是安全机制 - Cookie 是明文传输,可被伪造
- 是防错机制 - 防止用户因为操作错误导致困惑
- 建议保持一致 - Host 和 Plugin 两端的 HandshakeConfig 应完全相同
五、双向通信
| 方向 |
是否支持 |
说明 |
| Host → Plugin |
✅ 默认 |
Plugin 提供服务 |
| Plugin → Host |
✅ 可选 |
需用 MuxBroker 建立回调通道 |
通过 MuxBroker 可以让 Plugin 主动回调 Host。
六、性能开销
| 调用方式 |
延迟 |
| 直接函数调用 |
~1-10 ns |
| go-plugin RPC |
~50-500 μs |
| 本地网络 RPC |
~100μs - 1ms |
开销来源:
- 序列化(gob 编码)
- 进程间通信(socket)
- 上下文切换
- 反射开销
适用场景:调用频率不高的、需要进程隔离的场景。
七、”Plugin.Provide” 中的 “Plugin” 是什么?
在 provider_interface.go:19 中:
1
| err := p.client.Call("Plugin.Provide", new(interface{}), &resp)
|
"Plugin.Provide" 是 net/rpc 的调用格式:"类型名.方法名"。
那为什么是 “Plugin.Provide” ?
“Plugin” 是 go-plugin 框架硬编码的服务名。客户端调用 "Plugin.Provide" 正是因为服务端用
server.RegisterName("Plugin", impl) 注册了服务。
源码位置
在 go-plugin 库的 rpc_server.go:195 行:
1 2 3 4 5 6 7 8 9 10 11 12
| go func () { ... serve(conn, "Plugin", impl) }()
func serve(conn io.ReadWriteCloser, name string, v interface{}) { server := rpc.NewServer() if err := server.RegisterName(name, v); err != nil { ... } server.ServeConn(conn) }
|