关于apisix:WebAssembly-助力云原生APISIX-如何借助-Wasm-插件实现扩展功能

32次阅读

共计 6167 个字符,预计需要花费 16 分钟才能阅读完成。

本文将介绍 Wasm,以及 Apache APISIX 如何实现 Wasm 性能。

作者朱欣欣,API7.ai 技术工程师

原文链接

什么是 Wasm

Wasm 是 WebAssembly 的缩写。WebAssembly/Wasm 是一个基于堆栈的虚拟机设计的指令格局。
在 Wasm 未呈现之前,浏览器中只能反对运行 Javascript 语言。当 Wasm 呈现之后,使得高级语言例如 C/C++/Golang 可能在浏览器中运行。以后,支流的浏览器包含 Chrome、Firefox、Safari 等浏览器都已实现对 Wasm 的反对。并且得益于 WASI 我的项目的推动,服务端也曾经可能反对运行 Wasm 指令。
现在在网关侧,Apache APISIX 也已实现对 Wasm 的反对,开发者能够通过高级语言 C/C++/Go/Rust 并依照 proxy-wasm 标准来实现 Wasm 插件的开发。

为什么 APISIX 要反对 Wasm 插件

相比拟原生的 Lua 插件,Wasm 插件存在如下劣势:

  • 可扩展性:APISIX 通过反对 Wasm,咱们能够联合 proxy-wasm 提供的 SDK,应用 C++/Golang/Rust 等语言进行插件开发。因为高级语言往往领有更加丰盛的生态,所以咱们能够依靠于这些生态来实现反对更多功能丰盛的插件。
  • 安全性:因为 APISIX 和 Wasm 之前的调用依靠于 proxy-wasm 提供的 ABI(二进制利用接口),这部分的拜访调用更为平安。Wasm 插件只容许对申请进行特定的批改。另外,因为 Wasm 插件运行在特定的 VM 中,所以即便插件运行呈现解体也不会影响 APISIX 主过程的运行。

APISIX 如何反对 WASM

理解完 Wasm,当初咱们将从自顶向下的角度来看 APISIX 是如何反对 Wasm 插件性能的。

APISIX Wasm 插件

在 APISIX 中,咱们能够应用高级语言 C/C++/Go/Rust 来依照 proxy-wasm 标准以及对应的 SDK 来编写插件。

proxy-wasm 是 Envoy 推出的在 L4/L7 代理之间的 ABI 的标准与规范。
在该标准中定义了蕴含内存治理、四层代理、七层代理扩大等 ABI。
例如在七层代理中,proxy-wasm 标准定义了 proxy_on_http_request_headers,proxy_on_http_request_body,proxy_on_http_request_trailers,proxy_on_http_response_headers 等 ABI,使得模块可能在各个阶段对申请内容进行获取与批改。

例如,咱们应用 Golang 联合 proxy-wasm-go-sdk, 编写如下插件:

proxy-wasm-go-sdk 正是上述 proxy-wasm 标准的 SDK,它帮忙开发者更好的应用 Golang 编写 proxy-wasm 插件。
不过须要留神的是,因为原生 Golang 在反对 WASI 时存在一些问题,因而该 SDK 基于 TinyGo 实现,更多内容能够点击进行查看。

该插件的次要性能用于将 HTTP 批改申请的响应状态码与响应体,援用自 APISIX 链接,

...
func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {data, err := proxywasm.GetPluginConfiguration()
        if err != nil {proxywasm.LogErrorf("error reading plugin configuration: %v", err)
                return types.OnPluginStartStatusFailed
        }

        var p fastjson.Parser
        v, err := p.ParseBytes(data)
        if err != nil {proxywasm.LogErrorf("error decoding plugin configuration: %v", err)
                return types.OnPluginStartStatusFailed
        }
        ctx.Body = v.GetStringBytes("body")
        ctx.HttpStatus = uint32(v.GetUint("http_status"))
        if v.Exists("percentage") {ctx.Percentage = v.GetInt("percentage")
        } else {ctx.Percentage = 100}

        // schema check
        if ctx.HttpStatus < 200 {proxywasm.LogError("bad http_status")
                return types.OnPluginStartStatusFailed
        }
        if ctx.Percentage < 0 || ctx.Percentage > 100 {proxywasm.LogError("bad percentage")
                return types.OnPluginStartStatusFailed
        }

        return types.OnPluginStartStatusOK
}

