[K8S] Envoy wasm插件的使用(4)

0. 前言

  • WebAssembly (缩写为Wasm)是一种开放标准的二进制指令集,用于在Web浏览器中执行高性能的跨平台代码。它旨在成为一种通用的虚拟机,可以在各种环境中运行,不仅限于Web浏览器。WebAssembly 最初是为了提高Web应用程序的性能而设计的,但它已经扩展到其他领域,例如服务器端应用程序、嵌入式系统和桌面应用程序。本文主要介绍如何快速入门 wasm

1. 介绍

  • 借助一张网图大致了解下 wasm, 可以看到大部分主流语言都是能编译成 wasm, 然后借助 wasm 的虚拟机 (运行环境) 在 x86 或 ARM 架构的系统上运行;

  • 下图下介绍 wasm 能做什么以及 envoy filter 调用 wasm 的时机;

    • 图上方: 可以看到过滤器的介入是在 wasm 的 OnStart, OnConfigure, OnHeaders 等 hook 中介入;
    • 图左侧: 在 wasm 编码中可以调用的 rpc 接口, 当然不同的语言对于 rpc 的代码兼容也是有待完善, 后面会讲述到这个内容;
    • 图右侧: wasm 提供的内置状态API 及 打印日志的方法;
    • 图下方: wasm 提供的获取请求头, 请求内容, 及设置返回内容的方法;
    • 图中间: wasm 默认使用的沙箱环境 chrome v8引擎执行 wasm 的代码;

2. 学会安装服务器服务器

  • wasmer 提供基于 WebAssembly 的超轻量级容器,其可以在任何地方运行:从桌面到云、以及 IoT 设备,并且也能嵌入到任何编程语言 中 (它是RUST写的,所以对Rust支持是最好的,没有之一);

  • wasmer 提供自动安装方式和手动方式安装, 自动方式参考线上文档即可 自动安装

  • 由于我们使用golang开发wasm 插件, 所以还需要一个 tinygo 编译器将 golang 变异成 wasm.

2.1 安装 wasmer

  • 下载安装包

    bash 复制代码
    cd /usr/local/opt
    wget https://github.com/wasmerio/wasmer/releases/download/v3.3.0/wasmer-darwin-arm64.tar.gz
  • 安装

    bash 复制代码
    tar -xvf wasmer-darwin-arm64.tar.gz
    mv wasmer-darwin-arm64 wasmer
    echo 'export PATH=/usr/local/opt/wasmer/bin:$PATH' >> ~/.bash_profile
    source !$
  • 检查版本号

    bash 复制代码
    wasmer --version

2.2 安装 tinygo

  • 下载安装包

    bash 复制代码
    cd /usr/local/opt
    wget https://github.com/tinygo-org/tinygo/releases
  • 安装

    bash 复制代码
    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 !$
  • 验证

    bash 复制代码
    tinygo version
    # tinygo version 0.27.0 darwin/amd64 (using go version go1.19.4 and LLVM version 15.0.0)

2.3 安装优化工具链

Binaryen 是用 C + + 编写的 WebAssembly 编译器和工具链基础结构库。它的目标是使 WebAssembly 的编译变得简单、快速和有效:

  • 下载

    bash 复制代码
    wget https://github.com/WebAssembly/binaryen/releases/
  • 安装

    bash 复制代码
    cp -av binaryen /usr/local/opt/
    echo 'export PATH=/usr/local/opt/binaryen/bin:$PATH' >> ~/.bash_profile

2.4 测试编译与运行

  • main.go

    golang 复制代码
    package main
    
    import "fmt"
    
    func main()  {
            fmt.Println("hello wasm.")
    
    }
  • build wasm

    bash 复制代码
    tinygo build  -target=wasi  -o main.wasm main.go 
  • executing in local envrioment

    bash 复制代码
    wasmer main.wasm

2.5 proxy-wasm-go-sdk

  • proxy-wasm-go-sdk 是一个遵循 ABI 规范的 WebAssembly 开发工具,专为 L4/L7 代理而设计。该工具依赖于 Envoy 和 TinyGo。具体而言,它是为 Golang 开发的 Envoy WASM 插件提供支持的工具。

  • 安装 golang 库

    bash 复制代码
    go get github.com/tetratelabs/proxy-wasm-go-sdk@v0.18.0

3. 入门实战

代码量不是特别多就不上传 github 了, 也不想水多几篇就一篇写完了, 懒~~~;

3.1 简单构建 wasm

当前目录为 filter1

  • 整体的代码构建类似于代码框架的周期 hook, 下面是代码目录;

    shell 复制代码
    |____filter1
    | |____httpcontext.go
    | |____pluginctx.go
    | |____vm.go
    |____main.go
  • httpcontext.go 关键函数 OnHttpRequestHeadersOnHttpResponseHeaders, 代码中给出 user 参数如果不等于 shadow 就会暂停往后端传递请求;

    golang 复制代码
    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

    golang 复制代码
    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

    golang 复制代码
    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 到现在为止代码都没什么难度, 就没有解析代码, 简单来说就有点像框架的生命周期.

    golang 复制代码
    package main
    
    import (
            "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
            "study-wasm/2_wasm/filter1"
    )
    
    func main() {
            proxywasm.SetVMContext(filter1.NewMyVM())
    }
  • 把以上代码编译成 wasm

    bash 复制代码
    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 的配置即可

    yaml 复制代码
    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 目录下

    shell 复制代码
    # 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 支持的不同功能;

    shell 复制代码
    # curl http://127.0.0.1:8080
    用户没有权限或缺少参数
    # curl http://127.0.0.1:8080?user=shadow
    v1

