كيف يكون Apache APISIX سريعًا؟

Navendu Pottekkat

Navendu Pottekkat

June 12, 2023

Technology

"السرعة العالية"، "الحد الأدنى من الكمون"، و"الأداء الأمثل" هي مصطلحات تُستخدم غالبًا لوصف Apache APISIX. حتى عندما يسألني أحد عن APISIX، تكون إجابتي دائمًا تتضمن "بوابة API سحابية عالية الأداء".

تؤكد اختبارات الأداء (مقارنة بـ Kong، Envoy) أن هذه الخصائص دقيقة بالفعل (اختبرها بنفسك).

السرعة العالية، الحد الأدنى من الكمون، والأداء الأمثل

تم إجراء الاختبارات لـ 10 جولات مع 5000 مسار فريد على Standard D8s v3 (8 وحدات معالجة مركزية، 32 جيجابايت من الذاكرة).

ولكن كيف يحقق APISIX هذا؟

للإجابة على هذا السؤال، يجب أن ننظر إلى ثلاثة أشياء: etcd، وجداول التجزئة، وأشجار الراديكس.

في هذه المقالة، سنلقي نظرة تحت غطاء APISIX ونرى ما هي هذه الأشياء وكيف تعمل معًا للحفاظ على أداء APISIX في ذروته أثناء التعامل مع حركة مرور كبيرة.

etcd كمركز للتكوين

يستخدم APISIX etcd لتخزين ومزامنة التكوينات.

تم تصميم etcd ليعمل كمخزن قيم-مفاتيح لتكوينات أنظمة موزعة واسعة النطاق. تم تصميم APISIX ليكون موزعًا وقابلًا للتوسع بشكل كبير منذ البداية، واستخدام etcd بدلاً من قواعد البيانات التقليدية يسهل ذلك.

هندسة APISIX

ميزة أخرى لا غنى عنها لبوابات API هي أن تكون عالية التوفر، وتجنب التوقف عن العمل وفقدان البيانات. يمكن تحقيق ذلك بكفاءة عن طريق نشر عدة نسخ من etcd لضمان بنية سحابية متسامحة مع الأخطاء.

يمكن لـ APISIX قراءة/كتابة التكوينات من/إلى etcd بأقل كمون. يتم إخطار التغييرات في ملفات التكوين على الفور، مما يسمح لـ APISIX بمراقبة تحديثات etcd فقط بدلاً من استطلاع قاعدة البيانات بشكل متكرر، مما قد يزيد من عبء الأداء.

يُلخص هذا الرسم البياني كيفية مقارنة etcd مع قواعد البيانات الأخرى.

جداول التجزئة لعناوين IP

قوائم السماح/الحظر بناءً على عناوين IP هي حالة استخدام شائعة لبوابات API.

لتحقيق أداء عالي، يخزن APISIX قائمة عناوين IP في جدول تجزئة ويستخدمها للمطابقة (O(1)) بدلاً من التكرار عبر القائمة (O(N)).

مع زيادة عدد عناوين IP في القائمة، يصبح تأثير الأداء لاستخدام جداول التجزئة للتخزين والمطابقة واضحًا.

تحت الغطاء، يستخدم APISIX مكتبة lua-resty-ipmatcher لتنفيذ هذه الوظيفة. يوضح المثال التالي كيفية استخدام المكتبة:

local ipmatcher = require("resty.ipmatcher")
local ip = ipmatcher.new({
    "162.168.46.72",
    "17.172.224.47",
    "216.58.32.170",
})

ngx.say(ip:match("17.172.224.47")) -- true
ngx.say(ip:match("176.24.76.126")) -- false

تستخدم المكتبة جداول Lua التي هي جداول تجزئة. يتم تجزئة عناوين IP وتخزينها كمؤشرات في جدول، وللبحث عن عنوان IP معين، ما عليك سوى فهرسة الجدول واختبار ما إذا كان nil أم لا.

تخزين عناوين IP في جدول تجزئة

للبحث عن عنوان IP، يتم أولاً حساب التجزئة (المؤشر) والتحقق من قيمته. إذا كانت غير فارغة، يكون لدينا تطابق. يتم ذلك في وقت ثابت O(1).

أشجار الراديكس للتوجيه

يرجى مسامحتي لخداعكم في درس هياكل البيانات! ولكن اسمعوا مني؛ هذا هو المكان الذي يصبح فيه الأمر مثيرًا للاهتمام.

منطقة رئيسية حيث يحسن APISIX الأداء هي مطابقة المسارات.

يقوم APISIX بمطابقة مسار مع طلب من URI، وطرق HTTP، والمضيف، ومعلومات أخرى (انظر الموجه). ويجب أن يكون هذا فعالاً.

إذا كنت قد قرأت القسم السابق، فإن الإجابة الواضحة ستكون استخدام خوارزمية تجزئة. ولكن مطابقة المسارات صعبة لأن عدة طلبات يمكن أن تطابق نفس المسار.