func (ctx *httpLifecycle) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
        plugin := ctx.parent
        if !sampleHit(plugin.Percentage) {return types.ActionContinue}

        err := proxywasm.SendHttpResponse(plugin.HttpStatus, nil, plugin.Body, -1)
        if err != nil {proxywasm.LogErrorf("failed to send local response: %v", err)
                return types.ActionContinue
        }
        return types.ActionPause
}
...

之后,咱们通过 tiny-go 将上述的 Golang 代码编译生成 .wasm 文件

tinygo build -o wasm_fault_injection.go.wasm -scheduler=none -target=wasi ./main.go

实现编译之后,咱们失去了 fault_injection.go.wasm 文件

如果对 wasm 文件内容感兴趣的话,咱们能够应用 wasm-tool 工具来查看该 wasm 文件的具体内容。
wasm-tools dump hello.go.wasm

wasm_fault_injection.go.wasm 配置到 APISIX 到 config.yaml,并将该插件命名为 wasm_fault_injection。

apisix:
        ...
wasm:
  plugins:
    - name: wasm_fault_injection
      priority: 7997
      file: wasm_fault_injection.go.wasm

之后,咱们启动 APISIX,并创立一条路由援用该 Wasm 插件:

curl  http://127.0.0.1:9180/apisix/admin/routes/1 \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{"uri":"/*","upstream":{"type":"roundrobin","timeout":{"connect":1,"read":1,"send":1},
        "nodes":{"httpbin.org:80":1}
    },
    "plugins":{
        "wasm_fault_injection":{"conf":"{\"http_status\":200, \"body\":\"Hello WebAssembly!\n\"}"
        }
    },
    "name":"wasm_fault_injection"
}'

进行拜访测试,发现响应体已被批改为 “Hello WebAssembly”,由此 Wasm 插件曾经失效。

