Как Apache APISIX обеспечивает функциональность Wasm
Xinxin Zhu
April 7, 2023
Чтобы обеспечить выполнение высокопроизводительных приложений в веб-браузерах, Apache APISIX поддерживает Wasm на уровне шлюза — бинарный формат инструкций, который позволяет эффективно и безопасно выполнять код в веб-среде. В результате разработчики могут использовать языки программирования высокого уровня, такие как C/C++/Go/Rust, и следовать спецификации proxy-wasm для создания Wasm-плагинов. Спецификация proxy-wasm обеспечивает совместимость и взаимодействие с другими системами. Давайте углубимся в эту статью и узнаем больше деталей.
Что такое Wasm
WebAssembly (сокращенно Wasm) — это бинарный формат инструкций для стековой виртуальной машины.
До появления Wasm в веб-браузерах мог выполняться только JavaScript. Однако с появлением Wasm языки высокого уровня, такие как C/C++/Golang, теперь могут выполняться в браузерах. Основные браузеры, такие как Chrome, Firefox и Safari, теперь поддерживают Wasm. Более того, благодаря разработкам проекта WASI (WebAssembly System Interface), серверные среды также могут поддерживать выполнение инструкций Wasm.
В настоящее время Apache APISIX поддерживает Wasm на уровне шлюза. Разработчики могут использовать языки программирования высокого уровня, такие как C/C++/Go/Rust, и следовать спецификации proxy-wasm для создания Wasm-плагинов.

Почему APISIX поддерживает Wasm-плагины?
По сравнению с нативными Lua-плагинами, Wasm-плагины предлагают несколько преимуществ:
-
Масштабируемость: Поддерживая Wasm, APISIX может использовать SDK, предоставляемый proxy-wasm, для разработки плагинов на языках высокого уровня, таких как C++, Golang и Rust. Эти языки часто имеют более богатые экосистемы, что позволяет разработчикам реализовывать более функциональные плагины.
-
Безопасность: Поскольку взаимодействие между APISIX и Wasm основано на Application Binary Interface (ABI), предоставляемом proxy-wasm, этот доступ более безопасен. Wasm-плагины могут вносить только определенные изменения в запросы, что гарантирует, что они не могут выполнять вредоносные действия. Кроме того, поскольку Wasm-плагины выполняются в отдельной виртуальной машине, даже если плагин завершится с ошибкой, это не повлияет на основной процесс APISIX.
Как APISIX поддерживает Wasm?
Теперь, когда мы понимаем, что такое Wasm, давайте рассмотрим, как APISIX поддерживает Wasm-плагины, начиная с верхнего уровня.

Wasm-плагины APISIX
APISIX позволяет разработчикам создавать плагины с использованием популярных языков программирования высокого уровня, таких как C/C++, Go и Rust. Эти плагины могут быть созданы с использованием соответствующего SDK и следуя спецификации proxy-wasm.
proxy-wasm — это спецификация для ABI между прокси L4/L7, представленная Envoy. Эта спецификация определяет ABI, включая управление памятью, расширения прокси L4 и L7. Например, в HTTP(L7) спецификация proxy-wasm определяет ABI, такие как proxy_on_http_request_headers, proxy_on_http_request_body, proxy_on_http_request_trailers и proxy_on_http_response_headers, что позволяет модулям получать и изменять содержимое запроса на различных этапах.
Например, мы будем использовать Golang и proxy-wasm-go-sdk для разработки этого плагина:
proxy-wasm-go-sdk — это SDK для спецификации proxy-wasm, который помогает разработчикам создавать proxy-wasm-плагины более легко с использованием Golang. Однако важно отметить, что из-за некоторых проблем с поддержкой WASI в нативном Golang, этот SDK реализован на основе TinyGo. Для получения дополнительной информации вы можете нажать здесь, чтобы просмотреть детали.
Основная функция этого плагина — изменять код состояния HTTP-ответа и тело ответа 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 } ...
После этого мы используем TinyGo для компиляции вышеуказанного кода на 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 в файле config.yaml APISIX и назовите плагин как 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-плагинах, нам необходимо взаимодействовать с API, предоставляемыми OpenResty или NGINX. Именно это и делает wasm-nginx-module.
wasm-nginx-module — это модуль NGINX, который поддерживает Wasm и был разработан API7. Этот модуль пытается реализовать proxy-wasm-abi на основе NGINX и инкапсулирует Lua API, что позволяет нам выполнять вызовы proxy-wasm-abi на уровне Lua. Для получения дополнительной информации, пожалуйста, обратитесь к wasm-nginx-module.
Например, когда APISIX достигает фазы access, он вызывает Lua-метод on_http_request_headers, предоставляемый 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 ...
Позже в этом методе будет вызван метод ngx_http_wasm_on_http, предоставляемый 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); } ... }
В wasm-nginx-module мы устанавливаем cb_name на основе различных этапов, например, HTTP_REQUEST_HEADERS соответствует proxy_on_request_headers, а затем вызываем метод в виртуальной машине через ngx_wasm_vm->call, который является методом OnHttpRequestHeaders wasm-плагина, упомянутого ранее в этой статье.
Таким образом, завершается вся цепочка вызовов APISIX, вызывающего wasm-плагин и выполняющего Golang. Цепочка вызовов выглядит следующим образом:
Wasm VM
Wasm VM — это виртуальная машина, используемая для выполнения кода Wasm. wasm-nginx-module реализует два типа виртуальных машин, "wasmtime" и "wasmedge", для выполнения кода Wasm. В APISIX по умолчанию используется "wasmtime" для выполнения кода Wasm.
Wasmtime — это быстрая и безопасная среда выполнения для WebAssembly и WASI, открытая Bytecode Alliance. Она может выполнять код WebAssembly вне веб-среды и может использоваться как инструмент командной строки или встраиваться как библиотека среды выполнения WebAssembly в другие программы. Wasmedge — это легковесная, высокопроизводительная и масштабируемая виртуальная машина WebAssembly (Wasm), оптимизированная для edge-вычислений. Она может использоваться для облачных, edge и децентрализованных приложений.
Сначала в Wasm VM мы используем метод load для загрузки файла .wasm в память. После этого мы можем использовать метод call виртуальной машины для вызова этих функций. Виртуальная машина основана на реализации интерфейса WASI, что позволяет коду Wasm не только выполняться на стороне браузера, но и поддерживать выполнение на стороне сервера.
Итог
Мы узнали, что такое Wasm и как APISIX поддерживает Wasm-плагины. Предоставляя поддержку Wasm-плагинов, APISIX не только расширяет свои возможности в поддержке нескольких языков, таких как C++, Rust, Golang и AssemblyScript для разработки плагинов, но также получает выгоду от обширной экосистемы и вариантов использования WebAssembly, который выходит за пределы браузера в облачные среды.
В результате APISIX может использовать Wasm для предоставления более продвинутых функций на стороне API-шлюза, что позволяет ему охватывать более широкий спектр сценариев использования.