مقدمة عن واجهات برمجة التطبيقات (APIs) الشائعة في OpenResty
API7.ai
November 4, 2022
في المقالات السابقة، تعرفت على العديد من واجهات برمجة التطبيقات (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
؟ مرحبًا بك في مشاركة رأيك في التعليقات، وأيضًا مرحبًا بك في مشاركة هذه المقالة حتى نتمكن من التواصل والتحسين معًا.