赞
踩
❝原文链接:https://juejin.cn/post/7303798788719493157
WebAssembly (缩写为 Wasm) 是一种开放标准的二进制指令集,用于在 Web 浏览器中执行高性能的跨平台代码。它旨在成为一种通用的虚拟机,可以在各种环境中运行,不仅限于 Web 浏览器。WebAssembly 最初是为了提高 Web 应用程序的性能而设计的,但它已经扩展到其他领域,例如服务器端应用程序、嵌入式系统和桌面应用程序。本文主要介绍如何快速入门 Wasm。
借助一张网图大致了解下 Wasm,可以看到大部分主流语言都是能编译成 Wasm,然后借助 Wasm 的虚拟机 (运行环境) 在 x86 或 ARM 架构的系统上运行。
下图下介绍 Wasm 能做什么以及 envoy filter 调用 Wasm 的时机:
图上方:可以看到过滤器的介入是在 Wasm 的 OnStart
,OnConfigure
,OnHeaders
等 hook 中介入;
图左侧:在 Wasm 编码中可以调用的 rpc 接口,当然不同的语言对于 rpc 的代码兼容也是有待完善,后面会讲述到这个内容;
图右侧:wasm 提供的内置状态 API 及打印日志的方法;
图下方:wasm 提供的获取请求头,请求内容,及设置返回内容的方法;
图中间:wasm 默认使用的沙箱环境 chrome v8 引擎执行 Wasm 的代码。
wasmer
提供基于 WebAssembly 的超轻量级容器,其可以在任何地方运行:从桌面到云、以及 IoT 设备,并且也能嵌入到任何编程语言中 (它是 RUST 写的,所以对 Rust 支持是最好的,没有之一)。
wasmer
提供自动安装方式和手动方式安装,自动方式参考线上文档即可自动安装[1]。
由于我们使用 golang 开发 Wasm 插件,所以还需要一个 tinygo
编译器将 golang 变异成 Wasm。
下载安装包:
- $ cd /usr/local/opt
- $ wget https://github.com/wasmerio/wasmer/releases/download/v3.3.0/wasmer-darwin-arm64.tar.gz
安装:
- $ tar -xvf wasmer-darwin-arm64.tar.gz
- $ mv wasmer-darwin-arm64 wasmer
- $ echo 'export PATH=/usr/local/opt/wasmer/bin:$PATH' >> ~/.bash_profile
- $ source !$
检查版本号:
$ wasmer --version
下载安装包:
- $ cd /usr/local/opt
- $ wget https://github.com/tinygo-org/tinygo/releases
安装:
- $ mv tinygo /usr/local/opt/tinygo/
- $ echo 'export PATH=/usr/local/opt/tinygo/bin:$PATH' >> ~/.bash_profile
- $ echo 'export TINYGOROOT=/usr/local/opt/tinygo' >> ~/.bash_profile
- $ source !$
验证:
- $ tinygo version
- tinygo version 0.27.0 darwin/amd64 (using go version go1.19.4 and LLVM version 15.0.0)
Binaryen
是用 C + + 编写的 WebAssembly 编译器和工具链基础结构库。它的目标是使 WebAssembly 的编译变得简单、快速和有效:
下载:
$ wget https://github.com/WebAssembly/binaryen/releases/
安装:
- $ cp -av binaryen /usr/local/opt/
- $ echo 'export PATH=/usr/local/opt/binaryen/bin:$PATH' >> ~/.bash_profile
main.go
- package main
-
- import "fmt"
-
- func main() {
- fmt.Println("hello wasm.")
-
- }
build Wasm
$ tinygo build -target=wasi -o main.wasm main.go
executing in local envrioment
$ wasmer main.wasm
proxy-wasm-go-sdk ` 是一个遵循 ABI 规范的 WebAssembly 开发工具,专为 L4/L7 代理而设计。该工具依赖于 Envoy 和 TinyGo。具体而言,它是为 Golang 开发的 Envoy WASM 插件提供支持的工具。
安装 golang 库:
$ go get github.com/tetratelabs/proxy-wasm-go-sdk@v0.18.0
❝代码量不是特别多就不上传 github 了,也不想水多几篇就一篇写完了,懒~~~;
❝当前目录为 filter1
整体的代码构建类似于代码框架的周期 hook,下面是代码目录:
- |____filter1
- | |____httpcontext.go
- | |____pluginctx.go
- | |____vm.go
- |____main.go
httpcontext.go 关键函数 OnHttpRequestHeaders
和 OnHttpResponseHeaders
,代码中给出 user 参数如果不等于 shadow 就会暂停往后端传递请求。
- package filter1
-
- import (
- "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
- "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
- "net/url"
- )
-
- const (
- // 注意 proxywasm 获取 请求路径的方式
- HttpPath = ":path"
- )
-
- type MyHttpContext struct {
- types.DefaultHttpContext
- }
-
- func NewMyHttpContext() *MyHttpContext {
- return &MyHttpContext{}
- }
-
- func (this *MyHttpContext) OnHttpRequestHeaders(int, bool) types.Action {
- // 通过 header 获取request path
- hp, err := proxywasm.GetHttpRequestHeader(HttpPath)
- if err != nil {
- proxywasm.LogErrorf("get http path error: %s", err.Error())
- }
-
- proxywasm.LogInfof("request path = %s", hp)
-
- urlParser, err := url.Parse(hp)
- if err != nil {
- proxywasm.LogError(err.Error())
-
- }
- proxywasm.LogInfof("host = %s", urlParser.Host)
- proxywasm.LogInfof("uri = %s", urlParser.Path)
- proxywasm.LogInfof("params = %s", urlParser.RawQuery)
-
- // send response
- if user := urlParser.Query().Get("user"); user != "shadow" {
- _ = proxywasm.SendHttpResponse(401,
- [][2]string{
- {"content-type", "application/json; charset=utf-8"},
- },
- []byte("用户没有权限或缺少参数"),
- -1)
- // 表示不可继续
- return types.ActionPause
- }
-
- //表示正常action
- return types.ActionContinue
- }
-
-
-
- func (this *MyHttpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
- err := proxywasm.AddHttpResponseHeader("hello", "world")
- if err != nil {
- proxywasm.LogErrorf("add header error: %s", err.Error())
- }
- return types.ActionContinue
- }
pluginctx.go OnPluginStart
- package filter1
-
- import (
- "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
- "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
- )
-
- var (
- pluginStartCnt = 0
- )
-
- type HttpPluginContext struct {
- types.DefaultPluginContext
- }
-
- func NewHttpPluginContext() *HttpPluginContext {
- return &HttpPluginContext{}
- }
-
- func (this *HttpPluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
- pluginStartCnt++
- proxywasm.LogInfof("pluginStartCnt: %d", pluginStartCnt)
- return types.OnPluginStartStatusOK
- }
-
- func (this *HttpPluginContext) NewHttpContext(contextID uint32) types.HttpContext {
- return NewMyHttpContext()
- }
vm.go OnVMStart
- package filter1
-
- import (
- "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
- "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
- )
-
-
- type MyVM struct {
- types.DefaultVMContext
- }
-
- func NewMyVM() *MyVM {
- return &MyVM{}
- }
-
- func (this *MyVM) OnVMStart(vmConfigurationSize int) types.OnVMStartStatus {
- proxywasm.LogInfo("vm start filter 1")
-
- return types.OnVMStartStatusOK
- }
-
- func (this *MyVM) NewPluginContext(contextID uint32) types.PluginContext {
- return NewHttpPluginContext()
- }
main.go 到现在为止代码都没什么难度,就没有解析代码,简单来说就有点像框架的生命周期。
- package main
-
- import (
- "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
- "study-wasm/2_wasm/filter1"
- )
-
- func main() {
- proxywasm.SetVMContext(filter1.NewMyVM())
- }
把以上代码编译成 Wasm:
- $ cd study-wasm/2_wasm
- $ tinygo build -target=wasi -o myfilter1.wasm study-wasm/2_wasm/main.go
接下来需要启动 envoy,我们将会使用 docker 启动 envoy,在此之前需要先配置 envoy.yaml,主要留意 http_filters wasm 的配置即可。
- admin:
- address:
- socket_address: { address: 0.0.0.0, port_value: 9901 }
- static_resources:
- listeners:
- - name: listener_0
- address:
- socket_address: { address: 0.0.0.0, port_value: 8080 }
- listener_filters:
- - name: "envoy.filters.listener.http_inspector"
- filter_chains:
- - filters:
- - name: envoy.filters.network.http_connection_manager
- typed_config:
- "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
- stat_prefix: ingress_http
- codec_type: AUTO
- route_config:
- name: shadow-route
- virtual_hosts:
- - name: myhost
- domains: ["*"]
- routes:
- - match: {prefix: "/"}
- route:
- cluster: shadow_cluster_config
- http_filters:
- - name: envoy.filters.http.wasm
- typed_config:
- "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
- config:
- name: "study-wasm"
- # 这个root_id 随意就好
- root_id: "test-filter"
- vm_config:
- runtime: "envoy.wasm.runtime.v8"
- # vm_id 可以用来共享 vm 后面会说到
- vm_id: "f1"
- # 代码方式用本地挂载, 如果是生产环境可以配置 http url 的方式, 请自行查阅 envoy 文档
- code:
- local:
- filename: "/filters/wasm/myfilter1.wasm"
- - name: envoy.filters.http.router
- clusters:
- # 上游配置的是 nginx 服务器
- - name: shadow_cluster_config
- connect_timeout: 1s
- type: Static
- dns_lookup_family: V4_ONLY
- lb_policy: ROUND_ROBIN
- load_assignment:
- cluster_name: shadow_cluster
- endpoints:
- - lb_endpoints:
- - endpoint:
- address:
- socket_address:
- address: 172.17.0.5
- port_value: 80
使用 docker 启动 envoy,envoy 配置需要放置在 /opt/envoy/envoy.yaml
,编译好的 Wasm 文件需要放置在 /opt/envoy/filters/wasm
目录下。
- $ docker run --name=envoy -d \
- -p 9901:9901 \
- -p 8080:8080 \
- -v /opt/envoy/envoy.yaml:/etc/envoy/envoy.yaml \
- -v /opt/envoy/filters/wasm:/filters/wasm \
- envoyproxy/envoy-alpine:v1.21.0
启动后验证,不带参数访问会产生 401 错误,带正确参数访问成功获取数据。到此我们已经简单的实现了 Wasm 的拦截请求功能,后续我们在上面代码的基础上进行部分修改以演示 Wasm 支持的不同功能。
- $ curl http://127.0.0.1:8080
- 用户没有权限或缺少参数
- $ curl http://127.0.0.1:8080?user=shadow
- v1
这次演示的是 Wasm 如何读取配置,方式有很多下面演示如何读取 envoy 中配置,下面只放出变化部分的配置或代码。
envoy.yaml 主要配置了 configuration
- ...
- ...
- http_filters:
- - name: envoy.filters.http.wasm
- typed_config:
- "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
- config:
- name: "study-wasm"
- root_id: "test-filter"
- configuration:
- "@type": type.googleapis.com/google.protobuf.StringValue
- value: |
- {
- "welcome_content": "欢迎登陆 xxx.com"
- }
- vm_config:
- runtime: "envoy.wasm.runtime.v8"
- vm_id: "f1"
- code:
- local:
- filename: "/filters/wasm/myfilter1.wasm"
- - name: envoy.filters.http.router
- ...
- ...
增加获取配置代码,以下是在 pluginctx.go OnPluginStart
:
- func (this *HttpPluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
- // 获取 plugin 传递的 config, 对于配置插件 type.googleapis.com/google.protobuf.StringValue
- cfg, err := proxywasm.GetPluginConfiguration()
- if err != nil {
- proxywasm.LogErrorf("get plugin config error: %s", err.Error())
- }
- pluginStartCnt++
- proxywasm.LogInfof("pluginStartCnt: %d", pluginStartCnt)
- proxywasm.LogInfof("get plugin config: %s", string(cfg))
- }
需要重新编译 Wasm 插件及重启 envoy (重要)。
- # 查看envoy 日志是否打印 插件配置
- $ docker logs -f envoy
下图 (左) 可以看出 Wasm vm 并不运行在主线程上,所以它并不会阻碍主线程的运行;下图 (右) 可以看出多个 Wasm 服务运行在同一个 Wasm 虚拟机中,并不一定需要每个 Wasm 启动一个虚拟机。
从上图可以看出,主要标记相同的 vm_id
可以共享虚拟机。下面提供了 envoy.yaml
需要修改的部分。在配置中创建了两个 Wasm 插件,一个是 filter1
,另一个是 filter2
,而这两个插件配置的 vm_id
是相同的。
- ...
- ...
- http_filters:
- - name: envoy.filters.http.wasm
- typed_config:
- "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
- config:
- name: "study-wasm-1"
- root_id: "test-filter-1"
- vm_config:
- runtime: "envoy.wasm.runtime.v8"
- # 需要指定 vm_id
- vm_id: "f1"
- code:
- local:
- filename: "/filters/wasm/myfilter1.wasm"
- - name: envoy.filters.http.wasm
- typed_config:
- "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
- config:
- name: "study-wasm-2"
- root_id: "test-filter-2"
- vm_config:
- runtime: "envoy.wasm.runtime.v8"
- # 需要指定 vm_id
- vm_id: "f1"
- code:
- local:
- filename: "/filters/wasm/myfilter2.wasm"
- - name: envoy.filters.http.router
-
- ...
- ...
上述描述阐述了多个 Wasm 插件共享同一个虚拟机 (VM) 的主要目的。这种共享虚拟机的设计旨在实现资源的更有效利用,而其中最为重要的优势之一是能够共享存储。
延用 3.3 的 envoy 配置,我们将在插件 filter1
中存储数据,然后在 filter2
中获取数据;(filter1 和 filter2 是两套代码)
在 filter1
启动时设置共享数据,vm.go OnVMStart
:
- func (this *MyVM) OnVMStart(vmConfigurationSize int) types.OnVMStartStatus {
- proxywasm.LogInfo("vm start filter 1")
- // cas 是一个保证线程安全的值, 它会由 share-data 内部维护
- if err := proxywasm.SetSharedData("my_name", []byte("shadow"), 1); err != nil && err != types.ErrorStatusCasMismatch {
-
- proxywasm.LogErrorf("on vm start error: %s", err.Error())
-
- }
-
- return types.OnVMStartStatusOK
- }
在 filter2
将在返回响应时从共享存储中获取数据并返回到客户端。
- func (this *MyHttpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
- // 如果需要重新设置 sharedata 则需要重新传入 cas, 让它单调递增
- //v, cas, err := proxywasm.GetSharedData("my_name")
- // 跨 vm 获取share-data
- v, _, err := proxywasm.GetSharedData("my_name")
- if err != nil {
- proxywasm.LogError(err.Error())
- return types.ActionContinue
-
- }
- if err := proxywasm.AddHttpResponseHeader("my_name", string(v)); err != nil {
- proxywasm.LogError(err.Error())
- }
- return types.ActionContinue
- }
重新编译 2 个 Wasm 插件及替换 envoy.yaml 并重启 envoy,访问代理。
- # 可以看到 header 中存在 my_name: shadow
- $ curl -v http://127.0.0.1:8080?user=shadow
由于我们实际是通过 tinygo 进行代码编译,而 tinygo 仅支持 golang 的 net 包,而不支持 net/http 包,如果我们使用 net 包就构建 http 那就会比较复杂了,而且会有很多错误;所以我们将使用之前提到过的内置请求函数。
查看 tinygo 支持的包:
❝https://tinygo.org/docs/reference/lang-support/stdlib/
在通常情况下,我们了解到整个请求过程需要极低的延迟,而请求本身是一个网络的 IO。因此,SDK 为我们提供了一个异步的 RPC 请求方法,并且默认情况下不主动等待返回结果。
虽然 SDK 为我们提供了内置的 RPC 请求方式,但是并不允许我们直接访问外部 IP,而是只是放我们配置的上游服务,所以我们需要在 envoy 中增加一个上游服务。
- clusters:
- ....
- ....
- - name: shadow_cluster_v2
- connect_timeout: 1s
- type: Static
- dns_lookup_family: V4_ONLY
- lb_policy: ROUND_ROBIN
- load_assignment:
- cluster_name: shadow_cluster_v2
- endpoints:
- - lb_endpoints:
- - endpoint:
- address:
- socket_address:
- address: 172.17.0.7
- port_value: 80
httpcontext.go OnHttpRequestHeaders
在请求到来时,访问 shadow_cluster_v2,留意下代码注释的细节。
- func (this *MyHttpContext) OnHttpRequestHeaders(int, bool) types.Action {
- headers := [][2]string{
- {":method", "GET"},
- {":path", "/"},
- // 这里由于没有域名解析所以使用地址
- //{":authority", "172.17.0.7"},
- //{"Host", "172.17.0.7"},
- {"Host", "shadow_cluster_v2"},
- {"accept", "*/*"},
- {":scheme", "http"},
- }
-
- // 由于 golang 不支持 net/http 包在 Wasm 中使用, 所以这里使用 Wasm 的http call, 所以需要在配置中设置上游地址
- _, callerr := proxywasm.DispatchHttpCall("shadow_cluster_v2", headers,
- nil, nil, 1000, func(numHeaders, bodySize, numTrailers int) {
- b, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
- if err != nil {
- proxywasm.LogError("http 调用出错" + err.Error())
- } else {
- proxywasm.LogInfo("得到http请求内容: " + string(b))
- }
- })
- if callerr != nil {
- proxywasm.LogError(callerr.Error())
- }
-
- return types.ActionContinue
- }
重新编译 Wasm 插件及替换 envoy.yaml 并重启 envoy,通过终端访问代理后查看 envoy 的日志中是否打印 http 请求的内容。
在某些情境下,我们可能需要进行同步等待请求返回,比如在权限验证等情况下。同步请求意味着我们需要主动暂停主流程,等待请求返回后再恢复主流程。
httpcontext.go OnHttpRequestHeaders
,原来的 types.ActionContinue
需要改成 types.ActionPause
以暂停主流程。如果出发需要直接返回终端请求,否则恢复请求。
- func (this *MyHttpContext) OnHttpRequestHeaders(int, bool) types.Action {
- // 在默认情况下 DispatchHttpCall 是异步请求,主线程不会等待我们完成就进行下一步操作;
- // 现在我们需要通过主线程Pause不再传递请求,直到我们完成并执行恢复函数;
- headers := [][2]string{
- {":method", "GET"},
- {":path", "/"},
- // 这里由于没有域名解析所以使用地址
- //{":authority", "172.17.0.7"},
- //{"Host", "172.17.0.7"},
- {"Host", "shadow_cluster_v2"},
- {"accept", "*/*"},
- {":scheme", "http"},
- }
-
- _, callerr := proxywasm.DispatchHttpCall("shadow_cluster_v2", headers,
- nil, nil, 1000, func(numHeaders, bodySize, numTrailers int) {
- b, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
- if err != nil {
- proxywasm.LogError("http 调用出错" + err.Error())
- // 调用出错
- _ = proxywasm.SendHttpResponse(500, [][2]string{
- {"content-type", "application/json; charset=utf-8"},
- }, []byte(fmt.Sprint("call shadow_cluster_v2 error: %s", err.Error())), -1)
- } else {
- proxywasm.LogInfo("得到http请求内容: " + string(b))
- // 恢复请求
- if err := proxywasm.ResumeHttpRequest(); err != nil {
- proxywasm.LogErrorf("恢复请求错误, err:%s", err.Error())
- }
- }
- })
- if callerr != nil {
- proxywasm.LogError(callerr.Error())
- }
-
- return types.ActionPause
-
- //return types.ActionContinue
- }
重新编译 Wasm 插件及替换 envoy.yaml 并重启 envoy,通过终端访问代理后查看 envoy 的日志中是否打印 http 请求的内容。
Wasm 的基本入门编码方式就到这结束了,我们在生产上可以用作 istio gateway 的分流 (通过判断 header)、用户认证等场景。
[1]
自动安装: https://github.com/wasmerio/wasmer-install
加入 Sealos 开源社区
体验像个人电脑一样简单的云操作系统
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。