Higress项目解析(二):Proxy-Wasm Go SDK

article/2025/6/27 15:49:03

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 包中定义。 有四种类型的上下文:VMContextPluginContextTcpContextHttpContext。它们的关系如下图:

  1. VMContext 对应于每个 .vm_config.code,每个 VM 中只存在一个 VMContext
  2. VMContextPluginContexts 的父上下文,负责创建 PluginContext
  3. PluginContext 对应于一个 Plugin 实例。一个 PluginContext 对应于 Http FilterNetwork FilterWasm Serviceconfiguration 字段配置
  4. PluginContextTcpContextHttpContext 的父上下文,并且负责为处理 Http 流的 Http Filter 或 处理 Tcp 流的 Network Filter 创建上下文
  5. TcpContext 负责处理每个 Tcp 流
  6. 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.wasmbye.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)值来实现。如何使用 GetSharedDataSetSharedData 函数可以参考 示例

在 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)

RegisterSharedQueueDequeueSharedQueue 由队列的消费者使用,而 ResolveSharedQueueEnqueueSharedQueue 是为队列生产者准备的。请注意:

  • RegisterSharedQueue 用于为调用者的 name 和 vm_id 创建共享队列。使用一个队列,那么必须先由一个 VM 调用这个函数。这可以由 PluginContext 调用,因此可以认为 消费者 = PluginContexts
  • ResolveSharedQueue 用于获取 name 和 vm_id 的 queue id。这是为生产者准备的

这两个调用都返回一个队列 ID,该 ID 用于 DequeueSharedQueueEnqueueSharedQueue。同时当队列中入队新数据时消费者 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 的实现:

  1. 创建 CommonVmCtx 对象同时设置自定义插件回调钩子函数
  2. 然后再调用 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


http://www.hkcw.cn/article/gdpceXHHBx.shtml

相关文章

MySQL日志

日志 MySQL中的日志有:错误日志、二进制日志、查询日志、慢查询日志 1,错误日志 2,二进制日志 主从复制就是基于二进制日志 相关的三个参数: log_bin:表示二进制日志启动状态 log_bin_basename:最终生…

AE特效软件|after effects2025网盘下载与安装教程指南

如大家所熟悉的,本文要介绍的AE,是Adobe After Effects的简称。说起AE,大家可能会很快联系上PR(Premiere)。PR与AE算得上是兄弟软件,前者可以对创作者的影视作品进行剪辑处理,后者则可对后期作品…

LightEMMA:用于自动驾驶的轻量级端到端多模态模型

25年5月来自密歇根大学和密歇根大学交通研究所的论文“LightEMMA: Lightweight End-to-End Multimodal Model for Autonomous Driving”。 视觉-语言模型 (VLM) 已展示出端到端自动驾驶的巨大潜力。然而,充分利用其安全可靠的车辆控制能力仍然是一个开放的研究挑战…

案例研究 | Genspark 携手 Claude 共创 AI Agents 新未来

最近在调研自主规划 Agentic 系统的架构设计,尤其是多 Agents 协作这一块。 看到 Claude 刚发布的一篇关于 Genspark 的案例研究,觉得很有借鉴意义。 所以将文章翻译出来,分享给大家。 如果你也对 AI Agents、产品创新感兴趣,欢…

地图 APP 和购物 APP 是最急切上 AI的地方

现在 AI 这么火,我觉得目前最急切的是把地图 APP 赶快做成 AI 的。连我们 TDengine 数据库也在推出 AI 产品,这些传统生活服务 APP ,更应该马上使用 AI , 提供更智能的服务。 这两天球拍线断了,想在附近找一家拉线的,我…

C++——AVL平衡树

