代码扩展框架选型。Go 不支持加载动态库(虽然标准库 plugin 会把插件编译成动态库,但是跟通常语义下的动态库不一样)。贴近需求的有这几种方案。
Go plugin 标准库
Go 提供了 plugin 标准库:
- 优点:性能好,与普通 go 代码无异
- 缺点:
- 基本上是个半成品,官方不投入,累积了大量 issue
- 比如 app 需要与 plugin 有同样的 GOPATH,需要是同一个 Go 编译器编译出来的,需要开启 CGO,不支持 Windows 等
- Plugin 需要单独编译
独立 Plugin 进程
Plugin 作为单独进程,再通过 RPC 与 app 通信的方案,是最主流的。其中最好的实现是 hashicorp/go-plugin。
- 优点(见 hashicorp/go-plugin 的 文档):
- 隔离性好;多语言支持;plugin 和 app 可以分开运维
- 缺点:
- 性能差,app 与 plugin 通信需要走 UNIX socks / TCP
- Plugin 需要单独编译
- App 需要用代码管理 plugin 的启停
HashiCorp 表示他们早在 2012 年开始用这套机制,服务了海量请求,稳定性应该是足够的。其他的类似方案,大多是个人作品,缺乏商业公司支持。
Go 解释器(traefik/yaegi)
这种方案比较新,主流的实现是 traefik/yaegi。
- 优点:
- 性能好,仅比原生 go 代码慢 10 倍左右
- 灵活性好,plugin 代码可以在 app 运行时被编译,不需要单独编译及维护
- 官方声称的 features
- 缺点:
- 有一些 限制;涉及 interface / 反射的一些场景无法被正确解析。这使得无法在插件中运行一些很依赖反射的代码(比如 protobuf)
- 官方表示 100% 兼容 go 语法,但是实测很多 interface、反射、跨边界的场景,还是存在问题,无法被正常解析或执行
背景
Traefik 公司 研发了 yaegi,是为了给它的 Traefik proxy 增加插件的能力,并且也实际投入了使用。Yaegi 在 2019 年 7 月发布,目前已经持续开发了 2 年,issue 处理很多(75 open, 574 closed),但仍有一些难以解决的场景。
无法正确解析的场景
写了一个 仓库 演示一个场景。这个问题导致了 protobuf 无法被 yaegi 使用。
对于 protobuf 的场景,有两种做法:
- protobuf 库作为 binary form,通过 symbols 给 yaegi 解释器使用;这种情况在编译协议的 pb.go 文件时会遇到上面说的问题
- 把整个 protobuf 库给 yaegi 尝试解释,仍然会 panic,无法正常解析
另外还有一些涉及 interface 的 issue,是挺常见的代码写法,但是在 yaegi 中无法被正常解析:
Benchmark
实际写了 benchmark,编译比较吃性能(但是是一次性的),实际运行时 yaegi 比原生慢 8 倍(但仍好于 RPC 方案两个数量级):
goos: darwin
goarch: amd64
pkg: git.garena.com/shopee/chatbot/data-sync/common/service/datastore/test
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkCompiling (编译性能)
BenchmarkCompiling-16 3758 281264 ns/op
BenchmarkYaegiRun (使用 yaegi 编译后的代码运行)
BenchmarkYaegiRun-16 269101 4497 ns/op
BenchmarkNativeRun (使用 go 原生代码运行)
BenchmarkNativeRun-16 1453172 820.5 ns/op
PASS
扩展性
- yaegi 可以提供完整的 go 标准库供使用,可以覆盖绝大部分 case
- 可以预先写一批通用函数给用户使用,比如各类 digest 的函数(HMAC,CRC32 等)
- 如果用户实在有使用第三方库的需求,yaegi 也可支持
安全考虑
- yaegi 默认禁用了 go 标准库中不安全的部分(
unsafe
,syscall
),使得 handler 访问其范围之外的内存并不容易(考虑用白名单而不是黑名单) - Plugin 如果 panic,仅会影响该任务自身,也有日志供用户定位问题
- 可以进一步限制开放的标准库范围
指标对比
来自 uberswe/go-plugin-benchmark:
Name | Operations | ns/op |
go plugin package | 76898038 | 13.89 ns/op |
hashicorp/go-plugin | 24984 | 42563 ns/op |
natefinch/pie | 33962 | 37022 ns/op |
dullgiulio/pingo over tcp | 22694 | 53541 ns/op |
dullgiulio/pingo over unix | 29288 | 36140 ns/op |
elliotmr/plug | 96416 | 12674 ns/op |
traefik/yaegi | 1771837 | 713.4 ns/op |
整合成本:yaegi < hashicorp/go-plugin < go plugin package。
但 yaegi 存在比较大的问题是,代码无法被正确解释。