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