我们之前已经讲解过了二叉搜索树了。知道它的基本特性后,这次,我们来认识一下AVL平衡树。 在此之前,我们先来想想目前为止,我们可以通过哪些方式来寻找一个数?(有些我们还没有学,大概率后面会更…

Blaster - Multiplayer P127-PXXX: 多种武器

P129_ Rocket Projectiles P129_1 创建火箭 配置请自行查看. P129_2 创建火箭发射器 配置请自行查看. P129_3 初始化弹药 这里添加了一个新的武器类型. P129_4 禁止重复添加 CharacerOverlay P130_ Rocket Trails 本节制作了一个奶瓜特效. P131_Spawn Rocket Trails 如果…

基于SpringBoot运动会管理系统设计和实现(源码+文档+部署讲解)

技术范围:SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…

无畏契约 directx runtime修复

无畏契约 directx runtime修复 问题如下 解决办法

端午经济成为消费活力新引擎 民俗体验带动文旅热潮

端午节作为中国首个入选世界非物质文化遗产的传统节日,在今年展现出了多元的文旅消费方式。人们不仅在河湖边观看龙舟竞渡,还在古镇体验民俗技艺、参观文博场馆,享受艺术之美。这些活动不仅展现了中华文化的独特魅力,还成为拉动消费市场的新动力。今年端午假期,“民俗体验…

48岁妻子产子丈夫称孙子比儿子大3岁 28岁女儿喜迎弟弟

6月2日,广东河源一名48岁的再婚女子在怀孕后仅用15分钟就顺利产下孩子。她的28岁女儿对此表示非常高兴,并发文说:“从此多一个人为妈妈保驾护航了。”女子的丈夫提到,他们的孙子比儿子还要大3岁。据此前报道,这名女子发现自己怀孕时已经怀胎7个月,她之前一直以为自己是“…

印尼球员费迪南:目标是连胜中日全取6分,力争直接出线 豪言壮志冲击出线

印尼本土边锋费迪南表示,对于即将到来的18强赛最后两轮比赛,印尼队的目标是连胜中国和日本,全取6分。5月30日是印尼足协主席托希尔的55岁生日,他当时正在巴厘岛参加U23东南亚杯的小组抽签仪式,并与印尼队共进晚餐,庆祝自己和归化队长伊泽斯的生日。托希尔和伊泽斯收到的生…

俄州长宣布奖励向乌无人机投石民众 平民勇阻袭击获赞

俄罗斯伊尔库茨克州州长科布泽夫表示,向乌克兰无人机投掷石块的几名当地男子将获得奖励。此前社交媒体上流传的一段视频显示,几名俄罗斯男子爬上搭载乌克兰无人机的卡车车顶,试图阻止无人机起飞对俄境内发动袭击。俄罗斯国防部通报称,乌克兰当局使用FPV无人机对摩尔曼斯克州…

24岁大学生暗网贩毒3年捞钱超7亿 台大学霸落网

24岁大学生暗网贩毒3年捞钱超7亿 台大学霸落网。据环球网援引台媒报道,近日,美国联邦调查局(FBI)破获暗网毒品交易平台“隐身市场”,而该平台经营者“法老”的真实身份竟是24岁台湾大学资管系学生林睿庠。林睿庠因贩毒资产暴增,3年多其不法所得超过1亿美元(约7.2亿元人民…

嫂子还得多练!塞鸟妻子高铁上感慨中文太难:我不知道“手”中文 高铁学中文挑战大

嫂子还得多练!塞鸟妻子高铁上感慨中文太难:我不知道“手”中文 高铁学中文挑战大!塞尔吉尼奥的妻子社媒晒出在高铁上学中文的照片,并用中葡双语配文:这要求也太高了,我不知道“手”(说)中文责任编辑:0882

【C#】Quartz.NET怎么动态调用方法,并且根据指定时间周期执行,动态配置类何方法以及Cron表达式,有请DeepSeek

🌹欢迎来到《小5讲堂》🌹 🌹这是《C#》系列文章,每篇文章将以博主理解的角度展开讲解。🌹 🌹温馨提示:博主能力有限,理解水平有限,若有不对之处望指正!&#…

58岁女子山里失联6天5夜奇迹生还 救援队地毯式搜寻成功救出

6月1日,温州市黑马救援队发现一名失联6天5夜的老人。当时老人尚有生命体征,已被送往医院进一步救治。这名58岁的老人于5月27日在温州市鹿城区仰义教堂附近的山中失联,家属和救援队多日寻找未果。6月1日上午,黑马救援队接到鹿城区公安分局指挥中心指令后,组织了40余人,携带…

20250602在Ubuntu20.04.6下修改压缩包的日期和时间

rootrootrootroot-X99-Turbo:~$ ll -rwxrwxrwx 1 rootroot rootroot 36247187308 5月 23 10:23 Android13.0地面站.tgz* rootrootrootroot-X99-Turbo:~$ touch 1Android13.0地面站.tgz rootrootrootroot-X99-Turbo:~$ ll -rwxrwxrwx 1 rootroot rootroot 36247187308 6月…

金价再冲上3300美元 贸易局势推动

5月美股表现亮眼,标普500指数上涨6.15%,纳斯达克指数上涨9.56%,道琼斯指数上涨3.94%。尽管市场波动加剧,标普500和纳斯达克仍创下自2023年11月以来最大月度涨幅。特朗普宣布从6月4日起将进口钢铁关税提高至50%。这一消息导致现货黄金周一跳空高开,重新站上3300美元关口。亚…

graphviz, dot,python批量生成,示例1-10

0)运行python脚本 import osdir1 "dot1" allfiles os.listdir(dir1) for i in range(len(allfiles)):ff dir1"\\"allfiles[i]file_name, file_extension os.path.splitext(ff)if not ".txt" file_extension.lower():continuecm…