كيف تمكن 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)، يمكن أيضًا للبيئات من جانب الخادم دعم تنفيذ تعليمات 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 يعتمد على واجهة التطبيق الثنائية (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. ومع ذلك، من المهم ملاحظة أنه بسبب بعض المشكلات في دعم Golang الأصلي لـ WASI، يتم تنفيذ هذا 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، نحتاج إلى التفاعل مع واجهات برمجة التطبيقات (APIs) المقدمة من OpenResty أو NGINX. هذا بالضبط ما يفعله wasm-nginx-module
.
wasm-nginx-module هو وحدة NGINX تدعم Wasm وتم تطويرها بواسطة API7. تحاول هذه الوحدة تنفيذ proxy-wasm-abi بناءً على NGINX وتغليف واجهة برمجة التطبيقات Lua، مما يسمح لنا بإكمال استدعاءات 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
، ثم نستدعي الطريقة في VM من خلال 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) تم تحسينها للحوسبة الطرفية. يمكن استخدامها للتطبيقات السحابية والطرفية واللامركزية.
أولاً، في Wasm VM، نستخدم طريقة load
لتحميل ملف .wasm
في الذاكرة. بعد ذلك، يمكننا استخدام طريقة call
الخاصة بـ VM لاستدعاء هذه الوظائف. يعتمد VM على تنفيذ واجهة WASI، مما يتيح لكود Wasm ليس فقط العمل على جانب المتصفح ولكن أيضًا دعم التشغيل على جانب الخادم.
الخلاصة
لقد تعرفنا على ما هو Wasm وكيف يدعم APISIX إضافات Wasm. من خلال تقديم دعم لإضافات Wasm، لا يعزز APISIX فقط قدراته في دعم لغات متعددة مثل C++ وRust وGolang وAssemblyScript لتطوير الإضافات، ولكن أيضًا يستفيد من النظام البيئي الواسع وحالات الاستخدام لـ WebAssembly، الذي يتوسع خارج المتصفح إلى البيئات السحابية الأصلية.
نتيجة لذلك، يمكن لـ APISIX الاستفادة من Wasm لتقديم ميزات أكثر تقدمًا على جانب بوابة API، مما يمكنه من معالجة نطاق أوسع من حالات الاستخدام.