الجزء الثالث: كيفية بناء بوابة API للخدمات المصغرة باستخدام OpenResty
API7.ai
February 3, 2023
في هذه المقالة، نصل إلى نهاية بناء بوابة واجهة برمجة التطبيقات (API Gateway) للخدمات الصغيرة. دعونا نستخدم مثالًا بسيطًا لتجميع المكونات التي تم اختيارها مسبقًا وتشغيلها وفقًا للتصميم المخطط!
تكوين NGINX والتهيئة
نعلم أن بوابة واجهة برمجة التطبيقات تُستخدم للتعامل مع دخول حركة المرور، لذا نحتاج أولاً إلى إجراء تكوين بسيط في ملف nginx.conf
بحيث يتم التعامل مع كل حركة المرور من خلال كود Lua الخاص بالبوابة.
server {
listen 9080;
init_worker_by_lua_block {
apisix.http_init_worker()
}
location / {
access_by_lua_block {
apisix.http_access_phase()
}
header_filter_by_lua_block {
apisix.http_header_filter_phase()
}
body_filter_by_lua_block {
apisix.http_body_filter_phase()
}
log_by_lua_block {
apisix.http_log_phase()
}
}
}
هنا نستخدم بوابة واجهة برمجة التطبيقات المفتوحة المصدر Apache APISIX كمثال، لذا يحتوي المثال أعلاه على الكلمة المفتاحية apisix
. في هذا المثال، نستمع إلى المنفذ 9080
ونعترض جميع الطلبات إلى هذا المنفذ عبر location /
، ونعالجها من خلال مراحل access
، rewrite
، header filter
، body filter
و log
، مع استدعاء وظائف الإضافات المقابلة في كل مرحلة. يتم دمج مرحلة rewrite
في وظيفة apisix.http_access_phase
.
يتم التعامل مع تهيئة النظام في مرحلة init_worker
، والتي تشمل قراءة معلمات التكوين، وضبط الدليل المسبق في etcd، والحصول على قائمة الإضافات من etcd، وترتيب الإضافات حسب الأولوية، إلخ. لقد قمت بإدراج وشرح الأجزاء الرئيسية من الكود هنا، ويمكنك رؤية وظيفة التهيئة بشكل كامل على GitHub.
function _M.http_init_worker()
-- تهيئة التوجيه، الخدمات، والإضافات - الأجزاء الثلاثة الأكثر أهمية
router.init_worker()
require("apisix.http.service").init_worker()
require("apisix.plugins.ext-plugin.init").init_worker()
end
كما ترى من هذا الكود، فإن تهيئة أجزاء الموجه والإضافات أكثر تعقيدًا بعض الشيء، حيث تشمل قراءة معلمات التكوين واتخاذ بعض الخيارات بناءً عليها. نظرًا لأن هذا يتضمن قراءة البيانات من etcd، نستخدم ngx.timer
للالتفاف حول قيود "عدم القدرة على استخدام cosocket في مرحلة init_worker
". إذا كنت مهتمًا بهذا الجزء، نوصي بقراءة الكود المصدري لفهمه بشكل أفضل.
مطابقة المسارات
في بداية مرحلة access
، نحتاج أولاً إلى مطابقة المسار بناءً على الطلب الذي يحمل uri
، host
، args
، cookies
، إلخ، مع قواعد التوجيه التي تم إعدادها.
router.router_http.match(api_ctx)
الكود الوحيد المعرض للعامة هو السطر أعلاه، حيث يخزن api_ctx
معلومات uri
، host
، args
، و cookie
الخاصة بالطلب. يتم تنفيذ وظيفة المطابقة باستخدام lua-resty-radixtree
التي ذكرناها سابقًا. إذا لم يتم مطابقة أي مسار، فإن الطلب لا يحتوي على خادم مرجعي (upstream) مقابلة له، وسيتم إرجاع 404
.
local router = require("resty.radixtree")
local match_opts = {}
function _M.match(api_ctx)
-- الحصول على معلمات الطلب من ctx واستخدامها كشرط للحكم على المسار
match_opts.method = api_ctx.var.method
match_opts.host = api_ctx.var.host
match_opts.remote_addr = api_ctx.var.remote_addr
match_opts.vars = api_ctx.var
-- استدعاء وظيفة الحكم على المسار
local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx)
-- إذا لم يتم مطابقة أي مسار، يتم إرجاع 404
if not ok then
core.log.info("not find any matched route")
return core.response.exit(404)
end
return true
end
تحميل الإضافات
بالطبع، إذا تمت مطابقة المسار، ننتقل إلى خطوة تصفية وتحميل الإضافات، وهي جوهر بوابة واجهة برمجة التطبيقات. لنبدأ بالكود التالي.
local plugins = core.tablepool.fetch("plugins", 32, 0)
-- يتم تقاطع قائمة الإضافات في etcd مع قائمة الإضافات في ملف التكوين المحلي
api_ctx.plugins = plugin.filter(route, plugins)
-- تشغيل الوظائف المثبتة بواسطة الإضافة في مراحل rewrite و access بالتسلسل
run_plugin("rewrite", plugins, api_ctx)
run_plugin("access", plugins, api_ctx)
في هذا الكود، نطلب أولاً جدولًا بطول 32 من خلال تجمع الجداول، وهي تقنية تحسين أداء قدمناها سابقًا. ثم تأتي وظيفة التصفية الخاصة بالإضافة. قد تتساءل لماذا نحتاج إلى هذه الخطوة. في مرحلة init worker الخاصة بالإضافة، ألم نحصل بالفعل على قائمة الإضافات من etcd وفرزها؟
التصفية هنا تتم بالمقارنة مع التكوين المحلي للأسباب التالية:
- أولاً، الإضافة الجديدة التي تم تطويرها تحتاج إلى إصدار تجريبي. في هذه الحالة، تكون الإضافة الجديدة موجودة في قائمة etcd ولكنها مفتوحة فقط في بعض عقد البوابة. لذا، نحتاج إلى إجراء عملية تقاطع إضافية.
- لدعم وضع التصحيح. ما هي الإضافات التي يتم معالجتها بواسطة طلب العميل؟ ما هو ترتيب تحميل هذه الإضافات؟ ستكون هذه المعلومات مفيدة عند التصحيح، لذا ستحدد وظيفة التصفية أيضًا ما إذا كانت في وضع التصحيح، وتسجل هذه المعلومات في رأس الاستجابة.
لذا في نهاية مرحلة access
، نأخذ هذه الإضافات المصفاة ونشغلها واحدة تلو الأخرى حسب الأولوية، كما هو موضح في الكود التالي.
local function run_plugin(phase, plugins, api_ctx)
for i = 1, #plugins, 2 do
local phase_fun = plugins[i][phase]
if phase_fun then
-- الكود الأساسي للاستدعاء
phase_fun(plugins[i + 1], api_ctx)
end
end
return api_ctx
end
عند التكرار عبر الإضافات، يمكنك أن ترى أننا نقوم بذلك بفواصل 2
. هذا لأن كل إضافة ستحتوي على مكونين: كائن الإضافة ومعلمات تكوين الإضافة. الآن، دعونا ننظر إلى السطر الأساسي في الكود أعلاه.
phase_fun(plugins[i + 1], api_ctx)
إذا كان هذا السطر من الكود مجردًا بعض الشيء، دعنا نستبدله بإضافة limit_count
محددة، وسيكون الأمر أكثر وضوحًا.
limit_count_plugin_rewrite_function(conf_of_plugin, api_ctx)
في هذه المرحلة، نكون قد انتهينا تقريبًا من التدفق العام لبوابة واجهة برمجة التطبيقات. كل هذا الكود موجود في نفس الملف، والذي يحتوي على أكثر من 400 سطر من الكود، ولكن جوهر الكود هو بضع عشرات من الأسطر التي وصفناها أعلاه.
كتابة الإضافات
الآن، هناك شيء واحد متبقي قبل أن نتمكن من تشغيل عرض تجريبي كامل، وهو كتابة إضافة. لنأخذ إضافة limit-count
كمثال. التنفيذ الكامل يتكون من أكثر من 60 سطرًا من الكود، يمكنك رؤيته من خلال النقر على الرابط. هنا، سأشرح الأسطر الرئيسية من الكود بالتفصيل:
أولاً، نقدم lua-resty-limit-traffic
كالمكتبة الأساسية للحد من عدد الطلبات.
local limit_count_new = require("resty.limit.count").new
ثم، باستخدام json schema
في rapidjson
لتحديد ما هي معلمات هذه الإضافة:
local schema = {
type = "object",
properties = {
count = {type = "integer", minimum = 0},
time_window = {type = "integer", minimum = 0},
key = {type = "string",
enum = {"remote_addr", "server_addr"},
},
rejected_code = {type = "integer", minimum = 200, maximum = 600},
},
additionalProperties = false,
required = {"count", "time_window", "key", "rejected_code"},
}
هذه المعلمات الخاصة بالإضافة تتوافق مع معظم معلمات resty.limit.count
، والتي تحتوي على مفتاح الحد، حجم نافذة الوقت، وعدد الطلبات التي سيتم الحد منها. بالإضافة إلى ذلك، تضيف الإضافة معلمة: rejected_code
، والتي تُرجع رمز الحالة المحدد عند الحد من الطلب.
في الخطوة الأخيرة، نقوم بتثبيت وظيفة معالج الإضافة على مرحلة rewrite
:
function _M.rewrite(conf, ctx)
-- الحصول على كائن الحد من الذاكرة المؤقتة، إذا لم يكن موجودًا، استخدم وظيفة `create_limit_obj` لإنشاء كائن جديد وتخزينه في الذاكرة المؤقتة
local lim, err = core.lrucache.plugin_ctx(plugin_name, ctx, create_limit_obj, conf)
-- الحصول على قيمة المفتاح من `ctx.var` وتكوين مفتاح جديد مع نوع التكوين ورقم إصدار التكوين
local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
-- وظيفة لتحديد ما إذا تم الوصول إلى الحد
local delay, remaining = lim:incoming(key, true)
if not delay then
local err = remaining
-- إذا تم تجاوز قيمة الحد، يتم إرجاع رمز الحالة المحدد
if err == "rejected" then
return conf.rejected_code
end
core.log.error("failed to limit req: ", err)
return 500
end
-- إذا لم يتم تجاوز الحد، يتم الإفراج عنه وتعيين رأس الاستجابة المقابل
core.response.set_header("X-RateLimit-Limit", conf.count,
"X-RateLimit-Remaining", remaining)
end
يوجد سطر واحد فقط من المنطق في الكود أعلاه يقوم بتحديد الحد، والباقي هنا للقيام بأعمال التحضير وتعيين رؤوس الاستجابة. إذا لم يتم تجاوز الحد، سيستمر في تشغيل الإضافة التالية حسب الأولوية.
الخلاصة
أخيرًا، سأترك لكم سؤالًا مثيرًا للتفكير. نعلم أن بوابات واجهة برمجة التطبيقات يمكنها التعامل ليس فقط مع حركة المرور من الطبقة السابعة (Layer 7) ولكن أيضًا مع حركة المرور من الطبقة الرابعة (Layer 4). بناءً على ذلك، هل يمكنك التفكير في بعض سيناريوهات الاستخدام لها؟ نرحب بترك تعليقاتك ومشاركة هذه المقالة للتعلم والتواصل مع المزيد من الأشخاص.
السابق: الجزء 1: كيفية بناء بوابة واجهة برمجة التطبيقات للخدمات الصغيرة باستخدام OpenResty الجزء 2: كيفية بناء بوابة واجهة برمجة التطبيقات للخدمات الصغيرة باستخدام OpenResty