Como o Apache APISIX Habilita a Funcionalidade Wasm

Xinxin Zhu

April 7, 2023

Products

Para permitir que aplicações de alto desempenho sejam executadas em navegadores da web, o Apache APISIX suporta Wasm no nível do gateway, um formato de instrução binária que permite a execução eficiente e segura de código na web. Consequentemente, os desenvolvedores podem usar linguagens de programação de alto nível como C/C++/Go/Rust e seguir a especificação proxy-wasm para criar plugins Wasm. A especificação proxy-wasm garante compatibilidade e interoperabilidade com outros sistemas. Vamos mergulhar neste artigo e aprender mais detalhes.

O que é Wasm

WebAssembly (abreviado Wasm) é um formato de instrução binária para uma máquina virtual baseada em pilha.

Antes do surgimento do Wasm, apenas Javascript podia ser executado em navegadores da web. No entanto, com a introdução do Wasm, linguagens de alto nível como C/C++/Golang agora podem ser executadas em navegadores da web. Navegadores principais como Chrome, Firefox e Safari agora suportam Wasm. Além disso, devido aos avanços feitos pelo projeto WASI (WebAssembly System Interface), ambientes do lado do servidor também podem suportar a execução de instruções Wasm.

Atualmente, o Apache APISIX suporta Wasm no nível do gateway. Os desenvolvedores podem usar linguagens de programação de alto nível como C/C++/Go/Rust e seguir a especificação proxy-wasm para criar plugins Wasm.

wasm

Por que o APISIX suporta plugins Wasm?

Comparado aos plugins nativos em Lua, os plugins Wasm oferecem várias vantagens:

  • Escalabilidade: Ao suportar Wasm, o APISIX pode utilizar o SDK fornecido pelo proxy-wasm para desenvolver plugins em linguagens de programação de alto nível como C++, Golang e Rust. Essas linguagens geralmente têm ecossistemas mais ricos, permitindo que os desenvolvedores implementem plugins com mais funcionalidades.

  • Segurança: Como a interação entre o APISIX e o Wasm depende da Interface Binária de Aplicação (ABI) fornecida pelo proxy-wasm, esse acesso é mais seguro. Os plugins Wasm só podem fazer modificações específicas nas solicitações, garantindo que não possam realizar ações maliciosas. Além disso, como os plugins Wasm são executados em uma VM separada, mesmo que o plugin falhe, isso não afetará o processo principal do APISIX.

Como o APISIX suporta Wasm?

Agora que entendemos o que é Wasm, vamos dar uma olhada de cima para baixo em como o APISIX suporta plugins Wasm.

apisix-wasm

Plugins Wasm do APISIX

O APISIX permite que os desenvolvedores criem plugins usando linguagens de programação de alto nível populares como C/C++, Go e Rust. Esses plugins podem ser construídos usando o SDK correspondente e seguindo a especificação proxy-wasm.

proxy-wasm é uma especificação para ABIs entre proxies L4/L7, introduzida pelo Envoy. Essa especificação define ABIs incluindo gerenciamento de memória, extensões de proxy L4 e L7. Por exemplo, em HTTP(L7), a especificação proxy-wasm define ABIs como proxy_on_http_request_headers, proxy_on_http_request_body, proxy_on_http_request_trailers e proxy_on_http_response_headers, permitindo que os módulos recuperem e modifiquem o conteúdo da solicitação em várias etapas.

Por exemplo, usaremos Golang e proxy-wasm-go-sdk para desenvolver este plugin:

O proxy-wasm-go-sdk é um SDK para a especificação proxy-wasm que ajuda os desenvolvedores a criar plugins proxy-wasm mais facilmente usando Golang. No entanto, é importante observar que, devido a alguns problemas com o suporte nativo do Golang para WASI, este SDK é implementado com base no TinyGo. Para mais informações, você pode clicar aqui para ver os detalhes.

A função principal deste plugin é modificar o código de status da resposta HTTP e o corpo da resposta de uma solicitação de modificação HTTP, conforme referenciado no link do 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
}
...