3.2 wasm 读取配置

  • 这次演示的是 wasm 如何读取配置, 方式有很多下面演示如何读取envoy中配置, 下面只放出变化部分的配置或代码;

  • envoy.yaml 主要配置了 configuration

    yaml 复制代码
    ...
    ...
                    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

    golang 复制代码
    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 (重要)

    shell 复制代码
    // 查看envoy 日志是否打印 插件配置
    # docker logs -f envoy

3.3 多个 wasm 插件共享虚拟机

  • 下图(左) 可以看出 wasm vm 并不运行在主线程上, 所以它并不会阻碍主线程的运行; 下图(右) 可以看出多个 wasm 服务运行在同一个 wasm 虚拟机中, 并不一定需要每个wasm启动一个虚拟机;

  • 从上图可以看出,主要标记相同的 vm_id 可以共享虚拟机。下面提供了 envoy.yaml 需要修改的部分。在配置中创建了两个 wasm 插件,一个是 filter1,另一个是 filter2,而这两个插件配置的 vm_id 是相同的。

    yaml 复制代码
    ...
    ...
                    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
    
    ...
    ...

3.4 多个 wasm 插件共共享存储

  • 上述描述阐述了多个 Wasm 插件共享同一个虚拟机(VM)的主要目的。这种共享虚拟机的设计旨在实现资源的更有效利用,而其中最为重要的优势之一是能够共享存储。

  • 延用 3.3 的envoy 配置, 我们将在插件 filter1 中存储数据, 然后在 filter2 中获取数据; (filter1 和 filter2 是两套代码)

  • filter1 启动时设置共享数据, vm.go OnVMStart

    golang 复制代码
    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 将在返回响应时从共享存储中获取数据并返回到客户端;

    golang 复制代码
    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, 访问代理

    shell 复制代码
    // 可以看到 header 中存在 my_name: shadow
    # curl -v http://127.0.0.1:8080?user=shadow

3.5 对外请求

  • 由于我们实际是通过 tinygo 进行代码编译, 而 tinygo 仅支持 golang 的 net 包, 而不支持 net/http 包, 如果我们使用 net 包就构建http那就会比较复杂了, 而且会有很多错误; 所以我们将使用之前提到过的内置请求函数;

  • 查看 tinygo 支持的包: tinygo.org/docs/refere...

3.5.1 编写异步请求

  • 在通常情况下,我们了解到整个请求过程需要极低的延迟, 而请求本身是一个网络的IO。因此,SDK为我们提供了一个异步的RPC请求方法,并且默认情况下不主动等待返回结果。

  • 虽然 SDK 为我们提供了内置的 RPC 请求方式, 但是并不允许我们直接访问外部 IP, 而是只是放我们配置的上游服务, 所以我们需要在 envoy 中增加一个上游服务.

    yaml 复制代码
        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, 留意下代码注释的细节

    golang 复制代码
    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 请求的内容;

3.5.2 编写同步请求

  • 在某些情境下,我们可能需要进行同步等待请求返回,比如在权限验证等情况下。同步请求意味着我们需要主动暂停主流程,等待请求返回后再恢复主流程。

  • httpcontext.go OnHttpRequestHeaders, 原来的 types.ActionContinue 需要改成 types.ActionPause 以暂停主流程. 如果出发需要直接返回终端请求, 否则恢复请求.

    golang 复制代码
    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 请求的内容;

4. 写在最后

  • Wasm 的基本入门编码方式就到这结束了, 我们在生产上可以用作 istio gateway 的分流(通过判断header)、用户认证等场景.

  • 后续我将可能更新的几个主题(下面的主题都有在工作中使用):

    • Kubernetes ApiServer代理: 通过代理, 我们可以有效建立及管理每个用户的权限, 且由于我们代理了所有kubernetes 请求易于我们合规审计, 高危操作告警等功能的开发;
    • Kubernetes 的调度: 包括调度器的入门, 以及如何编写场景调度, 跨集群调度;
    • EBPF 的使用: XDP编写, 网络数据抓取及网络链路优化, 以及一些落地场景;
  • 以上主题如果有兴趣的请在评论区留言, 我将优先开更;

  • 👍点赞 ➕关注

4.1 参考引用

相关推荐
童先生3 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
幼儿园老大*4 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
牛角上的男孩7 小时前
Istio Gateway发布服务
云原生·gateway·istio
景天科技苑9 小时前
【云原生开发】K8S多集群资源管理平台架构设计
云原生·容器·kubernetes·k8s·云原生开发·k8s管理系统
架构师那点事儿9 小时前
golang 用unsafe 无所畏惧,但使用不得到会panic
架构·go·掘金技术征文
wclass-zhengge9 小时前
K8S篇(基本介绍)
云原生·容器·kubernetes
颜淡慕潇9 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
昌sit!17 小时前
K8S node节点没有相应的pod镜像运行故障处理办法
云原生·容器·kubernetes
A ?Charis20 小时前
Gitlab-runner running on Kubernetes - hostAliases
容器·kubernetes·gitlab
北漂IT民工_程序员_ZG21 小时前
k8s集群安装(minikube)
云原生·容器·kubernetes