مقدمة عن واجهات برمجة التطبيقات (APIs) الشائعة في OpenResty

API7.ai

November 4, 2022

OpenResty (NGINX + Lua)

في المقالات السابقة، تعرفت على العديد من واجهات برمجة التطبيقات (APIs) المهمة في Lua في OpenResty. اليوم، سنتعلم عن بعض واجهات برمجة التطبيقات العامة الأخرى، والتي ترتبط بشكل رئيسي بالتعبيرات العادية، الوقت، العمليات، وما إلى ذلك.

واجهات برمجة التطبيقات المتعلقة بالتعبيرات العادية

لنبدأ بالنظر إلى التعبيرات العادية الأكثر استخدامًا والأهم. في OpenResty، يجب علينا استخدام مجموعة واجهات برمجة التطبيقات التي توفرها ngx.re.* للتعامل مع المنطق المتعلق بالتعبيرات العادية بدلاً من استخدام مطابقة الأنماط في Lua. هذا ليس فقط لأسباب تتعلق بالأداء ولكن أيضًا لأن التعبيرات العادية في Lua مكتفية ذاتيًا وليست مواصفة PCRE، مما قد يكون مزعجًا لمعظم المطورين.

في المقالات السابقة، صادفت بالفعل بعض واجهات برمجة التطبيقات ngx.re.*، والتي تكون وثائقها مفصلة جدًا. لذلك لن أذكرها مرة أخرى. هنا، سأقدم واجهتي برمجة التطبيقات التاليتين بشكل منفصل.

ngx.re.split

الأولى هي ngx.re.split. تقسيم السلاسل النصية هو وظيفة شائعة جدًا، وOpenResty يوفر أيضًا واجهة برمجة تطبيقات مقابلة، ولكن العديد من المطورين لا يمكنهم العثور على مثل هذه الوظيفة ويضطرون إلى اختيار تنفيذها بأنفسهم.

لماذا؟ لأن واجهة برمجة التطبيقات ngx.re.split ليست في lua-nginx-module ولكن في lua-resty-core؛ وهي ليست في وثائق الصفحة الرئيسية لـ lua-resty-core ولكن في وثائق الدليل الثالث lua-resty-core/lib/ngx/re.md. نتيجة لذلك، العديد من المطورين غير مدركين تمامًا لوجود هذه الواجهة.

وبالمثل، تشمل واجهات برمجة التطبيقات التي يصعب اكتشافها ngx_resp.add_header، enable_privileged_agent، وغيرها، والتي ذكرناها سابقًا. إذن كيف يمكننا حل هذه المشكلة بسرعة؟ بالإضافة إلى قراءة وثائق الصفحة الرئيسية لـ lua-resty-core، تحتاج إلى قراءة وثائق *.md في دليل lua-resty-core/lib/ngx/ أيضًا.

lua_regex_match_limit

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

نحن نعلم أنه إذا استخدمنا محرك تعبيرات عادية يعتمد على NFA مع التراجع، فهناك خطر من التراجع الكارثي (Catastrophic Backtracking)، حيث يتراجع التعبير العادي كثيرًا عند المطابقة، مما يتسبب في أن يصبح CPU بنسبة 100% ويتم حظر الخدمات.

بمجرد حدوث تراجع كارثي، نحتاج إلى استخدام gdb لتحليل الـ dump أو استخدام systemtap لتحليل البيئة المباشرة لتحديده. لسوء الحظ، اكتشافه مسبقًا ليس سهلًا لأن فقط الطلبات الخاصة ستؤدي إلى تشغيله. هذا يسمح للمهاجمين بالاستفادة من ذلك، وReDoS (RegEx Denial of Service) يشير إلى هذا النوع من الهجمات.

هنا، أقدم لكم بشكل رئيسي كيفية استخدام السطر التالي من الكود في OpenResty لتجنب المشاكل المذكورة أعلاه ببساطة وفعالية:

lua_regex_match_limit يستخدم للحد من عدد التراجعات بواسطة محرك التعبيرات العادية PCRE. بهذه الطريقة، حتى إذا حدث تراجع كارثي، ستكون العواقب محدودة ضمن نطاق لن يتسبب في امتلاء CPU الخاص بك.

