I/O غير المحظور - المفتاح لتحسين أداء OpenResty
API7.ai
December 2, 2022
في فصل تحسين الأداء، سأأخذك عبر جميع جوانب تحسين الأداء في OpenResty وألخص النقاط المذكورة في الفصول السابقة في دليل شامل لكتابة كود OpenResty بحيث يمكنك كتابة كود OpenResty بجودة أفضل.
تحسين الأداء ليس بالأمر السهل. يجب أن نأخذ في الاعتبار تحسين بنية النظام، وتحسين قاعدة البيانات، وتحسين الكود، واختبار الأداء، وتحليل الرسوم البيانية للهب، وغيرها من الخطوات. ولكن من السهل تقليل الأداء، وكما يشير عنوان مقال اليوم، يمكنك تقليل الأداء بعشرة أضعاف أو أكثر بإضافة بضعة أسطر من الكود فقط. إذا كنت تستخدم OpenResty لكتابة الكود الخاص بك، ولكن الأداء لم يتحسن، فمن المحتمل أن يكون ذلك بسبب I/O الحاجز.
لذلك، قبل أن ندخل في تفاصيل تحسين الأداء، دعونا ننظر إلى مبدأ مهم في برمجة OpenResty: I/O غير الحاجز أولاً.
لقد تعلمنا من آبائنا ومعلمينا منذ الصغر ألا نلعب بالنار وألا نلمس القابس، فهذه سلوكيات خطيرة. نفس النوع من السلوكيات الخطيرة موجود في OpenResty. إذا كان عليك استخدام عمليات I/O الحاجزة في الكود الخاص بك، فسيؤدي ذلك إلى انخفاض حاد في الأداء، وسيتم إحباط الغرض الأصلي من استخدام OpenResty لبناء خادم عالي الأداء.
لماذا لا يمكننا استخدام عمليات I/O الحاجزة؟
فهم السلوكيات الخطيرة وتجنبها هو الخطوة الأولى في تحسين الأداء. لنبدأ بمراجعة سبب تأثير عمليات I/O الحاجزة على أداء OpenResty.
يمكن لـ OpenResty الحفاظ على أداء عالٍ ببساطة لأنه يستعير معالجة الأحداث من NGINX والـ coroutine من Lua، لذا:
- عندما تواجه عملية مثل I/O الشبكة التي تتطلب منك الانتظار لعودة قبل الاستمرار، تقوم باستدعاء coroutine Lua
yield
لتعليق نفسك ثم تسجيل رد فعل في NGINX. - بعد اكتمال عملية I/O (أو حدوث مهلة أو خطأ)، يقوم NGINX باستدعاء
resume
لإيقاظ coroutine Lua.
هذه العملية تضمن أن OpenResty يمكنه دائمًا استخدام موارد وحدة المعالجة المركزية بكفاءة لمعالجة جميع الطلبات.
في هذا التدفق للمعالجة، لا يعطي LuaJIT السيطرة لحلقة أحداث NGINX إذا لم يستخدم طريقة I/O غير الحاجزة مثل cosocket
، ولكن بدلاً من ذلك يستخدم دالة I/O حاجزة للتعامل مع I/O. هذا يؤدي إلى انتظار الطلبات الأخرى في الطابور حتى تنتهي معالجة حدث I/O الحاجز قبل أن تحصل على استجابة.
باختصار، في برمجة OpenResty، يجب أن نكون حذرين بشكل خاص من استدعاءات الدوال التي قد تحجب I/O؛ وإلا، فإن سطرًا واحدًا من كود I/O الحاجز يمكن أن يقلل من أداء الخدمة بأكملها.
فيما يلي، سأقدم بعض المشاكل الشائعة، وبعض دوال I/O الحاجزة التي يتم إساءة استخدامها بشكل متكرر؛ دعونا أيضًا نختبر كيفية استخدام أسهل طريقة "لإفساد" الأداء وجعل أداء خدمتك ينخفض بسرعة بعشرة أضعاف.
تنفيذ الأوامر الخارجية
في العديد من السيناريوهات، لا يستخدم المطورون OpenResty فقط كخادم ويب، بل يمنحونه المزيد من منطق الأعمال. في هذه الحالة، قد يكون من الضروري استدعاء الأوامر والأدوات الخارجية للمساعدة في إكمال بعض العمليات.
على سبيل المثال، لقتل عملية.
os.execute("kill -HUP " .. pid)
أو للعمليات الأكثر استهلاكًا للوقت مثل نسخ الملفات، استخدام OpenSSL لإنشاء المفاتيح، إلخ.
os.execute(" cp test.exe /tmp ")
os.execute(" openssl genrsa -des3 -out private.pem 2048 ")
على السطح، os.execute
هي دالة مدمجة في Lua، وفي عالم Lua، هي بالفعل الطريقة لاستدعاء الأوامر الخارجية. ومع ذلك، من المهم أن نتذكر أن Lua هي لغة برمجة مدمجة وستكون لها استخدامات موصى بها مختلفة في سياقات أخرى.
في بيئة OpenResty، os.execute
تحجب الطلب الحالي. لذا، إذا كان وقت تنفيذ هذا الأمر قصيرًا جدًا، فإن التأثير ليس كبيرًا. ولكن إذا استغرق الأمر مئات الميلي ثوانٍ أو حتى ثوانٍ للتنفيذ، فسيكون هناك انخفاض حاد في الأداء.
نفهم المشكلة، فكيف يجب أن نحلها؟ بشكل عام، هناك حلان.
1. إذا كانت هناك مكتبة FFI
متاحة، فإننا نفضل استخدام طريقة FFI لاستدعائها
على سبيل المثال، إذا استخدمنا سطر أوامر OpenSSL لإنشاء المفتاح أعلاه، يمكننا تغييره لاستخدام FFI
لاستدعاء دالة OpenSSL C لتجاوزه.
لقتل عملية، يمكنك استخدام lua-resty-signal
، وهي مكتبة تأتي مع OpenResty، لحلها بشكل غير حاجز. تنفيذ الكود كما يلي. بالطبع، هنا، lua-resty-signal
يتم حلها أيضًا باستخدام FFI
لاستدعاء دوال النظام.
local resty_signal = require "resty.signal"
local pid = 12345
local ok, err = resty_signal.kill(pid, "KILL")
بالإضافة إلى ذلك، يحتوي موقع LuaJIT الرسمي على صفحة خاصة تعرض مكتبات FFI المربوطة في فئات مختلفة. على سبيل المثال، عند التعامل مع الصور، تشفير وفك تشفير العمليات المكثفة لوحدة المعالجة المركزية، يمكنك الذهاب إلى هناك أولاً لمعرفة ما إذا كانت هناك مكتبات تم تغليفها ويمكن استخدامها مباشرة.
2. استخدام مكتبة lua-resty-shell
المبنية على ngx.pipe
كما تم وصفه سابقًا، يمكنك تشغيل أوامرك في shell.run
، وهي عملية I/O غير حاجزة.
$ resty -e 'local shell = require "resty.shell"
local ok, stdout, stderr, reason, status =
shell.run([[echo "hello, world"]])
ngx.say(stdout) '
I/O القرص
لننظر إلى سيناريو التعامل مع I/O القرص. في تطبيق الخادم، من العمليات الشائعة قراءة ملف تكوين محلي، مثل الكود التالي.
local path = "/conf/apisix.conf"
local file = io.open(path, "rb")
local content = file:read("*a")
file:close()
يستخدم هذا الكود io.open
للحصول على محتويات ملف معين. ومع ذلك، على الرغم من أنها عملية I/O حاجزة، لا تنسَ أنه يجب النظر إلى الأشياء في سياق واقعي. لذا إذا قمت باستدعائها في init
و init worker
، فهي عملية لمرة واحدة لا تؤثر على أي طلبات عملاء وهي مقبولة تمامًا.
بالطبع، تصبح غير مقبولة إذا كان كل طلب مستخدم يؤدي إلى قراءة أو كتابة على القرص. في تلك الحالة، تحتاج إلى التفكير بجدية في الحل.
أولاً، يمكننا استخدام lua-io-nginx-module
، وهو وحدة C تابعة لجهة خارجية. يوفر واجهة برمجة تطبيقات Lua لـ I/O غير الحاجز لـ OpenResty، ولكن لا يمكنك استخدامها كما تحب مع cosocket
. لأن استهلاك I/O القرص لا يختفي بدون سبب، إنها مجرد طريقة مختلفة للقيام بالأشياء.
تعمل هذه الطريقة لأن lua-io-nginx-module
يستفيد من تجميع خيوط NGINX لنقل عمليات I/O القرص من الخيط الرئيسي إلى خيط آخر لمعالجتها بحيث لا يتم حجب الخيط الرئيسي بعمليات I/O القرص.
تحتاج إلى إعادة تجميع NGINX عند استخدام هذه المكتبة لأنها وحدة C. يتم استخدامها بنفس طريقة مكتبة I/O في Lua.
local ngx_io = require "ngx.io"
local path = "/conf/apisix.conf"
local file, err = ngx_io.open(path, "rb")
local data, err = file: read("*a")
file:close()
ثانيًا، حاول تعديلًا معماريًا. هل يمكننا تغيير طريقتنا لهذا النوع من I/O القرص والتوقف عن القراءة والكتابة على الأقراص المحلية؟
دعني أعطيك مثالًا حتى تتعلم بالقياس. قبل سنوات، كنت أعمل على مشروع يتطلب تسجيل الدخول على قرص محلي لأغراض إحصائية واستكشاف الأخطاء وإصلاحها.
في ذلك الوقت، استخدم المطورون ngx.log
لكتابة هذه السجلات، مثل التالي.
ngx.log(ngx.WARN, "info")
هذا السطر من الكود يستدعي واجهة برمجة تطبيقات Lua المقدمة من OpenResty، ويبدو أنه لا توجد مشاكل. العيب، مع ذلك، هو أنك لا يمكنك استدعاؤه بشكل متكرر. أولاً، ngx.log
نفسها هي استدعاء دالة مكلفة؛ ثانيًا، حتى مع وجود مخزن مؤقت، فإن الكتابة الكبيرة والمتكررة على القرص يمكن أن تؤثر بشكل خطير على الأداء.
إذن كيف نحلها؟ دعنا نعود إلى الحاجة الأصلية - الإحصاءات، استكشاف الأخطاء وإصلاحها، وكتابة السجلات على القرص المحلي كانت مجرد واحدة من الوسائل لتحقيق الهدف.
لذا يمكنك أيضًا إرسال السجلات إلى خادم تسجيل بعيد لاستخدام cosocket
للقيام باتصال شبكة غير حاجز؛ أي إلقاء I/O القرص الحاجز على خدمة التسجيل لتجنب حجب الخدمة الخارجية. يمكنك استخدام lua-resty-logger-socket
للقيام بذلك.
local logger = require "resty.logger.socket"
if not logger.initted() then
local ok, err = logger.init{
host = 'xxx',
port = 1234,
flush_limit = 1234,
drop_limit = 5678,
}
local msg = "foo"
local bytes, err = logger.log(msg)
كما يجب أن تكون قد لاحظت، كلتا الطريقتين أعلاه هما نفس الشيء: إذا كان I/O الحاجز لا مفر منه، فلا تحجب الخيط العامل الرئيسي؛ ألقِه على خيوط أخرى أو خدمات خارجية.
luasocket
أخيرًا، دعونا نتحدث عن luasocket
، وهي مكتبة مدمجة في Lua يستخدمها المطورون بسهولة وغالبًا ما يتم الخلط بينها وبين cosocket
المقدمة من OpenResty. luasocket
يمكنها أيضًا أداء وظائف الاتصال بالشبكة. ولكنها لا تتمتع بميزة عدم الحجب. نتيجة لذلك، إذا استخدمت luasocket
، ينخفض الأداء بشكل كبير.
ومع ذلك، فإن luasocket لديها أيضًا سيناريوهات استخدام فريدة. على سبيل المثال، لا أعرف إذا كنت تتذكر أن cosocket
غير متاحة في عدة مراحل، ويمكننا عادة تجاوزها باستخدام ngx.timer
. أيضًا، يمكنك استخدام luasocket
لوظائف cosocket
في مراحل لمرة واحدة مثل init_by_lua*
و init_worker_by_lua*
. كلما كنت أكثر دراية بالتشابهات والاختلافات بين OpenResty وLua، ستجد المزيد من الحلول المثيرة للاهتمام مثل هذه.
بالإضافة إلى ذلك، lua-resty-socket
هو تغليف ثانوي لمكتبة مفتوحة المصدر تجعل luasocket و cosocket` متوافقة. هذا المحتوى يستحق أيضًا الدراسة الإضافية. إذا كنت لا تزال مهتمًا، فقد أعددت مواد لك لمواصلة التعلم.
الخلاصة
بشكل عام، في OpenResty، التعرف على أنواع عمليات I/O الحاجزة وحلولها هو أساس تحسين الأداء الجيد. لذا، هل واجهت عمليات I/O حاجزة مماثلة في التطوير الفعلي؟ كيف تجدها وتحلها؟ لا تتردد في مشاركة تجربتك معي في التعليقات، ولا تتردد في مشاركة هذه المقالة.