Higress项目解析(二):Proxy-Wasm Go SDK
3、Proxy-Wasm Go SDK
Proxy-Wasm Go SDK 依赖于 tinygo,同时 Proxy - Wasm Go SDK 是基于 Proxy-Wasm ABI 规范使用 Go 编程语言扩展网络代理(例如 Envoy)的 SDK,而 Proxy-Wasm ABI 定义了网络代理和在网络代理内部运行的 Wasm 虚拟机之间的接口。通过这个 SDK,可以轻松地生成符合 Proxy-Wasm 规范的 Wasm 二进制文件,而无需了解 Proxy-Wasm ABI 规范,同时开发人员可以依赖这个 SDK 的 Go API 来开发插件扩展 Enovy 功能
1)、Proxy-Wasm Go SDK API
1)Contexts
上下文(Contexts) 是 Proxy-Wasm Go SDK 中的接口集合,它们在 types 包中定义。 有四种类型的上下文:VMContext
、PluginContext
、TcpContext
和 HttpContext
。它们的关系如下图:

VMContext
对应于每个.vm_config.code
,每个 VM 中只存在一个VMContext
VMContext
是PluginContexts
的父上下文,负责创建PluginContext
PluginContext
对应于一个Plugin
实例。一个PluginContext
对应于Http Filter
、Network Filter
、Wasm Service
的configuration
字段配置PluginContext
是TcpContext
和HttpContext
的父上下文,并且负责为处理 Http 流的 Http Filter 或 处理 Tcp 流的 Network Filter 创建上下文TcpContext
负责处理每个 Tcp 流HttpContext
负责处理每个 Http 流
2)Hostcall API
Hostcall API 是指在 Wasm 模块内调用 Envoy 提供的功能。这些功能通常用于获取外部数据或与 Envoy 交互。在开发 Wasm 插件时,需要访问网络请求的元数据、修改请求或响应头、记录日志等,这些都可以通过 Hostcall API 来实现。 Hostcall API 在 proxywasm 包的 hostcall.go 中定义。 Hostcall API 包括配置和初始化、定时器设置、上下文管理、插件完成、共享队列管理、Redis 操作、Http 调用、TCP 流操作、HTTP 请求/响应头和体操作、共享数据操作、日志操作、属性和元数据操作、指标操作
3)插件调用入口 Entrypoint
当 Envoy 创建 VM 时,在虚拟机内部创建 VMContext
之前,它会在启动阶段调用插件程序的 main
函数。所以必须在 main
函数中传递插件自定义的 VMContext
实现。 proxywasm 包的 SetVMContext
函数是入口点。main
函数如下:
func main() {proxywasm.SetVMContext(&myVMContext{})
}type myVMContext struct { .... }var _ types.VMContext = &myVMContext{}// Implementations follow...
2)、跨虚拟机通信
Envoy 中的跨虚拟机通信(Cross-VM communications)允许在不同线程中运行 的Wasm 虚拟机(VMs)之间进行数据交换和通信。这在需要在多个 VMs 之间聚合数据、统计信息或缓存数据等场景中非常有用。 跨虚拟机通信主要有两种方式:
- 共享数据(Shared Data):
- 共享数据是一种在所有 VMs 之间共享的键值存储,可以用于存储和检索简单的数据项
- 它适用于存储小的、不经常变化的数据,例如配置参数或统计信息
- 共享队列(Shared Queue):
- 共享队列允许 VMs 之间进行更复杂的数据交换,支持发送和接收更丰富的数据结构
- 队列可以用于实现任务调度、异步消息传递等模式
1)共享数据 Shared Data
如果想要在所有 Wasm 虚拟机(VMs)运行的多个工作线程间拥有全局请求计数器,或者想要缓存一些应被所有 Wasm VMs 使用的数据,那么共享数据(Shared Data)或等效的共享键值存储(Shared KVS)就会发挥作用。 共享数据本质上是一个跨所有 VMs 共享的键值存储(即跨 VM 或跨线程)
共享数据 KVS 是根据 vm_config 中指定的创建的。可以在所有 Wasm VMs 之间共享一个键值存储,而它们不必具有相同的二进制文件 vm_config.code
,唯一的要求是具有相同的 vm_id

在上图中,可以看到即使它们具有不同的二进制文件( hello.wasm
和 bye.wasm
),vm_id=foo 的 VMs 也共享相同的共享数据存储。hostcall.go
中定义共享数据相关的 API如下:
// GetSharedData 用于检索给定 key 的值
// 返回的 CAS 应用于 SetSharedData 以实现该键的线程安全更新
func GetSharedData(key string) (value []byte, cas uint32, err error)// SetSharedData 用于在共享数据存储中设置键值对
// 共享数据存储按主机中的 vm_config.vm_id 定义
//
// 当给定的 CAS 值与当前值不匹配时,将返回 ErrorStatusCasMismatch
// 这表明其他 Wasm VM 已经成功设置相同键的值,并且该键的当前 CAS 已递增
// 建议在遇到此错误时实现重试逻辑
//
// 将 CAS 设置为 0 将永远不会返回 ErrorStatusCasMismatch 并且总是成功的,
// 但这并不是线程安全的,即可能在您调用此函数时另一个 VM 已经设置了该值,
// 看到的值与存储时的值已经不同
func SetSharedData(key string, value []byte, cas uint32) error
共享数据 API 是其线程安全性和跨 VM 安全性,这通过 CAS (Compare-And-Swap)值来实现。如何使用 GetSharedData
和 SetSharedData
函数可以参考 示例
在 Higress ai-proxy 插件处理 apiToken 的故障转移场景中就运用了该 API,具体代码可以查看 failover.go
2)共享队列 Shared Queue
如果要在请求/响应处理的同时跨所有 Wasm VMs 聚合指标,或者将一些跨 VM 聚合的信息推送到远程服务器,可以通过 Shared Queue 来实现
Shared Queue 是为 vm_id 和队列名称的组合创建的 FIFO(先进先出)队列。并为该组合(vm_id,名称)分配了一个唯一的 queue id,该 ID 用于入队/出队操作
入队和出队等操作具有线程安全性和跨 VM 安全性。在 hostcall.go
中与 Shared Queue 相关 API 如下:
// DequeueSharedQueue 从给定 queueID 的共享队列中出队数据
// 要获取目标队列的 queue id,请先使用 ResolveSharedQueue
func DequeueSharedQueue(queueID uint32) ([]byte, error)// RegisterSharedQueue 在此插件上下文中注册共享队列
// 注册意味着每当该 queueID 上有新数据入队时,将对此插件上下文调用 OnQueueReady
// 仅适用于 types.PluginContext。返回的 queueID 可用于 Enqueue/DequeueSharedQueue
// 请注意 name 必须在所有共享相同 vm_id 的 Wasm VMs 中是唯一的。使用 vm_id 来分隔共享队列的命名空间
//
// 只有在调用 RegisterSharedQueue 之后,ResolveSharedQueue(此 vm_id, 名称) 才能成功
// 通过其他 VMs 检索 queueID
func RegisterSharedQueue(name string) (queueID uint32, err error)// EnqueueSharedQueue 将数据入队到给定 queueID 的共享队列
// 要获取目标队列的 queue id,请先使用 ResolveSharedQueue
func EnqueueSharedQueue(queueID uint32, data []byte) error// ResolveSharedQueue 获取给定 vmID 和队列名称的 queueID
// 返回的 queueID 可用于 Enqueue/DequeueSharedQueues
func ResolveSharedQueue(vmID, queueName string) (queueID uint32, err error)
RegisterSharedQueue
和 DequeueSharedQueue
由队列的消费者使用,而 ResolveSharedQueue
和 EnqueueSharedQueue
是为队列生产者准备的。请注意:
- RegisterSharedQueue 用于为调用者的 name 和 vm_id 创建共享队列。使用一个队列,那么必须先由一个 VM 调用这个函数。这可以由 PluginContext 调用,因此可以认为
消费者 = PluginContexts
- ResolveSharedQueue 用于获取 name 和 vm_id 的 queue id。这是为生产者准备的
这两个调用都返回一个队列 ID,该 ID 用于 DequeueSharedQueue
和 EnqueueSharedQueue
。同时当队列中入队新数据时消费者 PluginContext 中有 OnQueueReady(queueID uint32)
接口会收到通知。 还强烈建议由 Envoy 的主线程上的单例 Wasm Service 创建共享队列。否则 OnQueueReady
将在工作线程上调用,这会阻塞它们处理 Http 或 Tcp 流
在上图中展示共享队列工作原理,更详细如何使用共享队列可以参考 示例
3)、Higress 插件 Go SDK 与处理流程
相对应于 proxy-wasm-go-sdk 中的 VMContext、PluginContext、HttpContext 3 个上下文, 在 Higress 插件 Go SDK 中是 CommonVmCtx、CommonPluginCtx、CommonHttpCtx 3 个支持泛型的 struct。 3 个 struct 的核心内容如下:
// plugins/wasm-go/pkg/wrapper/plugin_wrapper.go
type CommonVmCtx[PluginConfig any] struct {// proxy-wasm-go-sdk VMContext 接口默认实现types.DefaultVMContext// 插件名称pluginName string// 插件日志工具log LoghasCustomConfig bool// 插件配置解析函数parseConfig ParseConfigFunc[PluginConfig]// 插件路由、域名、服务级别配置解析函数parseRuleConfig ParseRuleConfigFunc[PluginConfig]// 以下是自定义插件回调钩子函数onHttpRequestHeaders onHttpHeadersFunc[PluginConfig]onHttpRequestBody onHttpBodyFunc[PluginConfig]onHttpStreamingRequestBody onHttpStreamingBodyFunc[PluginConfig]onHttpResponseHeaders onHttpHeadersFunc[PluginConfig]onHttpResponseBody onHttpBodyFunc[PluginConfig]onHttpStreamingResponseBody onHttpStreamingBodyFunc[PluginConfig]onHttpStreamDone onHttpStreamDoneFunc[PluginConfig]
}type CommonPluginCtx[PluginConfig any] struct {// proxy-wasm-go-sdk PluginContext 接口默认实现types.DefaultPluginContext// 解析后保存路由、域名、服务级别配置和全局插件配置matcher.RuleMatcher[PluginConfig]// 引用 CommonVmCtxvm *CommonVmCtx[PluginConfig]// tickFunc 数组onTickFuncs []TickFuncEntry
}type CommonHttpCtx[PluginConfig any] struct {// proxy-wasm-go-sdk HttpContext 接口默认实现types.DefaultHttpContext// 引用 CommonPluginCtxplugin *CommonPluginCtx[PluginConfig]// 当前 Http 上下文下匹配插件配置,可能是路由、域名、服务级别配置或者全局配置config *PluginConfig// 是否处理请求体needRequestBody bool// 是否处理响应体needResponseBody bool// 是否处理流式请求体streamingRequestBody bool// 是否处理流式响应体streamingResponseBody bool// 非流式处理缓存请求体大小requestBodySize int// 非流式处理缓存响应体大小responseBodySize int// Http 上下文 IDcontextID uint32// 自定义插件设置自定义插件上下文userContext map[string]interface{}// 用于在日志或链路追踪中添加自定义属性userAttribute map[string]interface{}
}
它们的关系如下图:
1)启动入口和 VM 上下文(CommonVmCtx)
func main() {wrapper.SetCtx(// 插件名称"hello-world",// 设置自定义函数解析插件配置,这个方法适合插件全局配置和路由、域名、服务级别配置内容规则是一样wrapper.ParseConfig(parseConfig),// 设置自定义函数解析插件全局配置和路由、域名、服务级别配置,这个方法适合插件全局配置和路由、域名、服务级别配置内容规则不一样wrapper.ParseOverrideConfig(parseConfig, parseRuleConfig),// 设置自定义函数处理请求头wrapper.ProcessRequestHeaders(onHttpRequestHeaders),// 设置自定义函数处理请求体wrapper.ProcessRequestBody(onHttpRequestBody),// 设置自定义函数处理响应头wrapper.ProcessResponseHeaders(onHttpResponseHeaders),// 设置自定义函数处理响应体wrapper.ProcessResponseBody(onHttpResponseBody),// 设置自定义函数处理流式请求体wrapper.ProcessStreamingRequestBody(onHttpStreamingRequestBody),// 设置自定义函数处理流式响应体wrapper.ProcessStreamingResponseBody(onHttpStreamingResponseBody),// 设置自定义函数处理流式请求完成wrapper.ProcessStreamDone(onHttpStreamDone),)
}
根据实际业务需要来选择设置回调钩子函数
跟踪一下 wrapper.SetCtx
的实现:
- 创建 CommonVmCtx 对象同时设置自定义插件回调钩子函数
- 然后再调用
proxywasm.SetVMContext
设置 VMContext
// plugins/wasm-go/pkg/wrapper/plugin_wrapper.go
func SetCtx[PluginConfig any](pluginName string, options ...CtxOption[PluginConfig]) {// 调用 proxywasm.SetVMContext 设置 VMContextproxywasm.SetVMContext(NewCommonVmCtx(pluginName, options...))
}func NewCommonVmCtx[PluginConfig any](pluginName string, options ...CtxOption[PluginConfig]) *CommonVmCtx[PluginConfig] {logger := &DefaultLog{pluginName, "nil"}opts := []CtxOption[PluginConfig]{WithLogger[PluginConfig](logger)}for _, opt := range options {if opt == nil {continue}opts = append(opts, opt)}return NewCommonVmCtxWithOptions(pluginName, opts...)
}func NewCommonVmCtxWithOptions[PluginConfig any](pluginName string, options ...CtxOption[PluginConfig]) *CommonVmCtx[PluginConfig] {ctx := &CommonVmCtx[PluginConfig]{pluginName: pluginName,hasCustomConfig: true,}// CommonVmCtx 里设置自定义插件回调钩子函数for _, opt := range options {opt.Apply(ctx)}if ctx.parseConfig == nil {var config PluginConfigif unsafe.Sizeof(config) != 0 {msg := "the `parseConfig` is missing in NewCommonVmCtx's arguments"panic(msg)}ctx.hasCustomConfig = falsectx.parseConfig = parseEmptyPluginConfig[PluginConfig]}return ctx
}
NewCommonVmCtxWithOptions 方法中遍历 options 调用其 Apply 方法来设置自定义插件回调钩子函数
以 onProcessRequestHeadersOption 为例,其定义及 Apply 方法实现如下:
// plugins/wasm-go/pkg/wrapper/plugin_wrapper.go
type onProcessRequestHeadersOption[PluginConfig any] struct {f onHttpHeadersFunc[PluginConfig]oldF oldOnHttpHeadersFunc[PluginConfig]
}func (o *onProcessRequestHeadersOption[PluginConfig]) Apply(ctx *CommonVmCtx[PluginConfig]) {// 设置 onHttpRequestHeaders 处理函数,这里兼容了旧版本方法(新版本方法中移除了 log 参数)if o.f != nil {ctx.onHttpRequestHeaders = o.f} else {ctx.onHttpRequestHeaders = func(context HttpContext, config PluginConfig) types.Action {return o.oldF(context, config, ctx.log)}}
}func ProcessRequestHeaders[PluginConfig any](f onHttpHeadersFunc[PluginConfig]) CtxOption[PluginConfig] {return &onProcessRequestHeadersOption[PluginConfig]{f: f}
}
2)插件上下文(CommonPluginCtx)
创建 CommonPluginCtx 对象:
通过 CommonVmCtx 的 NewPluginContext 方法创建 CommonPluginCtx 对象, 设置 CommonPluginCtx 的 vm 引用。
// plugins/wasm-go/pkg/wrapper/plugin_wrapper.go
func (ctx *CommonVmCtx[PluginConfig]) NewPluginContext(uint32) types.PluginContext {return &CommonPluginCtx[PluginConfig]{vm: ctx,}
}
插件启动和插件配置解析:
CommonPluginCtx 的 OnPluginStart 部分核心代码如下:
// plugins/wasm-go/pkg/wrapper/plugin_wrapper.go
func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStartStatus {// 调用 proxywasm.GetPluginConfiguration 获取插件配置data, err := proxywasm.GetPluginConfiguration()globalOnTickFuncs = nilif err != nil && err != types.ErrorStatusNotFound {ctx.vm.log.Criticalf("error reading plugin configuration: %v", err)return types.OnPluginStartStatusFailed}var jsonData gjson.Resultif len(data) == 0 {if ctx.vm.hasCustomConfig {ctx.vm.log.Warn("config is empty, but has ParseConfigFunc")}} else {if !gjson.ValidBytes(data) {ctx.vm.log.Warnf("the plugin configuration is not a valid json: %s", string(data))return types.OnPluginStartStatusFailed}pluginID := gjson.GetBytes(data, PluginIDKey).String()if pluginID != "" {ctx.vm.log.ResetID(pluginID)data, _ = sjson.DeleteBytes([]byte(data), PluginIDKey)}// 插件配置转成 jsonjsonData = gjson.ParseBytes(data)}// 设置 parseOverrideConfigvar parseOverrideConfig func(gjson.Result, PluginConfig, *PluginConfig) errorif ctx.vm.parseRuleConfig != nil {parseOverrideConfig = func(js gjson.Result, global PluginConfig, cfg *PluginConfig) error {// 解析插件路由、域名、服务级别插件配置return ctx.vm.parseRuleConfig(js, global, cfg)}}// 解析插件配置err = ctx.ParseRuleConfig(jsonData,func(js gjson.Result, cfg *PluginConfig) error {// 解析插件全局或者当 parseRuleConfig 没有设置时候同时解析路由、域名、服务级别插件配置return ctx.vm.parseConfig(js, cfg)},parseOverrideConfig,)if err != nil {ctx.vm.log.Warnf("parse rule config failed: %v", err)ctx.vm.log.Error("plugin start failed")return types.OnPluginStartStatusFailed}if globalOnTickFuncs != nil {ctx.onTickFuncs = globalOnTickFuncsif err := proxywasm.SetTickPeriodMilliSeconds(100); err != nil {ctx.vm.log.Error("SetTickPeriodMilliSeconds failed, onTick functions will not take effect.")ctx.vm.log.Error("plugin start failed")return types.OnPluginStartStatusFailed}}ctx.vm.log.Info("plugin start successfully")return types.OnPluginStartStatusOK
}
可以发现在解析插件配置过程中有两个回调钩子函数,parseConfig 和 parseRuleConfig
- parseConfig:解析插件全局配置,如果 parseRuleConfig 没有设置,那么 parseConfig 会同时解析全局配置和路由、域名、服务级别配置。也就是说插件全局配置和路由、域名、服务级别配置规则是一样
- parseRuleConfig:解析路由、域名、服务级别插件配置。如果设置 parseRuleConfig,也就是说插件全局配置和路由、域名、服务级别配置规则是不同的
大部分情况下插件全局配置和路由、域名、服务级别配置规则是一样的,因此在定义插件时只需要调用 wrapper.ParseConfigBy(parseConfig)
来设置插件配置解析回调钩子函数。 而有些插件(如 basic-auth)的全局配置和路由、域名、服务级别配置规则是不一样的
3)HTTP 上下文(CommonHttpCtx)
创建 CommonHttpCtx:
CommonPluginCtx 的 NewHttpContext 部分核心代码如下:
// plugins/wasm-go/pkg/wrapper/plugin_wrapper.go
func (ctx *CommonPluginCtx[PluginConfig]) NewHttpContext(contextID uint32) types.HttpContext {httpCtx := &CommonHttpCtx[PluginConfig]{plugin: ctx,contextID: contextID,userContext: map[string]interface{}{},userAttribute: map[string]interface{}{},}// 根据插件实现的函数设置是否需要处理请求和响应的 bodyif ctx.vm.onHttpRequestBody != nil || ctx.vm.onHttpStreamingRequestBody != nil {httpCtx.needRequestBody = true}if ctx.vm.onHttpResponseBody != nil || ctx.vm.onHttpStreamingResponseBody != nil {httpCtx.needResponseBody = true}if ctx.vm.onHttpStreamingRequestBody != nil {httpCtx.streamingRequestBody = true}if ctx.vm.onHttpStreamingResponseBody != nil {httpCtx.streamingResponseBody = true}return httpCtx
}
OnHttpRequestHeaders:
// plugins/wasm-go/pkg/wrapper/plugin_wrapper.go
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {requestID, _ := proxywasm.GetHttpRequestHeader("x-request-id")_ = proxywasm.SetProperty([]string{"x_request_id"}, []byte(requestID))// 获取当前 HTTP 请求生效插件配置config, err := ctx.plugin.GetMatchConfig()if err != nil {ctx.plugin.vm.log.Errorf("get match config failed, err:%v", err)return types.ActionContinue}if config == nil {return types.ActionContinue}// 设置插件配置到 HttpContextctx.config = config// 如果请求 content-type 是 octet-stream/grpc 或者定义 content-encoding,则不处理请求 body// To avoid unexpected operations, plugins do not read the binary content bodyif IsBinaryRequestBody() {ctx.needRequestBody = false}if ctx.plugin.vm.onHttpRequestHeaders == nil {return types.ActionContinue}// 调用自定义插件 onHttpRequestHeaders 回调钩子函数return ctx.plugin.vm.onHttpRequestHeaders(ctx, *config)
}
主要处理逻辑如下:
- 获取匹配当前 HTTP 请求插件配置,可能是路由、域名、服务级别配置或者全局配置
- 设置插件配置到 HttpContext
- 如果请求 content-type 是 octet-stream/grpc 或者定义 content-encoding,则不处理请求 body
- 调用自定义插件 onHttpRequestHeaders 回调钩子函数
关于插件配置可以看出, Higress 插件 Go SDK 封装如下:
- 在插件启动时候,解析插件路由、域名、服务级别插件配置和全局配置保存到 CommonPluginCtx 中
- 在 onHttpRequestHeaders 阶段,根据当前 HTTP 上下文中路由、域名、服务等信息匹配插件配置,返回路由、域名、服务级别配置或者全局配置。然后把匹配到插件配置设置到 HttpContext 对象的 config 属性中,这样自定义插件的所有回调钩子函数就可以获取到这个配置
参考:
Wasm 插件原理
Higress 插件 Go SDK 与处理流程
4)、proxy-wasm-go-sdk tinygo 内存泄漏问题
前置知识:
什么是保守式 GC?
以 JVM 场景下为例:
![]()
对于变量 A,JVM 在得到 A 的值后,能够立刻判断出它不是一个引用。因为引用是一个地址,JVM 中地址是 32 位的,也就是 8 位的 16 进制,很明显 A 是一个 4 位 16 进制,不能作为引用(这里称为对齐检查)
对于变量 D, JVM 也能够立刻判断出它不是引用,因为 Java 堆的上下边界是知道的,如图中所标识的堆起始地址和最后地址,JVM 发现变量 D 的值早就超出了 Java 堆的边界,故认为它不是引用(这里称为上下边界检查)
对于变量 B(实际是一个引用) 和变量 C(实际就是一个 int 型变量),发现它们两个的值是一样的,于是 JVM 就不能判断了。基于这种无法精确识别指针(引用)和非指针(非引用)的垃圾回收方式,被称为保守式 GC
当执行 b = null 之后,对象 B 的实例就应该没有任何指向了,此时它就是个垃圾,应该被回收掉。但是 JVM 错误的认为变量 C 的值是一个引用,因为此时 JVM 很保守,担心会判断错误,所以只好认为 C 也是一个引用,这样,JVM 认为仍然有人在引用对象 B,所以不会回收对象 B
保守式 GC 采用的是模糊的检查方式,这就导致一些实际上已经没有引用指向的对象(即死掉的对象)被错误地认为仍然有引用存在。这些对象无法被垃圾回收器回收,从而造成了无用的内存占用,最终引发资源浪费。这就是保守式 GC 可能导致内存泄漏的核心原因
1)内存泄漏问题
性能问题:
tinygo 最初的 GC 实现性能较差,引入 bdwgc 的保守式 GC 后,性能有了显著提升,例如在 coraza-proxy-wasm 中,每个请求的处理时间从 300ms 缩减到 30ms,GC 暂停时间从几百毫秒减少到 5 - 10ms。但这只是部分情况,并非所有场景都能有如此好的效果
保守式 GC 内存泄漏问题:
保守式 GC 在某些工作负载下会导致无界内存使用。这是因为 32 位、非随机化的地址空间会使指针和普通数学值大量重叠。保守式 GC 在判断一个值是否为指针时,只能通过一些启发式规则进行猜测,当指针和普通数据的值范围重叠时,就可能误判,从而无法正确回收一些不再使用的内存,导致内存不断增长
精确 GC 信息缺失问题:
当尝试使用 bdwgc 的精确 GC 时,虽然能为一些失败的工作负载带来合理的性能,但仍然存在许多内存泄漏的报告。原因是 tinygo 编译器仅在某些情况下为精确 GC 填充信息,而不是所有情况。精确 GC 需要编译器提供准确的对象布局和指针信息,以便准确判断哪些是指针,哪些是普通数据。由于信息不完整,精确 GC 无法正常工作,这本质上还是与保守式 GC 的局限性相关,因为保守式 GC 依赖于不完整或不准确的信息来管理内存
多插件独立 GC 堆导致内存浪费:
即使解决了上述问题,将 bdwgc 集成到 tinygo 中,还会面临另一个问题。当有多个用 Go 编写的 Envoy 插件时,每个插件都有独立的 GC 堆,这会导致大量的内存浪费。因为每个插件的 GC 堆都需要维护自己的内存管理结构,而这些结构可能会有重复,并且无法共享内存资源。虽然 wasm-gc 提案可以解决 GC 语言的这个问题,但由于它不支持内部指针,无法用于 go 语言,并且要实现对 go 语言的支持可能需要大约 2 年的时间
综上所述,保守式 GC 存在性能、内存使用、信息准确性、稳定性和多实例内存管理等多方面的问题,这些问题使得在某些场景下使用保守式 GC 变得困难,甚至不可行
2)社区的后续解决思路
Go 1.24 已支持用原生 Go 编写 Wasm 插件,可通过原生 GC 解决 tinygo + 保守式 GC 的内存泄漏问题。Higress 社区正升级,后续将以 Go 1.24 编写 Wasm 插件为主(代码分支:https://github.com/alibaba/higress/tree/wasm-go-1.24)
使用原生 Go 语言编写 wasm 的一些劣势:
相比于 tinygo 来说,使用原生 Go 语言编写的 Wasm 插件,Wasm 插件的文件大小会更大一些,也有一定的 RT 损耗,详细可以看下 Higress 中使用 Go 1.24 编译 Wasm 插件验证(https://github.com/alibaba/higress/issues/1768)
参考:
保守式 GC 与准确式 GC,如何在堆中找到某个对象的具体位置?
proxy-wasm-go-sdk 内存泄漏问题说明
Higress go wasm 插件内存泄漏相关 issue:
go-wasm插件需要自行考虑gc问题吗?
推荐阅读:
使用 nottinygc 内存泄漏 case
件的文件大小会更大一些,也有一定的 RT 损耗,详细可以看下 Higress 中使用 Go 1.24 编译 Wasm 插件验证(https://github.com/alibaba/higress/issues/1768)
参考:
保守式 GC 与准确式 GC,如何在堆中找到某个对象的具体位置?
proxy-wasm-go-sdk 内存泄漏问题说明
Higress go wasm 插件内存泄漏相关 issue:
go-wasm插件需要自行考虑gc问题吗?
推荐阅读:
使用 nottinygc 内存泄漏 case