curl 127.0.0.1:9080/get -v
*   Trying 127.0.0.1:9080...
* Connected to 127.0.0.1 (127.0.0.1) port 9080 (#0)
> GET /get HTTP/1.1
> Host: 127.0.0.1:9080
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 09 Feb 2023 07:46:50 GMT
< Content-Type: text/plain; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: APISIX/3.1.0
<
Hello WebAssembly!

Wasm-nginx-module

理解完 Apache APISIX 如何应用 Wasm 插件,当初咱们更进一步来理解 “ 为什么咱们能在 Wasm 插件中获取到申请的内容并批改申请?”。
因为 APISIX 选用 Openresty 作为底层框架,因而 Wasm 插件中想要可能获取到申请内容和批改申请内容,就须要和 openResty 或者 NGINX 提供的 API 进行交互。wasm-nginx-module 正是提供了这部分能力。

wasm-nginx-module 是由 API7 研发的反对 Wasm 的 NGINX 模块。
该模块尝试在 NGINX 的根底上实现 proxy-wasm-abi,并且向上封装了 Lua API,使得咱们可能在 Lua 层面实现 proxy-wasm-abi 的调用。
更多内容可参考 wasm-nginx-module。

例如,当咱们的 APISIX 运行到 “access” 阶段时,会调用 wasm-nginx-module 中提供的 Lua 办法
on_http_request_headers。

-- apisix/wasm.lua
...
local ok, err = wasm.on_http_request_headers(plugin_ctx)
   if not ok then
       core.log.error(name, ": failed to run wasm plugin:", err)
       return 503
   end
end
...

之后在该办法中,将调用 wasm-nginx-modulengx_http_wasm_on_http 办法,

ngx_int_t
ngx_http_wasm_on_http(ngx_http_wasm_plugin_ctx_t *hwp_ctx, ngx_http_request_t *r,
                      ngx_http_wasm_phase_t type, const u_char *body, size_t size,
                      int end_of_body)
{
    ...

    ctx = ngx_http_wasm_get_module_ctx(r);

    if (type == HTTP_REQUEST_HEADERS) {cb_name = &proxy_on_request_headers;} else if (type == HTTP_REQUEST_BODY) {cb_name = &proxy_on_request_body;} else if (type == HTTP_RESPONSE_HEADERS) {cb_name = &proxy_on_response_headers;} else {cb_name = &proxy_on_response_body;}

    if (type == HTTP_REQUEST_HEADERS || type == HTTP_RESPONSE_HEADERS) {if (hwp_ctx->hw_plugin->abi_version == PROXY_WASM_ABI_VER_010) {
            rc = ngx_wasm_vm->call(hwp_ctx->hw_plugin->plugin,
                                   cb_name,
                                   true, NGX_WASM_PARAM_I32_I32, http_ctx->id, 0);
        } else {
            rc = ngx_wasm_vm->call(hwp_ctx->hw_plugin->plugin,
                                   cb_name,
                                   true, NGX_WASM_PARAM_I32_I32_I32, http_ctx->id,
                                   0, 1);
        }

    } else {
        rc = ngx_wasm_vm->call(hwp_ctx->hw_plugin->plugin,
                              cb_name,
                              true, NGX_WASM_PARAM_I32_I32_I32, http_ctx->id,
                              size, end_of_body);
    }
    ...
}

wasm-nginx-module 中,咱们将依据不同的阶段,设置 cb_name,例如:HTTP_REQUEST_HEADERS 对应 proxy_on_request_headers,之后将在 ngx_wasm_vm->call 中调用 vm 中的办法也就是咱们在上文中提到的 wasm 插件 OnHttpRequestHeaders 的办法。

至此,整个 APISIX 调用 wasm 插件,运行 Golang 的调用链便梳理实现,调用链如下:

Wasm VM

Wasm VM 用于真正执行 Wasm 代码的虚拟机,在 wasm-nginx-module 中实现了对两种虚拟机 “wasmtime” 和 “wasmedge” 两种虚拟机,在 APISIX 中默认抉择应用 “wasmtime” 作为 Wasm 代码的运行虚拟机。

Wasmtime
Wasmtime 是由 bytecodealliance 开源的 WebAssembly 和 WASI 的小型高效运行时。它可能在 Web 内部运行 WebAssembly 代码,即能够用作命令行应用,也能够作为 WebAssembly 运行引擎嵌入到其余程序作为库应用。
Wasmedge
Wasmedge 是为边缘计算优化的轻量级、高性能、可扩大的 WebAssembly (Wasm) 虚拟机,可用于云原生、边缘和去中心化的利用。

在 Wasm vm 中首先通过 load 办法将 .wasm 文件加载到内存,之后咱们便能够通过 VM 的 call 办法来调用这些办法。VM 底层依靠于 WASI 的接口实现,使得 Wasm 代码不仅可能运行在浏览器端,同时也反对可能在服务端进行。

总结

通过本文咱们理解到 Wasm 是什么以及 APISIX 如何反对 Wasm 插件。APISIX 通过反对 Wasm 插件,岂但能够裁减对多语言的反对,例如通过 C++, Rust, Golang, AssemblyScript 等进行插件开发,而且因为 WebAssembly 正在从浏览器走向云原生领有了更加丰盛的生态与应用场景,因而 APISIX 也能够借助 Wasm 实现在 API 网关侧更多的扩大性能,解决更多应用场景。

对于 API7.ai 与 APISIX

API7.ai(干流科技)是一家提供 API 解决和剖析的开源根底软件公司,于 2019 年开源了新一代云原生 API 网关 — APISIX 并捐献给 Apache 软件基金会。尔后,API7.ai 始终踊跃投入反对 Apache APISIX 的开发、保护和社区经营。与千万贡献者、使用者、支持者一起做出世界级的开源我的项目,是 API7.ai 致力的指标。

正文完
 0