OpenResty الأسئلة الشائعة | التحميل الديناميكي، NYI، وتخزين Shared Dict مؤقتًا
API7.ai
January 19, 2023
تم تحديث سلسلة مقالات Openresty حتى الآن، وجزء تحسين الأداء هو كل ما تعلمناه. تهانينا لكم لعدم التأخر، لا تزالون تتعلمون وتمارسون بنشاط، وتتركون أفكاركم بحماس.
لقد جمعنا الكثير من الأسئلة الأكثر نموذجية وإثارة للاهتمام، وهنا نلقي نظرة على خمسة منها.
السؤال 1: كيف يمكنني تحقيق التحميل الديناميكي لوحدات Lua؟
الوصف: لدي سؤال حول التحميل الديناميكي المطبق في OpenResty. كيف يمكنني استخدام دالة
loadstring
لإنهاء تحميل ملف جديد بعد استبداله؟ أفهم أنloadstring
يمكنها فقط تحميل السلاسل، لذا إذا كنت أرغب في إعادة تحميل ملف/وحدة lua، كيف يمكنني القيام بذلك في OpenResty؟
كما نعلم، loadstring
تُستخدم لتحميل سلسلة، بينما loadfile
يمكنها تحميل ملف محدد، على سبيل المثال: loadfile("foo.lua")
. هاتان الأوامر تحققان نفس النتيجة. أما بالنسبة لكيفية تحميل وحدات Lua، إليك مثال:
resty -e 'local s = [[
local ngx = ngx
local _M = {}
function _M.f()
ngx.say("hello world")
end
return _M
]]
local lua = loadstring(s)
local ret, func = pcall(lua)
func.f()'
محتوى السلسلة s
هو وحدة Lua كاملة. لذا، عندما تجد تغييرًا في كود هذه الوحدة، يمكنك إعادة التحميل باستخدام loadstring
أو loadfile
. بهذه الطريقة، سيتم تحديث الدوال والمتغيرات فيها معها.
للتقدم خطوة أخرى، يمكنك أيضًا تغليف جلب التغييرات وإعادة التحميل بطبقة تسمى دالة code_loader
.
local func = code_loader(name)
هذا يجعل تحديثات الكود أكثر إيجازًا. في الوقت نفسه، code_loader
تستخدم عادة lru cache
لتخزين s
لتجنب استدعاء loadstring
في كل مرة.
السؤال 2: لماذا لا يمنع OpenResty العمليات الحاجزة؟
الوصف: على مر السنين، كنت أتساءل دائمًا، بما أن هذه الاستدعاءات الحاجزة يتم تثبيطها رسميًا، لماذا لا يتم تعطيلها ببساطة؟ أو إضافة علامة للسماح للمستخدم باختيار تعطيلها؟
هذا رأيي الشخصي. أولاً، لأن النظام البيئي حول OpenResty ليس مثاليًا، أحيانًا يتعين علينا استدعاء مكتبات حاجزة لتنفيذ بعض الوظائف. على سبيل المثال، قبل الإصدار 1.15.8، كان عليك استخدام مكتبة Lua os.execute
بدلاً من lua-resty-shell
لاستدعاء الأوامر الخارجية. على سبيل المثال، في OpenResty، قراءة وكتابة الملفات لا تزال ممكنة فقط مع مكتبة Lua I/O، ولا يوجد بديل غير حاجز.
ثانيًا، OpenResty حذرة جدًا بشأن مثل هذه التحسينات. على سبيل المثال، lua-resty-core
تم تطويرها لفترة طويلة، ولكن لم يتم تشغيلها افتراضيًا، مما يتطلب منك استدعاء require 'resty.core'
يدويًا. تم تشغيلها حتى الإصدار الأخير 1.15.8.
أخيرًا، يفضل مشرفو OpenResty توحيد الاستدعاءات الحاجزة عن طريق توليد كود Lua مُحسّن للغاية من خلال المترجم وDSL. لذا، لا يوجد جهد للقيام بشيء مثل خيارات العلامات على منصة OpenResty نفسها. بالطبع، لست متأكدًا مما إذا كان هذا الاتجاه يمكن أن يحل المشكلة.
من وجهة نظر مطور خارجي، المشكلة الأكثر عملية هي كيفية تجنب مثل هذه الحواجز. يمكننا توسيع أدوات اكتشاف كود Lua، مثل luacheck
، للعثور على العمليات الحاجزة الشائعة والتنبيه عليها، أو يمكننا تعطيل أو إعادة كتابة بعض الدوال مباشرة عن طريق إعادة كتابة _G
، على سبيل المثال:
resty -e '_G.ngx.print = function()
ngx.say("hello")
end
ngx.print()'
# hello
مع هذا الكود النموذجي، يمكنك إعادة كتابة دالة ngx.print
مباشرة.
السؤال 3: هل لعملية NYI في LuaJIT تأثير كبير على الأداء؟
الوصف:
loadstring
تظهرnever
في قائمة NYI في LuaJIT. هل سيكون لها تأثير كبير على الأداء؟
بشأن NYI في LuaJIT، لا نحتاج إلى أن نكون صارمين للغاية. بالنسبة للعمليات التي يمكن JIT، فإن نهج JIT هو الأفضل بطبيعة الحال؛ ولكن بالنسبة للعمليات التي لا يمكن JIT بعد، يمكننا الاستمرار في استخدامها.
لتحسين الأداء، نحتاج إلى اتخاذ نهج علمي قائم على الإحصاءات، وهو ما تدور حوله عينات الرسم البياني للهب. التحسين المبكر هو أصل كل شر. نحتاج فقط إلى تحسين الكود الساخن الذي يتم استدعاؤه كثيرًا ويستهلك الكثير من وحدة المعالجة المركزية.
بالعودة إلى loadstring
، سنستدعيها فقط لإعادة التحميل عندما يتغير الكود، وليس عند الطلب، لذا فهي ليست عملية متكررة. في هذه المرحلة، لا داعي للقلق بشأن تأثيرها على الأداء العام للنظام.
بالاقتران مع مشكلة الحواجز الثانية، في OpenResty، نستدعي أحيانًا عمليات I/O للملفات الحاجزة خلال مراحل init
وinit worker
. هذه العملية أكثر إضعافًا للأداء من NYI، ولكن بما أنها تتم مرة واحدة فقط عند بدء الخدمة، فهي مقبولة.
كما هو الحال دائمًا، يجب النظر إلى تحسين الأداء من منظور شامل، وهي نقطة تحتاج إلى الانتباه إليها بشكل خاص. وإلا، بالتركيز على تفصيل معين، من المحتمل أن تقوم بالتحسين لفترة طويلة ولكن دون تحقيق تأثير جيد.
السؤال 4: هل يمكنني تنفيذ upstream الديناميكي بنفسي؟
الوصف: بالنسبة لـ upstream الديناميكي، أسلوبي هو إعداد 2 upstreams لخدمة ما، اختيار upstreams مختلفة وفقًا لشروط التوجيه، وتعديل IP في upstream مباشرة عندما يتغير IP الجهاز. هل هناك أي عيب أو فخ في هذا الأسلوب مقارنة باستخدام
balancer_by_lua
مباشرة؟
ميزة balancer_by_lua
هي أنها تسمح للمستخدم باختيار خوارزمية موازنة الحمل، على سبيل المثال، سواء استخدام roundrobin
أو chash
، أو أي خوارزمية أخرى ينفذها المستخدم، مما يجعلها مرنة وعالية الأداء.
إذا قمت بذلك بطريقة قواعد التوجيه، فإن النتيجة هي نفسها. ولكن فحص صحة upstream يحتاج إلى تنفيذه من قبلك، مما يضيف الكثير من العمل الإضافي.
يمكننا أيضًا توسيع هذا السؤال بالسؤال عن كيفية تنفيذ هذا السيناريو لـ abtest
، الذي يتطلب upstream مختلف.
يمكنك تحديد أي upstream تستخدم في مرحلة balancer_by_lua
بناءً على uri
، host
، parameters
، إلخ. يمكنك أيضًا استخدام بوابات API لتحويل هذه الأحكام إلى قواعد توجيه، وتحديد أي مسار تستخدم في مرحلة access
الأولية، ثم العثور على upstream المحدد من خلال العلاقة الربطية بين المسار وupstream. هذا أسلوب شائع في بوابات API، وسنتحدث عنه بشكل أكثر تحديدًا لاحقًا في قسم التطبيق العملي.
السؤال 5: هل التخزين المؤقت لـ shared dict
إلزامي؟
الوصف:
في التطبيقات الإنتاجية الحقيقية، أعتقد أن طبقة التخزين المؤقت لـ
shared dict
هي ضرورة. يبدو أن الجميع يتذكرون فقط جودةlru cache
، لا قيود على تنسيق البيانات، لا حاجة لإلغاء التسلسل، لا حاجة لحساب مساحة الذاكرة بناءً على حجم k/v، لا تنافس بين العمال، لا أقفال قراءة/كتابة وأداء عالي.ومع ذلك، لا تتجاهل أن أحد أضعف نقاطها القاتلة هو أن دورة حياة
lru cache
تتبعWorker
. كلما تم إعادة تحميل NGINX، سيتم فقدان هذا الجزء من التخزين المؤقت بالكامل، وفي هذه المرحلة، إذا لم يكن هناكshared dict
، فإن مصدر البياناتL3
سيعلق في دقائق.بالطبع، هذا هو الحال في حالات الازدحام العالي، ولكن بما أن التخزين المؤقت مستخدم، فإن حجم الأعمال بالتأكيد ليس صغيرًا، مما يعني أن التحليل المذكور لا يزال ينطبق. إذا كنت على صواب في هذا الرأي؟
في بعض الحالات، صحيح، كما قلت، أن shared dict
لا يتم فقدها أثناء إعادة التحميل، لذا فهي ضرورية. ولكن هناك حالة خاصة حيث يكون lru cache
فقط مقبولًا إذا كانت جميع البيانات متاحة بشكل نشط من مصدر البيانات L3
، في مرحلة init
أو init_worker
.
على سبيل المثال، إذا كانت بوابة API المفتوحة المصدر APISIX لديها مصدر البيانات في etcd
، فإنها تجلب البيانات فقط من etcd
. يتم تخزينها مؤقتًا في lru cache
خلال مرحلة init_worker
، ويتم تحديث التخزين المؤقت لاحقًا بشكل نشط من خلال آلية watch
في etcd
. بهذه الطريقة، حتى إذا تم إعادة تحميل NGINX، لن يكون هناك تدافع في التخزين المؤقت.
لذا، يمكننا أن نفضل اختيار التقنية ولكن لا نعمم بشكل مطلق لأنه لا يوجد حل واحد يناسب جميع سيناريوهات التخزين المؤقت. إنها طريقة ممتازة لبناء حل قابل للاستخدام وفقًا لاحتياجات السيناريو الفعلي ثم زيادته تدريجيًا.