lua_regex_match_limit 100000;

واجهات برمجة التطبيقات المتعلقة بالوقت

أكثر واجهات برمجة التطبيقات المتعلقة بالوقت استخدامًا هي ngx.now، والتي تطبع الطابع الزمني الحالي، مثل السطر التالي من الكود:

resty -e 'ngx.say(ngx.now())'

كما ترى من النتائج المطبوعة، ngx.now تتضمن الجزء الكسري، لذا فهي أكثر دقة. واجهة برمجة التطبيقات ذات الصلة ngx.time تعيد فقط الجزء الصحيح من القيمة. الباقي، ngx.localtime، ngx.utctime، ngx.cookie_time و ngx.http_time تستخدم بشكل رئيسي لإعادة ومعالجة الوقت بتنسيقات مختلفة. إذا كنت تريد استخدامها، يمكنك التحقق من الوثائق، فهي ليست صعبة الفهم، لذا لا داعي للحديث عنها.

ومع ذلك، تجدر الإشارة إلى أن هذه الواجهات التي تعيد الوقت الحالي، إذا لم يتم تشغيلها بواسطة عملية IO شبكية غير متوقفة، ستظل تعيد القيمة المخزنة مؤقتًا بدلاً من الوقت الحقيقي الحالي كما نريد. انظر إلى نموذج الكود التالي:

$ resty -e 'ngx.say(ngx.now())
os.execute("sleep 1")
ngx.say(ngx.now())'

بين الاستدعائين لـ ngx.now، استخدمنا وظيفة Lua المتوقفة للنوم لمدة 1 ثانية، ولكن الطابع الزمني الذي تم إرجاعه هو نفسه في كلتا الحالتين، كما يظهر من النتائج المطبوعة.

إذن، ماذا لو استبدلناها بوظيفة نوم غير متوقفة؟ على سبيل المثال، الكود الجديد التالي:

$ resty -e 'ngx.say(ngx.now())
ngx.sleep(1)
ngx.say(ngx.now())'

سيطبع طابعًا زمنيًا مختلفًا. هذا يقودنا إلى ngx.sleep، وهي وظيفة نوم غير متوقفة. بالإضافة إلى النوم لفترة زمنية محددة، هذه الوظيفة لها غرض خاص آخر.

على سبيل المثال، إذا كان لديك جزء من الكود يقوم بحسابات مكثفة، والتي تستغرق الكثير من الوقت، فإن الطلبات المقابلة لهذا الجزء من الكود ستستمر في استهلاك موارد العامل وCPU خلال هذا الوقت، مما يتسبب في تراكم الطلبات الأخرى وعدم الحصول على استجابة في الوقت المناسب. في هذه الحالة، يمكننا إدراج ngx.sleep(0) لجعل هذا الكود يتخلى عن التحكم حتى يمكن معالجة الطلبات الأخرى أيضًا.

واجهات برمجة التطبيقات المتعلقة بالعامل والعمليات

يوفر OpenResty واجهات برمجة التطبيقات ngx.worker.* و ngx.process.* للحصول على معلومات عن العمال والعمليات. الأولى تتعلق بعمليات عامل Nginx، بينما تشير الثانية إلى جميع عمليات Nginx بشكل عام، ليس فقط عمليات العامل، ولكن أيضًا العملية الرئيسية، العملية المميزة، وما إلى ذلك.

مشكلة قيم true و null

أخيرًا، لننظر إلى مشكلة قيم true و null. في OpenResty، تحديد قيمة true وقيم null كان دائمًا نقطة إشكالية ومربكة.

لننظر إلى تعريف قيمة true في Lua: باستثناء nil و false، كلها قيم true.

إذن، قيم true ستشمل أيضًا 0، سلسلة نصية فارغة string، جدول فارغ table، إلخ.

لننظر إلى nil في Lua، والتي تعني غير معرّف. على سبيل المثال، إذا قمت بتعريف متغير ولكن لم تقم بتهيئته، فإن قيمته تكون nil.