على سبيل المثال، إذا كان لدينا مسار /api/*، فإن كل من /api/create و /api/destroy يجب أن يطابقا المسار. ولكن هذا غير ممكن مع خوارزمية تجزئة.

يمكن أن تكون التعبيرات العادية حلاً بديلاً. يمكن تكوين المسارات في تعبير عادي، ويمكن أن يطابق عدة طلبات دون الحاجة إلى ترميز كل طلب يدويًا.

إذا أخذنا مثالنا السابق، يمكننا استخدام التعبير العادي /api/[A-Za-z0-9]+ لمطابقة كل من /api/create و /api/destroy. يمكن للتعبيرات العادية الأكثر تعقيدًا أن تطابق مسارات أكثر تعقيدًا.

ولكن التعبيرات العادية بطيئة! ونحن نعلم أن APISIX سريع. لذا بدلاً من ذلك، يستخدم APISIX أشجار الراديكس التي هي أشجار بادئة مضغوطة (trie) تعمل بشكل جيد للبحث السريع.

لننظر إلى مثال بسيط. لنفترض أن لدينا الكلمات التالية:

  • romane
  • romanus
  • romulus
  • rubens
  • ruber
  • rubicon
  • rubicundus

سيتم تخزينها في شجرة بادئة بهذا الشكل:

شجرة البادئة

يظهر التحديد اجتياز الكلمة "rubens."

تقوم شجرة الراديكس بتحسين شجرة البادئة عن طريق دمج العقد الفرعية إذا كانت العقدة تحتوي على عقدة فرعية واحدة فقط. سيبدو مثالنا كشجرة راديكس بهذا الشكل:

شجرة الراديكس

لا يزال التحديد يظهر اجتياز الكلمة "rubens." ولكن الشجرة تبدو أصغر بكثير!

عندما تقوم بإنشاء مسارات في APISIX، يقوم APISIX بتخزينها في هذه الأشجار.

يمكن لـ APISIX بعد ذلك العمل بسلاسة لأن الوقت الذي يستغرقه مطابقة مسار يعتمد فقط على طول URI في الطلب وهو مستقل عن عدد المسارات (O(K)، حيث K هو طول المفتاح/URI).

لذا سيكون APISIX سريعًا كما هو عند مطابقة 10 مسارات عندما تبدأ لأول مرة و 5000 مسار عندما تقوم بالتوسع.

يظهر هذا المثال البسيط كيف يمكن لـ APISIX تخزين ومطابقة المسارات باستخدام أشجار الراديكس:

مثال بسيط لمطابقة المسارات في APISIX

يظهر التحديد اجتياز المسار /user/* حيث يمثل * بادئة. لذا فإن URI مثل /user/navendu سيطابق هذا المسار. يجب أن يعطي مثال الكود التالي وضوحًا أكبر لهذه الأفكار.

يستخدم APISIX مكتبة lua-resty-radixtree، التي تغلف rax، وهي تنفيذ لشجرة الراديكس بلغة C. هذا يحسن الأداء مقارنة بتنفيذ المكتبة بلغة Lua بحتة.

يوضح المثال التالي كيفية استخدام المكتبة:

local radix = require("resty.radixtree")
local rx = radix.new({
    {
        paths = { "/api/*action" },
        metadata = { "metadata /api/action" }
    },
    {
        paths = { "/user/:name" },
        metadata = { "metadata /user/name" },
        methods = { "GET" },
    },
    {
        paths = { "/admin/:name" },
        metadata = { "metadata /admin/name" },
        methods = { "GET", "POST", "PUT" },
        filter_fun = function(vars, opts)
            return vars["arg_access"] == "admin"
        end
    }
})

local opts = {
    matched = {}
}

-- matches the first route
ngx.say(rx:match("/api/create", opts)) -- metadata /api/action
ngx.say("action: ", opts.matched.action) -- action: create

ngx.say(rx:match("/api/destroy", opts)) -- metadata /api/action
ngx.say("action: ", opts.matched.action) -- action: destroy

local opts = {
    method = "GET",
    matched = {}
}

-- matches the second route
ngx.say(rx:match("/user/bobur", opts)) -- metadata /user/name
ngx.say("name: ", opts.matched.name) -- name: bobur

local opts = {
    method = "POST",
    var = ngx.var,
    matched = {}
}

-- matches the third route
-- the value for `arg_access` is obtained from `ngx.var`
ngx.say(rx:match("/admin/nicolas", opts)) -- metadata /admin/name
ngx.say("admin name: ", opts.matched.name) -- admin name: nicolas

القدرة على إدارة عدد كبير من المسارات بكفاءة جعلت APISIX بوابة API المفضلة للعديد من المشاريع واسعة النطاق.

النظر تحت الغطاء

هناك الكثير مما يمكنني شرحه عن الأعمال الداخلية لـ APISIX في مقال واحد.

ولكن الجزء الأفضل هو أن المكتبات المذكورة هنا و Apache APISIX هي مفتوحة المصدر بالكامل، مما يعني أنه يمكنك النظر تحت الغطاء وتعديل الأشياء بنفسك.

وإذا كان بإمكانك تحسين APISIX للحصول على تلك القطعة الأخيرة من الأداء، يمكنك المساهمة بالتغييرات في المشروع والسماح للجميع بالاستفادة من عملك.

Tags: