ما وراء خادم الويب: العمليات المميزة ومهام المؤقت

API7.ai

November 3, 2022

OpenResty (NGINX + Lua)

في المقال السابق، قدمنا واجهات برمجة OpenResty، shared dict، و cosocket، وكلها تنفذ وظائف ضمن نطاق NGINX وخوادم الويب، مما يوفر خادم ويب قابل للبرمجة بتكلفة أقل وأسهل في الصيانة.

ومع ذلك، يمكن لـ OpenResty أن تفعل أكثر من ذلك. دعونا نختار بعض الميزات في OpenResty التي تتجاوز خادم الويب ونقدمها اليوم. وهي مهام المؤقت، العملية المميزة، و ngx.pipe غير المعوق.

مهام المؤقت

في OpenResty، نحتاج أحيانًا إلى تنفيذ مهام محددة بانتظام في الخلفية، مثل مزامنة البيانات، تنظيف السجلات، إلخ. إذا كنت ستقوم بتصميم ذلك، كيف ستفعل ذلك؟ أسهل طريقة يمكن التفكير فيها هي توفير واجهة برمجة تطبيقات (API) للخارج لتنفيذ هذه المهام، ثم استخدام crontab الخاص بالنظام لاستدعاء curl على فترات منتظمة للوصول إلى هذه الواجهة، وبالتالي تنفيذ هذا المطلب بطريقة غير مباشرة.

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

يمكن تقسيم مهام المؤقت في OpenResty إلى النوعين التاليين.

  • ngx.timer.at يستخدم لتنفيذ مهام المؤقت لمرة واحدة.
  • ngx.timer.every يستخدم لتنفيذ مهام المؤقت ذات الفترة الثابتة.

هل تتذكر السؤال المثير للتفكير الذي تركته في نهاية المقال السابق؟ كان السؤال هو كيفية كسر القيد الذي يمنع استخدام cosocket في init_worker_by_lua، والإجابة هي ngx.timer.

الكود التالي يبدأ مهمة مؤقت بتأخير 0. يبدأ وظيفة رد الاتصال handler، وفي هذه الوظيفة، يستخدم cosocket للوصول إلى موقع ويب.

init_worker_by_lua_block {
    local function handler()
        local sock = ngx.socket.tcp()
        local ok, err = sock:connect(“api7.ai", 80)
    end

    local ok, err = ngx.timer.at(0, handler)
}

بهذه الطريقة، نتجاوز القيد الذي يمنع استخدام cosocket في هذه المرحلة.

بالعودة إلى متطلبات المستخدم التي ذكرناها في بداية هذا القسم، ngx.timer.at لا يعالج الحاجة إلى التشغيل الدوري؛ في مثال الكود أعلاه، إنها مهمة لمرة واحدة.

إذن، كيف نفعل ذلك بشكل دوري؟ يبدو أن لديك خيارين بناءً على واجهة برمجة التطبيقات ngx.timer.at.

  • يمكنك تنفيذ المهمة الدورية بنفسك باستخدام حلقة لا نهائية while true في وظيفة رد الاتصال التي sleep لفترة بعد تنفيذ المهمة.
  • يمكنك أيضًا إنشاء مؤقت جديد في نهاية وظيفة رد الاتصال.

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

لذلك، الحل الأول باستخدام while true لتنفيذ المهام الدورية غير موثوق. الحل الثاني ممكن ولكنه ينشئ مؤقتات بشكل متكرر، مما يجعل فهمه صعبًا.

إذن، هل هناك حل أفضل؟ واجهة برمجة التطبيقات الجديدة ngx.timer.every خلف OpenResty مصممة خصيصًا لحل هذه المشكلة، وهي حل أقرب إلى crontab.

الجانب السلبي هو أنه لا توجد فرصة لإلغاء مهمة المؤقت بعد بدئها. بعد كل شيء، ngx.timer.cancel لا تزال وظيفة قيد التنفيذ.

في هذه المرحلة، ستواجه مشكلة: المؤقت يعمل في الخلفية ولا يمكن إلغاؤه؛ إذا كان هناك العديد من المؤقتات، فمن السهل استنفاد موارد النظام.

لذلك، يوفر OpenResty توجيهين، lua_max_pending_timers و lua_max_running_timers للحد منها. الأول يمثل الحد الأقصى لعدد المؤقتات التي تنتظر التنفيذ، والثاني يمثل الحد الأقصى لعدد المؤقتات التي تعمل حاليًا.

يمكنك أيضًا استخدام واجهة برمجة التطبيقات Lua للحصول على قيم مهام المؤقت التي تنتظر وتعمل حاليًا، كما هو موضح في المثالين التاليين.

content_by_lua_block {
    ngx.timer.at(3, function() end)
    ngx.say(ngx.timer.pending_count())
}

سيطبع هذا الكود 1، مما يشير إلى وجود مهمة مجدولة واحدة تنتظر التنفيذ.

content_by_lua_block {
    ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
    ngx.sleep(0.2)
    ngx.say(ngx.timer.running_count())
}

سيطبع هذا الكود 1، مما يشير إلى وجود مهمة مجدولة واحدة تعمل.

العملية المميزة

بعد ذلك، دعونا نلقي نظرة على العملية المميزة. كما نعلم جميعًا، يتم تقسيم NGINX إلى عملية Master وعمليات Worker، حيث تعالج عمليات العامل طلبات المستخدم. يمكننا الحصول على نوع العملية من خلال واجهة برمجة التطبيقات process.type المقدمة في lua-resty-core. على سبيل المثال، يمكنك استخدام resty لتشغيل الوظيفة التالية.

$ resty -e 'local process = require "ngx.process"
ngx.say("process type:", process.type())'

سترى أنه يعيد نتيجة single بدلاً من worker، مما يعني أن resty يبدأ NGINX بعملية Worker، وليس عملية Master. هذا صحيح. في تنفيذ resty، يمكنك أن ترى أن عملية Master يتم إيقافها بسطر مثل هذا.

master_process off;

يقوم OpenResty بتوسيع NGINX بإضافة privileged agent، العملية المميزة لها الميزات الخاصة التالية.

  • لا تراقب أي منافذ، مما يعني أنها لا تقدم خدمات للخارج.

  • لديها نفس الصلاحيات مثل عملية Master، والتي تكون عادة صلاحيات المستخدم root، مما يسمح لها بالقيام بالعديد من المهام التي يستحيل على عملية Worker القيام بها.

  • يمكن فتح العملية المميزة فقط في سياق init_by_lua.

  • أيضًا، العملية المميزة تكون ذات معنى فقط إذا كانت تعمل في سياق init_worker_by_lua لأنه لا يتم تحريك أي طلبات، ولا تذهب إلى سياقات content، access، إلخ.

دعونا نلقي نظرة على مثال لعملية مميزة يتم تشغيلها.

init_by_lua_block {
    local process = require "ngx.process"

    local ok, err = process.enable_privileged_agent()
    if not ok then
        ngx.log(ngx.ERR, "enables privileged agent failed error:", err)
    end
}

بعد فتح العملية المميزة بهذا الكود وبدء خدمة OpenResty، يمكننا أن نرى أن العملية المميزة أصبحت الآن جزءًا من عملية NGINX.

nginx: master process
nginx: worker process
nginx: privileged agent process

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

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

init_worker_by_lua_block {
    local process = require "ngx.process"

    local function reload(premature)
        local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
        if not f then
            return
        end
        local pid = f:read()
        f:close()
        os.execute("kill -HUP " .. pid)
    end

    if process.type() == "privileged agent" then
         local ok, err = ngx.timer.every(5, reload)
        if not ok then
            ngx.log(ngx.ERR, err)
        end
    end
}

ينفذ الكود أعلاه القدرة على إرسال إشارات HUP إلى عملية master كل 5 ثوانٍ. بشكل طبيعي، يمكنك البناء على هذا للقيام بأشياء أكثر إثارة، مثل استطلاع قاعدة البيانات لمعرفة ما إذا كانت هناك مهام للعملية المميزة وتنفيذها. نظرًا لأن العملية المميزة لديها صلاحيات root، فإن هذا يعتبر نوعًا من "برنامج الباب الخلفي".

ngx.pipe غير المعوق

أخيرًا، دعونا نلقي نظرة على ngx.pipe غير المعوق، الذي يستخدم مكتبة Lua القياسية لتنفيذ أمر خارجي يرسل إشارة إلى عملية Master في مثال الكود الذي وصفناه للتو.

os.execute("kill -HUP " .. pid)

بشكل طبيعي، هذه العملية ستكون معوقة. إذن، هل هناك طريقة غير معوقة لاستدعاء البرامج الخارجية في OpenResty؟ بعد كل شيء، أنت تعلم أنه إذا كنت تستخدم OpenResty كمنصة تطوير كاملة وليس كخادم ويب، فهذا ما تحتاجه. لهذا السبب، تم إنشاء مكتبة lua-resty-shell، واستخدامها لاستدعاء سطر الأوامر يكون غير معوق:

$ resty -e 'local shell = require "resty.shell"
local ok, stdout, stderr, reason, status =
    shell.run([[echo "hello, world"]])
    ngx.say(stdout)

هذا الكود هو طريقة مختلفة لكتابة hello world، باستدعاء أمر echo الخاص بالنظام لإكمال الإخراج. وبالمثل، يمكنك استخدام resty.shell كبديل لاستدعاء os.execute في Lua.

نعلم أن التنفيذ الأساسي لـ lua-resty-shell يعتمد على واجهة برمجة التطبيقات ngx.pipe في lua-resty-core، لذلك يستخدم هذا المثال lua-resty-shell لطباعة hello world، باستخدام ngx.pipe بدلاً من ذلك، سيبدو هكذا.

$ resty -e 'local ngx_pipe = require "ngx.pipe"
local proc = ngx_pipe.spawn({"echo", "hello world"})
local data, err = proc:stdout_read_line()
ngx.say(data)'

أعلاه هو الكود الأساسي لتنفيذ lua-resty-shell. يمكنك الاطلاع على وثائق ngx.pipe واختباراتها للحصول على مزيد من المعلومات حول كيفية استخدامها. لذلك، لن أتطرق إليها هنا.

الخلاصة

هذا كل شيء. لقد انتهينا من المحتوى الرئيسي لليوم. من الميزات أعلاه، يمكننا أن نرى أن OpenResty تحاول أيضًا التحرك نحو اتجاه منصة عامة أثناء جعل NGINX أفضل، على أمل أن يتمكن المطورون من محاولة توحيد مكدس التكنولوجيا واستخدام OpenResty لحل احتياجات التطوير الخاصة بهم. هذا ودود للغاية للتشغيل والصيانة لأن تكاليف الصيانة تكون أقل طالما قمت بنشر OpenResty عليها.

أخيرًا، سأترك لكم سؤالًا مثيرًا للتفكير. نظرًا لوجود العديد من عمليات Worker في NGINX، فإن timer سيعمل مرة واحدة لكل Worker، وهو أمر غير مقبول في معظم السيناريوهات. كيف يمكننا التأكد من أن timer يعمل مرة واحدة فقط؟

لا تتردد في ترك تعليق بحلولك، ولا تتردد في مشاركة هذه المقالة مع زملائك وأصدقائك حتى نتمكن من التواصل والتحسين معًا.