الجزء الثالث: كيفية بناء بوابة API للخدمات المصغرة باستخدام OpenResty

API7.ai

February 3, 2023

OpenResty (NGINX + Lua)

في هذه المقالة، نصل إلى نهاية بناء بوابة واجهة برمجة التطبيقات (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 وفرزها؟

التصفية هنا تتم بالمقارنة مع التكوين المحلي للأسباب التالية:

  1. أولاً، الإضافة الجديدة التي تم تطويرها تحتاج إلى إصدار تجريبي. في هذه الحالة، تكون الإضافة الجديدة موجودة في قائمة etcd ولكنها مفتوحة فقط في بعض عقد البوابة. لذا، نحتاج إلى إجراء عملية تقاطع إضافية.
  2. لدعم وضع التصحيح. ما هي الإضافات التي يتم معالجتها بواسطة طلب العميل؟ ما هو ترتيب تحميل هذه الإضافات؟ ستكون هذه المعلومات مفيدة عند التصحيح، لذا ستحدد وظيفة التصفية أيضًا ما إذا كانت في وضع التصحيح، وتسجل هذه المعلومات في رأس الاستجابة.

لذا في نهاية مرحلة 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