Cómo Apache APISIX habilita la funcionalidad Wasm

Xinxin Zhu

April 7, 2023

Products

Para permitir que aplicaciones de alto rendimiento se ejecuten en navegadores web, Apache APISIX admite Wasm a nivel de puerta de enlace, un formato de instrucción binaria que permite la ejecución eficiente y segura de código en la web. Como resultado, los desarrolladores pueden usar lenguajes de programación de alto nivel como C/C++/Go/Rust y seguir la especificación proxy-wasm para crear plugins Wasm. La especificación proxy-wasm garantiza compatibilidad e interoperabilidad con otros sistemas. Profundicemos en este artículo para conocer más detalles.

¿Qué es Wasm?

WebAssembly (abreviado Wasm) es un formato de instrucción binaria para una máquina virtual basada en pilas.

Antes de la aparición de Wasm, solo se podía ejecutar Javascript en los navegadores web. Sin embargo, con la introducción de Wasm, ahora es posible ejecutar lenguajes de alto nivel como C/C++/Golang en navegadores web. Los principales navegadores, como Chrome, Firefox y Safari, ahora admiten Wasm. Además, gracias a los avances del proyecto WASI (WebAssembly System Interface), los entornos del lado del servidor también pueden admitir la ejecución de instrucciones Wasm.

Actualmente, Apache APISIX admite Wasm a nivel de puerta de enlace. Los desarrolladores pueden usar lenguajes de programación de alto nivel como C/C++/Go/Rust y seguir la especificación proxy-wasm para crear plugins Wasm.

wasm

¿Por qué APISIX admite plugins Wasm?

En comparación con los plugins nativos de Lua, los plugins Wasm ofrecen varias ventajas:

  • Escalabilidad: Al admitir Wasm, APISIX puede utilizar el SDK proporcionado por proxy-wasm para desarrollar plugins en lenguajes de programación de alto nivel como C++, Golang y Rust. Estos lenguajes suelen tener ecosistemas más ricos, lo que permite a los desarrolladores implementar plugins con más funciones.

  • Seguridad: Dado que la interacción entre APISIX y Wasm depende de la Interfaz Binaria de Aplicación (ABI) proporcionada por proxy-wasm, este acceso es más seguro. Los plugins Wasm solo pueden realizar modificaciones específicas en las solicitudes, lo que garantiza que no puedan realizar acciones maliciosas. Además, como los plugins Wasm se ejecutan en una máquina virtual separada, incluso si el plugin falla, no afectará el proceso principal de APISIX.

¿Cómo admite APISIX Wasm?

Ahora que entendemos qué es Wasm, veamos de manera general cómo APISIX admite plugins Wasm.

apisix-wasm

Plugins Wasm de APISIX

APISIX permite a los desarrolladores crear plugins utilizando lenguajes de programación de alto nivel populares como C/C++, Go y Rust. Estos plugins se pueden construir utilizando el SDK correspondiente y siguiendo la especificación proxy-wasm.

proxy-wasm es una especificación para ABIs entre proxies L4/L7, introducida por Envoy. Esta especificación define ABIs que incluyen gestión de memoria, extensiones de proxy L4 y L7. Por ejemplo, en HTTP(L7), la especificación proxy-wasm define ABIs como proxy_on_http_request_headers, proxy_on_http_request_body, proxy_on_http_request_trailers y proxy_on_http_response_headers, lo que permite a los módulos recuperar y modificar el contenido de la solicitud en varias etapas.

Por ejemplo, usaremos Golang y proxy-wasm-go-sdk para desarrollar este plugin:

El proxy-wasm-go-sdk es un SDK para la especificación proxy-wasm que ayuda a los desarrolladores a crear plugins proxy-wasm más fácilmente usando Golang. Sin embargo, es importante tener en cuenta que, debido a algunos problemas con el soporte nativo de Golang para WASI, este SDK se implementa basado en TinyGo. Para más información, puedes hacer clic aquí para ver los detalles.

La función principal de este plugin es modificar el código de estado de la respuesta HTTP y el cuerpo de la respuesta de una solicitud de modificación HTTP, como se referencia en el enlace de 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
}
...

Luego, usamos TinyGo para compilar el código Golang anterior y generar un archivo .wasm.

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

Después de la compilación, obtenemos el archivo fault_injection.go.wasm.

Si estás interesado en el contenido del archivo wasm, puedes usar wasm-tool para ver el contenido específico del archivo wasm. wasm-tools dump hello.go.wasm

Configura wasm_fault_injection.go.wasm en el archivo config.yaml de APISIX y nombra el plugin como wasm_fault_injection.

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

Luego, iniciamos APISIX y creamos una ruta que haga referencia al 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"
}'

Después de realizar una prueba de acceso, encontramos que el cuerpo de la respuesta se ha modificado a "Hello WebAssembly", lo que indica que el plugin Wasm ahora es efectivo.

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

Después de aprender cómo Apache APISIX usa plugins Wasm, profundicemos en "¿por qué podemos acceder y modificar el contenido de las solicitudes en los plugins Wasm?"

Dado que APISIX usa OpenResty como marco subyacente, para poder acceder y modificar el contenido de las solicitudes en los plugins Wasm, necesitamos interactuar con las API proporcionadas por OpenResty o NGINX. Esto es exactamente lo que hace wasm-nginx-module.

wasm-nginx-module es un módulo de NGINX que admite Wasm y fue desarrollado por API7. Este módulo intenta implementar proxy-wasm-abi basado en NGINX y encapsula la API de Lua, lo que nos permite completar llamadas proxy-wasm-abi a nivel de Lua. Para más información, consulta wasm-nginx-module.

Por ejemplo, cuando APISIX llega a la fase access, llama al método Lua on_http_request_headers proporcionado por 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
...

Luego, en este método, se llamará al método ngx_http_wasm_on_http proporcionado por wasm-nginx-module.

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

En wasm-nginx-module, configuramos cb_name según las diferentes etapas, como HTTP_REQUEST_HEADERS que corresponde a proxy_on_request_headers, y luego llamamos al método en la VM a través de ngx_wasm_vm->call, que es el método OnHttpRequestHeaders del plugin wasm mencionado anteriormente en este artículo.

Con esto, se completa toda la cadena de llamadas de APISIX llamando al plugin wasm y ejecutando Golang. La cadena de llamadas es la siguiente:

Wasm VM

Wasm VM es una máquina virtual que se utiliza para ejecutar código Wasm. wasm-nginx-module implementa dos tipos de máquinas virtuales, "wasmtime" y "wasmedge", para ejecutar código Wasm. En APISIX, "wasmtime" es el predeterminado para ejecutar código Wasm.

Wasmtime es un runtime rápido y seguro para WebAssembly y WASI, de código abierto por Bytecode Alliance. Puede ejecutar código WebAssembly fuera del entorno web y puede usarse como una herramienta de línea de comandos o incrustarse como un motor de runtime WebAssembly en otros programas como una biblioteca. Wasmedge es una máquina virtual WebAssembly (Wasm) ligera, de alto rendimiento y escalable, optimizada para la computación en el borde. Puede usarse para aplicaciones nativas de la nube, en el borde y descentralizadas.

Primero, en la VM Wasm, usamos el método load para cargar el archivo .wasm en la memoria. Luego, podemos usar el método call de la VM para invocar estas funciones. La VM se basa en la implementación de la interfaz WASI, lo que permite que el código Wasm no solo se ejecute en el lado del navegador, sino que también admita la ejecución en el lado del servidor.

Resumen

Hemos entendido qué es Wasm y cómo APISIX admite plugins Wasm. Al ofrecer soporte para plugins Wasm, APISIX no solo mejora sus capacidades para admitir múltiples lenguajes, como C++, Rust, Golang y AssemblyScript para el desarrollo de plugins, sino que también se beneficia del extenso ecosistema y casos de uso de WebAssembly, que se está expandiendo más allá del navegador hacia entornos nativos de la nube.

Como resultado, APISIX puede aprovechar Wasm para ofrecer funciones más avanzadas en el lado de la puerta de enlace API, permitiéndole abordar una gama más amplia de escenarios de uso.

Tags: