Cómo Apache APISIX habilita la funcionalidad Wasm
Xinxin Zhu
April 7, 2023
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.
¿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.
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
yproxy_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.