❝原文链接: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。
安装 Wasmer
下载安装包:
$ 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
安装 tinygo
下载安装包:
$ 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
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 了,也不想水多几篇就一篇写完了,懒~~~;
简单构建 Wasm
❝当前目录为 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 读取配置
这次演示的是 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 插件共享虚拟机
下图 (左) 可以看出 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 插件共共享存储
上述描述阐述了多个 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 开源社区
体验像个人电脑一样简单的云操作系统
🏠官网链接
https://sealos.run
🐙GitHub 地址
https://github.com/labring/sealos
📑访问 Sealos 文档
https://sealos.run/docs/Intro
🏘️逛逛论坛
https://forum.laf.run/
往期推荐
脸贴脸教大家使用 Sealos 一键部署 Kubernetes 集群,老奶奶都会
关于 Sealos
Sealos 是一款以 Kubernetes 为内核的云操作系统发行版。它以云原生的方式,抛弃了传统的云计算架构,转向以 Kubernetes 为云内核的新架构,使企业能够像使用个人电脑一样简单地使用云。
关注 Sealos 公众号与我们一同成长👇👇👇