$ resty -e 'local a
ngx.say(type(a))'

و nil هي أيضًا نوع بيانات في Lua. بعد فهم هاتين النقطتين، لننظر الآن إلى القضايا الأخرى المشتقة من هذين التعريفين.

ngx.null

القضية الأولى هي ngx.null. لأن nil في Lua لا يمكن استخدامها كقيمة في table، قدم OpenResty ngx.null كقيمة null في الجدول.

$ resty -e  'print(ngx.null)'
null
$ resty -e 'print(type(ngx.null))'
userdata

كما ترى من الكودين أعلاه، ngx.null يتم طباعته كـ null، ونوعه هو userdata، فهل يمكن اعتباره قيمة false؟ بالطبع لا. القيمة المنطقية لـ ngx.null هي true.

$ resty -e 'if ngx.null then
ngx.say("true")
end'

لذا، تذكر أن فقط nil و false هما قيمتا false. إذا فاتتك هذه النقطة، فمن السهل الوقوع في الأخطاء، على سبيل المثال، عند استخدام lua-resty-redis وإجراء الحكم التالي:

local res, err = red:get("dog")
if not res then
    res = res + "test"
end

إذا كانت القيمة المرجعة res هي nil، فإن استدعاء الوظيفة قد فشل؛ إذا كانت res هي ngx.null، فإن المفتاح dog غير موجود في redis، ثم يتعطل الكود إذا كان المفتاح dog غير موجود.

cdata:NULL

القضية الثانية هي cdata:NULL. عند استدعاء وظيفة C من خلال واجهة LuaJIT FFI، وإذا كانت الوظيفة ترجع مؤشر NULL، فستواجه نوعًا آخر من قيم null، وهي cdata:NULL.

$ resty -e 'local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
if cdata_null then
    ngx.say("true")
end'

مثل ngx.null، cdata:NULL هي أيضًا true. ولكن الأكثر إرباكًا هو أن الكود التالي، الذي يطبع true، يعني أن cdata:NULL تعادل nil.

$ resty -e 'local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
ngx.say(cdata_null == nil)'

إذن كيف يجب أن نتعامل مع ngx.null و cdata:NULL؟ ليس حلًا جيدًا أن نهتم بهذه المشاكل في طبقة التطبيق. من الأفضل القيام بطبقة ثانية من التغليف وعدم إعلام المتصل بهذه التفاصيل.

من الأفضل القيام بطبقة ثانية من التغليف وعدم إعلام المتصل بهذه التفاصيل.

cjson.null

أخيرًا، لننظر إلى قيم null التي تظهر في cjson. مكتبة cjson تأخذ NULL في json، وتفك تشفيرها إلى lightuserdata في Lua، وتستخدم cjson.null لتمثيلها.

$ resty -e 'local cjson = require "cjson"
local data = cjson.encode(nil)
local decode_null = cjson.decode(data)
ngx.say(decode_null == cjson.null)'

nil في Lua تصبح cjson.null بعد التشفير وفك التشفير بواسطة JSON. كما يمكنك أن تتخيل، تم تقديمها لنفس السبب مثل ngx.null، لأن nil لا يمكن استخدامها كقيمة في table.

حتى الآن، هل تشعر بالارتباك بسبب العديد من أنواع قيم null في OpenResty؟ لا تقلق. اقرأ هذا الجزء عدة مرات وقم بترتيبه بنفسك، ثم لن تشعر بالارتباك. بالطبع، نحتاج إلى التفكير أكثر في المستقبل عما إذا كان يعمل عند كتابة شيء مثل if not foo then.

الخلاصة

يقدم مقال اليوم واجهات برمجة التطبيقات الشائعة الاستخدام في Lua في OpenResty.

أخيرًا، سأترك لك سؤالًا: في مثال ngx.now، لماذا لا يتم تعديل قيمة ngx.now عندما لا تكون هناك عملية yield؟ مرحبًا بك في مشاركة رأيك في التعليقات، وأيضًا مرحبًا بك في مشاركة هذه المقالة حتى نتمكن من التواصل والتحسين معًا.

Share article link