Golang: Plugin System

 5th January 2022 at 6:42pm

代码扩展框架选型。Go 不支持加载动态库(虽然标准库 plugin 会把插件编译成动态库,但是跟通常语义下的动态库不一样)。贴近需求的有这几种方案。

Go plugin 标准库

Go 提供了 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

扩展性

  1. yaegi 可以提供完整的 go 标准库供使用,可以覆盖绝大部分 case
  2. 可以预先写一批通用函数给用户使用,比如各类 digest 的函数(HMAC,CRC32 等)
  3. 如果用户实在有使用第三方库的需求,yaegi 也可支持

安全考虑

  1. yaegi 默认禁用了 go 标准库中不安全的部分(unsafe, syscall),使得 handler 访问其范围之外的内存并不容易(考虑用白名单而不是黑名单)
  2. Plugin 如果 panic,仅会影响该任务自身,也有日志供用户定位问题
  3. 可以进一步限制开放的标准库范围

指标对比

来自 uberswe/go-plugin-benchmark

NameOperationsns/op
go plugin package7689803813.89 ns/op
hashicorp/go-plugin2498442563 ns/op
natefinch/pie3396237022 ns/op
dullgiulio/pingo over tcp2269453541 ns/op
dullgiulio/pingo over unix2928836140 ns/op
elliotmr/plug9641612674 ns/op
traefik/yaegi1771837713.4 ns/op

整合成本:yaegi < hashicorp/go-plugin < go plugin package。

但 yaegi 存在比较大的问题是,代码无法被正确解释。