Depois, usamos o TinyGo para compilar o código Golang acima e gerar um arquivo .wasm.

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

Após a compilação, obtemos o arquivo fault_injection.go.wasm.

Se você estiver interessado no conteúdo do arquivo wasm, pode usar o wasm-tool para visualizar o conteúdo específico do arquivo wasm. wasm-tools dump hello.go.wasm

Configure wasm_fault_injection.go.wasm no arquivo config.yaml do APISIX e nomeie o plugin como wasm_fault_injection.

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

Depois, iniciamos o APISIX e criamos uma rota que referencia o plugin 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"
}'

Após realizar um teste de acesso, descobrimos que o corpo da resposta foi modificado para "Hello WebAssembly", indicando que o plugin Wasm agora está em vigor.

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

Depois de aprender como o Apache APISIX usa plugins Wasm, vamos nos aprofundar em "por que podemos acessar e modificar o conteúdo das solicitações em plugins Wasm?"

Como o APISIX usa o OpenResty como estrutura subjacente, para poder acessar e modificar o conteúdo das solicitações em plugins Wasm, precisamos interagir com APIs fornecidas pelo OpenResty ou NGINX. Isso é exatamente o que o wasm-nginx-module faz.

wasm-nginx-module é um módulo NGINX que suporta Wasm e foi desenvolvido pela API7. Este módulo tenta implementar proxy-wasm-abi com base no NGINX e encapsula a API Lua, permitindo que completemos chamadas proxy-wasm-abi no nível Lua. Para mais informações, consulte wasm-nginx-module.

Por exemplo, quando o APISIX executa a fase access, ele chama o método Lua on_http_request_headers fornecido pelo wasm-nginx-module.

-- 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
...

Mais tarde, neste método, o método ngx_http_wasm_on_http fornecido pelo wasm-nginx-module será chamado.

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);
    }
    ...
}

No wasm-nginx-module, definimos o cb_name com base em diferentes estágios, como HTTP_REQUEST_HEADERS corresponde a proxy_on_request_headers, e então chamamos o método na VM através de ngx_wasm_vm->call, que é o método OnHttpRequestHeaders do plugin wasm mencionado anteriormente neste artigo.

Com isso, toda a cadeia de chamadas do APISIX chamando o plugin wasm e executando Golang é concluída. A cadeia de chamadas é a seguinte:

Wasm VM

Wasm VM é uma máquina virtual usada para executar código Wasm. O wasm-nginx-module implementa dois tipos de máquinas virtuais, "wasmtime" e "wasmedge", para executar código Wasm. No APISIX, "wasmtime" é o padrão para executar código Wasm.

Wasmtime é um runtime rápido e seguro para WebAssembly e WASI, de código aberto pela Bytecode Alliance. Ele pode executar código WebAssembly fora do ambiente da web e pode ser usado como uma ferramenta de linha de comando ou incorporado como um motor de runtime WebAssembly em outros programas como uma biblioteca. Wasmedge é uma máquina virtual WebAssembly (Wasm) leve, de alto desempenho e escalável, otimizada para computação de borda. Ele pode ser usado para aplicações nativas da nuvem, de borda e descentralizadas.

Primeiro, na Wasm VM, usamos o método load para carregar o arquivo .wasm na memória. Depois disso, podemos usar o método call da VM para invocar essas funções. A VM é baseada na implementação da interface WASI, o que permite que o código Wasm não apenas seja executado no lado do navegador, mas também suporte a execução no lado do servidor.

Resumo

Entendemos o que é Wasm e como o APISIX suporta plugins Wasm. Ao oferecer suporte a plugins Wasm, o APISIX não apenas aprimora suas capacidades em suportar várias linguagens, como C++, Rust, Golang e AssemblyScript para o desenvolvimento de plugins, mas também se beneficia do extenso ecossistema e casos de uso do WebAssembly, que está se expandindo além do navegador para ambientes nativos da nuvem.

Como resultado, o APISIX pode aproveitar o Wasm para oferecer recursos mais avançados no lado do gateway de API, permitindo que ele aborde uma gama mais ampla de cenários de uso.

Tags: