nginx
بنية تطبيقات المصادر المفتوحة (المجلد 2)nginx
بنية تطبيقات المصادر المفتوحة (المجلد 2) nginx
Andrew Alexeev
إذا استمتعت بهذه الكتب، فربما يعجبك أيضًا التصميم البرمجي بالأمثلة في Python والتصميم البرمجي بالأمثلة في JavaScript.
nginx (يُنطق «engine x») هو خادم ويب مفتوح المصدر ومجاني كتبه Igor Sysoev، وهو مهندس برمجيات روسي. منذ إطلاقه للعامة عام 2004، ركّز nginx على الأداء العالي والتزامن العالي (high concurrency) وانخفاض استهلاك الذاكرة. وقد ساعدت الميزات الإضافية فوق وظائف خادم الويب الأساسية، مثل موازنة الحمل، والتخزين المؤقت (caching)، والتحكم في الوصول والنطاق الترددي، والقدرة على التكامل بكفاءة مع مجموعة متنوعة من التطبيقات، في جعل nginx خيارًا جيدًا لمعماريات المواقع الحديثة. ويُعدّ nginx اليوم ثاني أكثر خوادم الويب مفتوحة المصدر شعبية على الإنترنت.
14.1. لماذا يهمّ التزامن العالي؟
صار الإنترنت في هذه الأيام منتشرًا إلى حدّ بعيد وحاضرًا في كل مكان، بحيث يصعب تخيّل أنه لم يكن موجودًا على النحو الذي نعرفه اليوم قبل عقد من الزمان. لقد تطوّر بدرجة كبيرة، من HTML بسيط كان يُنتج نصًا قابلًا للنقر، استنادًا إلى خوادم NCSA ثم إلى خوادم Apache، إلى وسيط اتصال دائم التشغيل يستخدمه أكثر من ملياري مستخدم حول العالم. ومع انتشار أجهزة الحاسب المكتبي المتصلة دائمًا والأجهزة المحمولة، ثم الأجهزة اللوحية في الآونة الأخيرة، يتغيّر مشهد الإنترنت بسرعة واتصلت اقتصاديات كاملة رقميًا. وأصبحت الخدمات التي تُقدَّم عبر الإنترنت أعقد بكثير، مع ميل واضح نحو المعلومات والترفيه المباشرين المتاحين فورًا. كما تغيّرت الجوانب الأمنية لتشغيل الأعمال عبر الإنترنت تغيّرًا ملحوظًا. وبناءً على ذلك، أصبحت المواقع اليوم أعقد بكثير من السابق، وتحتاج عمومًا إلى جهد هندسي أكبر بكثير كي تكون متينة وقابلة للتوسّع.
كان التزامن (concurrency) أحد أكبر التحديات التي واجهت مهندسي المواقع على الدوام. فمنذ بدايات خدمات الويب، ظلّ مستوى التزامن ينمو بشكل مستمر. وليس من غير المألوف أن يخدم موقع شهير مئات الآلاف بل ملايين المستخدمين في الوقت نفسه. قبل عقد من الزمان، كان السبب الرئيسي للتزامن هو العملاء البطيئون — المستخدمون ذوو اتصالات ADSL أو الطلب الهاتفي. أما اليوم، فينشأ التزامن من مزيج من عملاء الهاتف المحمول وبنى التطبيقات الأحدث، التي تستند عادةً إلى الحفاظ على اتصال دائم يسمح بتحديث العميل بالأخبار والتغريدات وتغذيات الأصدقاء وغيرها. وثمة عامل مهم آخر يسهم في زيادة التزامن، هو تغيّر سلوك المتصفحات الحديثة التي تفتح من أربع إلى ست اتصالات متزامنة مع الموقع الواحد لتحسين سرعة تحميل الصفحة.
ولتوضيح مشكلة العملاء البطيئين، تخيّل خادم ويب بسيطًا قائمًا على Apache ينتج استجابة قصيرة نسبيًا بحجم 100 كيلوبايت — صفحة ويب نصية أو صورة. قد يستغرق توليد هذه الصفحة أو استرجاعها جزءًا من الثانية فحسب، لكن إرسالها إلى عميل يملك نطاقًا تردديًا قدره 80 كيلوبت في الثانية (10 كيلوبايت/ثانية) يستغرق 10 ثوانٍ. على نحو جوهري، كان خادم الويب يسحب محتوى بحجم 100 كيلوبايت بسرعة معقولة، ثم يظل مشغولًا لمدة 10 ثوانٍ وهو يرسل هذا المحتوى ببطء إلى العميل قبل تحرير اتصاله. الآن تخيّل لديك 1000 عميل متصل في الوقت نفسه وقد طلبوا محتوى مماثلًا. ولو خُصِّص للعميل الواحد ميغابايت واحد فقط من الذاكرة الإضافية، لكانت النتيجة 1000 ميغابايت (نحو 1 غيغابايت) من الذاكرة الزائدة مخصَّصة لخدمة 1000 عميل فحسب بمحتوى حجمه 100 كيلوبايت. وفي الواقع، يخصص خادم ويب نموذجي قائم على Apache عادةً أكثر من ميغابايت واحد من الذاكرة الإضافية لكل اتصال، ومن المؤسف أن عشرات الكيلوبتات في الثانية ما زالت غالبًا السرعة الفعلية للاتصالات المحمولة. ورغم أن إرسال المحتوى إلى عميل بطيء قد يتحسن إلى حدٍّ ما عبر زيادة حجم مخازن المقابس (socket buffers) في نواة نظام التشغيل، فإن ذلك ليس حلًا عامًا للمشكلة وقد يترتب عليه آثار جانبية غير مرغوبة.
مع الاتصالات الدائمة تصبح مشكلة معالجة التزامن أكثر وضوحًا، لأنه تفاديًا للزمنية المرتبطة بإنشاء اتصالات HTTP جديدة يبقى العملاء متصلين، ولكل عميل متصل تخصّص خادم الويب قدرًا معينًا من الذاكرة.
وبالتالي، ولمواجهة أحمال العمل المتزايدة الناتجة عن نمو الجماهير وبالتالي مستويات التزامن الأعلى — والقدرة على مواصلة ذلك باستمرار — ينبغي أن يستند الموقع إلى عدد من لبنات البناء شديدة الكفاءة. فبينما تُعدّ الأجزاء الأخرى من المعادلة، مثل الأجهزة (وحدة المعالجة المركزية والذاكرة والأقراص)، وسعة الشبكة، وبنى التطبيقات وتخزين البيانات واضحة الأهمية، فإن قبول اتصالات العملاء ومعالجتها يحدث في برمجية خادم الويب. لذا ينبغي أن يكون خادم الويب قادرًا على التوسّع لا خطيًا مع النمو في عدد الاتصالات المتزامنة وفي عدد الطلبات في الثانية.
ألا يناسب Apache؟
ترجع جذور Apache، تلك البرمجية التي ما زالت تسيطر على الإنترنت إلى حدٍّ كبير اليوم، إلى مطلع تسعينيات القرن الماضي.أصلًا، كانت بنيته تطابق أنظمة التشغيل والأجهزة القائمة في حينه، وكذلك حالة الإنترنت آذاك، حين كان الموقع الإلكتروني عادةً خادمًا ماديًا مستقلًا يشغّل نسخة واحدة من Apache. وفي مطلع الألفية الثالثة، صار واضحًا أن نموذج خادم الويب المستقل لا يمكن تكراره بسهولة لتلبية خدمات الويب المتنامية. ورغم أن Apache وفّر أساسًا متينًا للتطوير المستقبلي، فقد صُمّم ليُنشئ نسخة منه لكل اتصال جديد، وهو ما لم يكن مناسبًا للتوسّع غير الخطي للموقع. وفي النهاية، أصبح Apache خادم ويب للأغراض العامة يركّز على امتلاك مجموعة كبيرة من الميزات المختلفة، وتنوّع من إضافات الأطراف الثالثة، وانطباق شامل عمليًا على كل أنواع تطوير تطبيقات الويب. غير أن لا شيء يأتي بلا ثمن، وثمن هذه التركيبة الغنية والشاملة من الأدوات في قطعة برمجية واحدة هو ضعف قابلية التوسّع بسبب ازدياد استهلاك المعالج والذاكرة لكل اتصال.
وعليه، حين لم تعد أجهزة الخوادم وأنظمة التشغيل وموارد الشبكة قيودًا رئيسية على نمو المواقع، بدأ مطوّرو الويب في أنحاء العالم يبحثون عن وسيلة أكفأ لتشغيل خوادم الويب. قبل نحو عشر سنوات، أعلن Daniel Kegel، وهو مهندس برمجيات بارز، أعلن أنّ «حان وقت خوادم الويب للتعامل مع عشرة آلاف عميل في الوقت نفسه»، وتوقّع ما نسمّيه اليوم بخدمات السحابة على الإنترنت. وقد يحّزت مطالبته C10K عددًا من المحاولات لحل مشكلة تحسين خادم الويب للتعامل مع عدد كبير من العملاء في الوقت نفسه، وتبيّن أن nginx كان أحد أكثر هذه المحاولات نجاحًا.
وُضع nginx بقصد حل مشكلة C10K المتمثلة في 10,000 اتصال متزامن، وكُتب ببنية مختلفة تمامًا — بنية أكثر ملاءمة بدرجة بعيدة للتوسّع غير الخطي، سواء في عدد الاتصالات المتزامنة أو في عدد الطلبات في الثانية. nginx قائم على الأحداث (event-based)، لذا لا يتبع أسلوب Apache في إنشاء عمليات أو خيوط جديدة لكل طلب صفحة ويب. والنتيجة النهائية هي أن استهلاك الذاكرة والمعالج يبقى قابلاً للإدارة حتى مع زيادة الحمل. ويستطيع nginx اليوم تقديم عشرات الآلاف من الاتصالات المتزامنة على خادم ذو أجهزة نموذجية.
عند إصدار الإصدار الأول من nginx كان المقصود نشره إلى جانب Apache بحيث تتولى nginx المحتوى الساكن مثل HTML وCSS وJavaScript والصور، مما يخفّض حمل التزامن والمعالجة الزمنية (latency) عن خوادم التطبيقات القائمة على Apache. وعلى مدار تطوره، أضاف nginx التكامل مع التطبيقات عبر بروتوكولات FastCGI وuswgi وSCGI، ومع أنظمة تخزين كائنات في الذاكرة الموزّعة مثل memcached. كما أُضيفت وظائف أخرى مفيدة مثل الوكيل العكسي (reverse proxy) مع موازنة الحمل والتخزين المؤقت. وقد شكّلت هذه الميزات الإضافية من nginx مجموعة أدوات فعّالة لبناء بنية تحتية ويب قابلة للتوسّع.
وفي فبراير 2012، أُطلق فرع Apache 2.4.x للعامة. ورغم أن هذا الإصدار الأحدث من Apache أضاف وحدات نواة جديدة متعددة المعالجة وووموات وكيل جديدة تهدف إلى تحسين قابلية التوسّع والأداء، إلا أنه مبكر جدًا للحكم على ما إذا كان أداءه وتزامنه واستخدامه للموارد قد صار الآن مماثلًا أو أفضل من خوادم الويب القائمة كليًا على الأحداث. وسيكون من المفيد جدًا أن نرى خوادم تطبيقات Apache تتوسّع بشكل أفضل مع الإصدار الجديد، إذ قد يخفّف ذلك من الاختناقات في جهة الخلفية (backend) التي كثيرًا ما تبقى بلا حل في الإعدادات النموذجية التي تجمع nginx مع Apache.
هل هناك مزايا إضافية لاستخدام nginx؟
كانت معالجة التزامن العالي بأداء وكفاءة عاليين هي الفائدة الأساسية دائمًا من نشر nginx. غير أن هناك اليوم مزايا أكثر إثارة للاهتمام.
خلال السنوات الأخيرة، تبنّى مهندسو المواقع فكرة فصل البنية التحتية لتطبيقاتهم عن خادم الويب. غير أن ما كان سابقًا يوجد على هيئة موقع قائم على LAMP (Linux وApache وMySQL وPHP أو Python أو Perl) قد لا يصبح اليوم موقعًا قائمًا على LEMP فحسب (E' اختصارًا لـ Engine x')، بل يصير أكثر فأكثر تمرينًا في دفع خادم الويب إلى حافة البنية التحتية، ودمج المجموعة نفسها من التطبيقات وأدوات قواعد البيانات، أو نسخة منقّحة منها، حوله بطريقة مختلفة.
nginx ملائم جدًا لهذا الغرض، لأنه يوفّر الميزات الأساسية اللازمة لتخفيف التزامن ومعالجة الزمنية (latency) وطبقة المقابس الآمنة (SSL — secure sockets layer) والمحتوى الساكن والضغط والتخزين المؤقت، وتنظيم الاتصالات والطلبات، وحتى بث الوسائط عبر HTTP من طبقة التطبيق إلى طبقة خادم ويب حافة أكثر كفاءة بكثير. كما يتيح التكامل مباشرةً مع memcached/Redis أو حلول «NoSQL» الأخرى، لرفع الأداء عند خدمة عدد كبير من المستخدمين المتزامنين.
ومع انتشار حِزَم أدوات التطوير ولغات البرمجة الحديثة على نطاق واسع، بات عدد متزايد من الشركات يغيّر عاداتها في تطوير التطبيقات ونشرها. وأصبح nginx أحد أهم مكوّنات هذه النماذج المتغيّرة، وقد ساعد بالفعل شركات عديدة على إطلاق خدمات الويب وتطويرها بسرعة وضمن ميزانياتها.
كُتبت السطور الأولى من nginx عام 2002. وفي عام 2004 أُطلق للعامة بموجب رخصة BSD من بندين. ولم يتوقف عدد مستخدمي nginx عن النمو منذ ذلك الحين، وهم يساهمون بالأفكار وببلاغات الأخطاء والاقتراحات والملاحظات التي كانت ذات فائدة بالغة للمجتمع بأسره.
الشيفرة الأساسية لـ nginx أصلية وكُتبت بالكامل من الصفر بلغة C. وقد نُقلت nginx إلى معماريات وأنظمة تشغيل عديدة، منها Linux وFreeBSD وSolaris وMac OS X وAIX وMicrosoft Windows. تمتلك nginx مكتباتها الخاصة، ومع وحداتها القياسية لا تتجاوز اعتمادها مكتبة C الخاصة بالنظام سوى zlib وPCRE وOpenSSL، ويمكن استثناءها اختياريًا من عملية البناء إن لم تكن مطلوبة أو بسبب احتمال تعارض التراخيص.
بضع كلمات عن إصدار Windows من nginx. فرغم أن nginx تعمل في بيئة Windows، فإن إصدار Windows منها أقرب إلى إثبات جدوى (proof-of-concept) منه إلى منفذ مكتمل الوظائف. وهناك قيود معينة في بنيات كل من nginx ونواة Windows لا تتفاعل جيدًا في الوقت الحالي. وتشمل المشكلات المعروفة لإصدار nginx على Windows عددًا أقل بكثير من الاتصالات المتزامنة، وانخفاضًا في الأداء، وغياب التخزين المؤقت، وغياب تنظيم النطاق الترددي. وستطابق الإصدارات المستقبلية من nginx على Windows وظائف الإصدار الرئيسي عن قرب.
14.2. نظرة عامة على بنية nginx
تنطوي النماذج التقليدية القائمة على العمليات أو الخيوط لمعالجة الاتصالات المتزامنة على معالجة كل اتصال عبر عملية أو خيط منفصل، مع التوقف الانحصاري (blocking) في انتظار عمليات الشبكة أو الإدخال/الإخراج. وحسب التطبيق، قد تكون هذه الطريقة شديدة عدم الكفاءة من حيث استهلاك الذاكرة والمعالج. فتشغيل عملية أو خيط منفصل يتطلب تجهيز بيئة تشغيل جديدة، بما في ذلك تخصيص ذاكرة الكتلة (heap) والمكدس (stack)، وإنشاء سياق تنفيذ جديد. ويُنفق وقت إضافي من المعالج أيضًا في إنشاء هذه العناصر، ما قد يؤدي في النهاية إلى أداء ضعيف بسبب «تداخل الخيوط» (thread thrashing) الناتج عن تبديل السياق بكثرة. وتتجلى كل هذه التعقيدات في بنيات خوادم الويب الأقدم مثل بنية Apache. وهذه مقايضة بين توفير مجموعة غنية من الميزات قابلة للتطبيق على نطاق عام، من جهة، والاستخدام الأمثل لموارد الخادم من جهة أخرى.
ومنذ البداية الأولى قُصد بـ nginx أن يكون أداة متخصصة لتحقيق أداء أعلى وكثافة أكبر واقتصاد في استخدام موارد الخادم، مع إتاحة النمو الديناميكي للموقع، ولذلك اتبع نموذجًا مختلفًا. فقد استمد في الواقع من التطور الجاري لآليات متقدمة قائمة على الأحداث في مجموعة متنوعة من أنظمة التشغيل. وقد نتج عن ذلك بنية معيارية وقائمة على الأحداث وغير متزامنة (asynchronous) أحادية الخيط وغير حاجبة (non-blocking)، صارت أساس شيفرة nginx.
يعتمد nginx بكثافة على تعدد الإرسال (multiplexing) وإشعارات الأحداث، ويوكل مهام محددة إلى عمليات منفصلة. تُعالَج الاتصالات في حلقة تشغيل (run-loop) شديدة الكفاءة داخل عدد محدود من العمليات أحادية الخيط التي تُسمى workers. وداخل كل worker يستطيع nginx معالجة آلاف الاتصالات المتزامنة وعشرات الطلبات في الثانية.
بنية الشيفرة
تتألف شيفرة worker في nginx من النواة والوحدات الوظيفية. النواة مسؤولة عن الحفاظ على حلقة تشغيل محكمة وتنفيذ الأجزاء الملائمة من شيفرة الوحدات في كل مرحلة من مراحل معالجة الطلب. وتُشكّل الوحدات معظم وظائف طبقة العرض وطبقة التطبيق. تقرأ الوحدات من الشبكة والتخزين وتكتب فيهما، وتحوّل المحتوى، وتنفّذ الترشيح الصادر، وتطبّق إجراءات التضمين من جهة الخادم (server-side includes)، وتمرّر الطلبات إلى خوادم المصدر (upstream) عند تفعيل الوكيل.
تتيح بنية nginx المعيارية عمومًا للمطوّرين توسيع مجموعة ميزات خادم الويب دون تعديل نواة nginx. وتأتي وحدات nginx بأشكال مختلفة، وهي: وحدات النواة، ووحدات الأحداث، ومعالجات المراحل، والبروتوكولات، ومعالجات المتغيّرات، والمرشِّحات (filters)، ومصادر المصدر، وموازنات الحمل. وفي الوقت الحالي لا يدعم nginx الوحدات المحمَّلة ديناميكيًا؛ أي أن الوحدات تُصرَّف مع النواة في مرحلة البناء. غير أن دعم الوحدات القابلة للتحميل وواجهة ثنائية الاستدعاء (ABI) مُخطَّط له في الإصدارات الرئيسة المستقبلية. ويمكن العثور على معلومات أكثر تفصيلًا عن أدوار الوحدات المختلفة في القسم 14.4.
وأثناء تعامله مع مجموعة متنوعة من الإجراءات المرتبطة بقبول اتصالات الشبكة وإدارتها وجلب المحتوى، يستخدم nginx آليات إشعار بالأحداث وعددًا من تحسينات أداء الإدخال/الإخراج على الأقراص في أنظمة التشغيل Linux وSolaris والأنظمة المستندة إلى BSD، مثل kqueue وepoll وevent ports. والهدف تزويد نظام التشغيل بأكبر عدد ممكن من الإشارات بما يتعلق بالحصول على تغذية راجعة غير متزامنة في الوقت المناسب للحركة الواردة والصادرة، وعمليات الأقراص، والقراءة من المقابس أو الكتابة إليها، والمهلات الزمنية وما شابه. واستخدام مختلف أساليب تعدد الإرسال وعمليات الإدخال/الإخراج المتقدمة مُحسَّن بدقة لكل نظام تشغيل مستند إلى Unix تعمل عليه nginx.
يُعرض في الشكل 14.1 نظرة عامة عالية المستوى على بنية nginx.
الشكل 14.1: مخطط بنية nginx
نموذج العُمل
كما ذُكر سابقًا، لا يُنشئ nginx عملية أو خيطًا لكل اتصال. وبدلًا من ذلك، تقبل عمليات worker الطلبات الجديدة من مقبس «استماع» مشترك، وتنفّذ حلقة تشغيل شديدة الكفاءة داخل كل worker لمعالجة آلاف الاتصالات لكل worker. ولا يوجد في nginx تحكيم متخصص في الاتصالات ولا توزيع لها على workers؛ فهذا العمل تتولّاه آليات نواة نظام التشغيل. وعند بدء التشغيل، تُنشأ مجموعة أولية من مقابس الاستماع. ثم تظل workers تقبل الاتصال وتقرأ من المقابس وتكتب إليها بصورة مستمرة أثناء معالجة طلبات HTTP واستجاباتها.
تُعدّ حلقة التشغيل أكثر أجزاء شيفرة worker في nginx تعقيدًا. فهي تتضمن استدعاءات داخلية شاملة وتعتمد بقوة على فكرة المعالجة غير المتزامنة للمهام. وتُنفَّذ العمليات غير المتزامنة عبر المعيارية وإشعارات الأحداث والاستخدام الواسع لدوال الاستدعاء الخلفي (callbacks) والمؤقتات المضبوطة بدقة. وإجمالًا، المبدأ الأساسي هو أن يكون nginx غير حاجب قدر الإمكان. والموقف الوحيد الذي لا يزال nginx يواجه التوقف الانحصاري هو عدم كفاية أداء تخزين الأقراص بالنسبة لعملية worker.
ولأن nginx لا يُنشئ عملية أو خيطًا لكل اتصال، فإن استهلاك الذاكرة محافظ جدًا وشديد الكفاءة في الغالبية العظمى من الحالات. كما أن nginx يوفّر دورات المعالج، إذ لا يوجد نمط مستمر لإنشاء العمليات أو الخيوط وإتلافها. أما ما تفعله nginx فهو التحقق من حالة الشبكة والتخزين، وتهيئة الاتصالات الجديدة، وإضافتها إلى حلقة التشغيل، ومعالجتها بصورة غير متزامنة حتى اكتمالها، وعندئذٍ تُحرَّر ذاكرة الاتصال ويُزال من حلقة التشغيل. وبالاقتران بالاستخدام المتأني لـ syscalls وبتنفيذ دقيق للواجهات الداعمة مثل موزّعي ذاكرة pool وslab، يحقق nginx عادةً استهلاكًا متوسطًا إلى منخفضًا للمعالج حتى تحت الأحمال الفائقة.
ولأن nginx يُنشئ عدة workers للتعامل مع الاتصالات، فإنه يتوسّع جيدًا عبر عدة أنوية. وبشكل عام، يُتيح worker منفصل لكل نواة استغلالًا كاملًا لبنيات المعالج متعدد الأنوية، ويمنع تداخل الخيوط والتجمّدات (lock-ups). فلا يوجد نزاع على الموارد، وآليات التحكم بالموارد معزولة داخل عمليات worker أحادية الخيط. كما يتيح هذا النموذج قابلية توسّع أكبر عبر أجهزة التخزين المادية، ويسهّل استغلال الأقراص أكثر، ويتجنّب التوقف الانحصاري في انتظار الإدخال/الإخراج من الأقراص. ونتيجةً لذلك، تُستثمر موارد الخادم بكفاءة أكبر مع توزيع حمل العمل على عدة عُمّال.
ومع أنماط استهلاك الأقراص وحمل المعالج، ينبغي تعديل عدد workers في nginx. والقواعد هنا الأساسية إلى حدٍّ ما، وينبغي لمسؤولي النظام تجربة عدد من التكوينات الملائمة لأحمال عملهم. وقد تكون التوصيات العامة كالتالي: إذا كان نمط الحمل معتمدًا على المعالج (CPU intensive) — على سبيل المثال، معالجة كميات كبيرة من TCP/IP، أو تنفيذ SSL، أو الضغط — فينبغي أن يساوي عدد workers في nginx عدد أنوية المعالج؛ وإذا كان الحمل معتمدًا في معظمه على الإدخال/الإخراج من الأقراص (disk I/O bound) — على سبيل المثال، تقديم مجموعات مختلفة من المحتوى من التخزين، أو توكيل شديد — فيمكن أن يكون عدد workers من مرة ونصف إلى مرتين عدد الأنوية. ويختار بعض المهندسين عدد workers استنادًا بدلًا من ذلك إلى عدد وحدات التخزين المفردة، وإن كان كفاءة هذا النهج تعتمد على نوع تخزين الأقراص وضبطه.
ومن أكبر المشكلات التي سيعالجها مطوّرو nginx في الإصدارات القادمة كيفية تجنّب معظم حالات التوقف الانحصاري في انتظار الإدخال/الإخراج من الأقراص. وفي الوقت الحالي، إذا لم يكن أداء التخزين كافيًا لخدمة عمليات القرص التي يولّدها worker معيّن، فقد يتوقف ذلك worker عند القراءة أو الكتابة على القرص. وهناك عدد من الآليات وتوجيهات ملف الإعداد لتخفيف مثل هذه الحالات. وبالدرجة الأولى، تترك تركيبات الخيارات مثل sendfile وAIO عادةً هامشًا واسعًا لأداء القرص. وينبغي التخطيط لتثبيت nginx استنادًا إلى مجموعة البيانات، وحجم الذاكرة المتاحة لـ nginx، وبنية التخزين الأساسية.
ومن المشكلات الأخرى في نموذج worker الحالي الدعم المحدود للبرمجة النصية المضمّنة (embedded scripting). فأولًا، في التوزيعة القياسية من nginx، فإن تضمين نصوص Perl هو المدعوم فقط. ولهذا تفسير بسيط: المشكلة الجوهرية هي احتمال أن يتوقف السكربت المضمّن عند أي عملية أو ينتهي بشكل غير متوقع. وسيؤدي كلا نوعَي السلوك فورًا إلى تعليق العملية بدون استجابة، ما يؤثر في الوقت نفسه على آلاف الاتصالات. وهناك المزيد من العمل المخطط لجعل البرمجة النصية المضمّنة مع nginx أبسط وأكثر موثوقية وأكثر ملاءمة لمجموعة أوسع من التطبيقات.
أدوار عمليات nginx
يعمل nginx بعدة عمليات في الذاكرة؛ هناك عملية رئيسية (master) واحدة وعدة عمليات worker. وهناك أيضًا عمليتان ذوا غرض خاص، وهما بالضبط مُحمِّل ذاكرة التخزين المؤقت ومُدير ذاكرة التخزين المؤقت. وجميع العمليات أحادية الخيط في الإصدار 1.x من nginx. وتستخدم جميع العمليات أساسًا آليات الذاكرة المشتركة للتواصل بين العمليات (IPC). وتعمل العملية الرئيسية بوصفها المستخدم root. أما مُحمِّل ذاكرة التخزين المؤقت ومُديرها وworkers فتعمل كمستخدم غير مُميَّز الصلاحيات.
العملية الرئيسية مسؤولة عن المهام التالية:
- قراءة الإعدادات والتحقق منها
- إنشاء المقابس وربطها وإغلاقها
- بدء عمليات
workerوإيقافها والمحافظة على العدد المُهيّأ منها - إعادة التهيئة من دون قطع الخدمة
- ضبط الترقيات الثنائية دون توقف (بدء الملف الثنائي الجديد والتراجع عند الحاجة)
- إعادة فتح ملفات السجلّات
- تصريف نصوص Perl المضمّنة
تقبل عمليات worker الاتصالات من العملاء وتتعامل معها ومعالجتها، وتوفّر وظائف الوكيل العكسي والترشيح، وتقوم عمليًا بكل ما عدا ذلك مما يستطيع nginx أداؤه. وفي ما يخص مراقبة سلوك نسخة nginx، ينبغي لمسؤول النظام أن يُبقي عينه على workers لأنها العمليات التي تعكس العمليات اليومية الفعلية لخادم الويب.
عملية مُحمِّل ذاكرة التخزين المؤقت مسؤولة عن فحص عناصر ذاكرة التخزين المؤقت الموجودة على القرص وتغذية قاعدة بيانات nginx في الذاكرة ببيانات وصفية (metadata) عن الذاكرة المؤقتة. وبعبارة أخرى، يهيّئ مُحمِّل الذاكرة المؤقتة نسخ nginx للعمل مع الملفات المخزَّنة بالفعل على القرص ضمن بنية أدلة مخصَّصة. فيتصفح الأدلة، ويتحقق من بيانات المحتوى المخزَّن مؤقتًا، ويحدّث المدخلات ذات الصلة في الذاكرة المشتركة، ثم يخرج حين يصبح كل شيء نظيفًا وجاهزًا للاستخدام.
أما مُدير ذاكرة التخزين المؤقت فمسؤول أساسًا عن انتهاء صلاحية الذاكرة المؤقتة وإبطالها. ويبقى في الذاكرة أثناء التشغيل الطبيعي لـ nginx، ويعيد تشغيله العملية الرئيسية في حال حدوث خلل.
لمحة سريعة عن التخزين المؤقت في nginx
يُنفَّذ التخزين المؤقت في nginx على هيئة تخزين هرمي للبيانات في نظام ملفات. ومفاتيح الذاكرة المؤقتة قابلة للتهيئة، ويمكن استخدام معاملات مختلفة خاصة بكل طلب للتحكم في ما يدخل الذاكرة المؤقتة. وتُخزَّن مفاتيح الذاكرة المؤقتة وبياناتها الوصفية في مقاطع الذاكرة المشتركة التي يستطيع مُحمِّل الذاكرة المؤقتة ومُديرها وworkers الوصول إليها. وحتى الآن لا يوجد أي تخزين مؤقت للملفات في الذاكرة، باستثناء التحسينات التي تفرضها آليات نظام الملفات الافتراضية في نظام التشغيل. يوضع كل استجابة مخزَّنة مؤقتًا في ملف مختلف على نظام الملفات. وتُتحكَّم في التسلسل الهرمي (المستويات وتفاصيل التسمية) من خلال توجيهات إعداد nginx. وحين تُكتب استجابة في بنية أدلة الذاكرة المؤقتة، يُشتق مسار الملف واسمه من تجزئة MD5 لعنوان URL المُوكَّل.
تتمثل عملية وضع المحتوى في الذاكرة المؤقتة فيما يلي: حين تقرأ nginx الاستجابة من خادم مصدر، يُكتب المحتوى أولًا في ملف مؤقت خارج بنية أدلة الذاكرة المؤقتة. وحين تنتهي nginx من معالجة الطلب، تُعيد تسمية الملف المؤقت وتنقله إلى دليل الذاكرة المؤقتة. وإذا كان دليل الملفات المؤقتة المستخدم للوكيل على نظام ملفات آخر، فسيُنسخ الملف، لذا يُوصى بإبقاء دليلَي الملفات المؤقتة والذاكرة المؤقتة على نظام الملفات نفسه. كما أنه من الآمن تمامًا حذف الملفات من بنية أدلة الذاكرة المؤقتة عندما يلزم إبطالها صراحةً. وهناك إضافات من الأطراف الثالثة تتيح التحكم عن بُعد في المحتوى المخزَّن مؤقتًا، والمزيد من العمل مُخطَّط لدمج هذه الوظيفة في التوزيعة الرئيسة.
14.3. إعداد nginx
استُوحى نظام إعداد nginx من تجارب Igor Sysoev مع Apache. وكانت رؤيته الأساسية أن نظام إعداد قابل للتوسّع عنصر جوهري لأي خادم ويب. وقد وُوجهت مشكلة التوسّع الرئيسة عند صيانة إعدادات كبيرة ومعقدة تحتوي على الكثير من الخوادم الافتراضية والأدلة والمواقع ومجموعات البيانات. وفي أي إعداد ويب كبير نسبيًا، قد يتحول الأمر إلى كابوس إن لم يُنجَز على نحو سليم على مستوى التطبيق وعلى يد مهندس النظام نفسه.
وبناءً على ذلك، صُمّم إعداد nginx ليُبسِّط العمليات اليومية ويوفّر وسيلة سهلة للتوسيع في إعداد خادم الويب.
يُحفظ إعداد nginx في عدد من ملفات النص الصريح التي تقع عادةً في /usr/local/etc/nginx أو /etc/nginx. ويُسمَّى ملف الإعداد الرئيس عادةً nginx.conf. ولإبقائه خاليًا من الفوضى، يمكن وضع أجزاء من الإعداد في ملفات منفصلة يمكن تضمينها تلقائيًا في الملف الرئيس. غير أنه يجدر الإشارة هنا إلى أن nginx لا يدعم حاليًا الإعدادات الموزّعة بأسلوب Apache (أي ملفات .htaccess). وينبغي أن يقيم كل ما يتصل بسلوك خادم الويب من إعداد داخل مجموعة مركزية من ملفات الإعداد.
تُقرأ ملفات الإعداد أولًا وتُتحقق منها على يد العملية الرئيسية. وتتوفر لدى عمليات worker صيغة مُصرَّفة للقراءة فقط من إعداد nginx فور إنشائها بالتفرّع من العملية الرئيسية. كما تتشارك هياكل الإعداد تلقائيًا عبر آليات إدارة الذاكرة الافتراضية المعتادة.
يملك إعداد nginx عدة سياقات مختلفة لكتل التوجيهات main وhttp وserver وupstream وlocation (وكذلك mail لتوكيل البريد). ولا تتداخل السياقات إطلاقًا. فعلى سبيل المثال لا وجود لمفهوم وضع كتلة location داخل كتلة main الخاصة بالتوجيهات. أيضًا، تفاديًا لأي غموض غير ضروري، لا يوجد شيء أشبه بإعداد «خادم ويب عام». ويُقصد بإعداد nginx أن يكون نظيفًا ومنطقيًا، بما يتيح للمستخدمين صيانة ملفات إعداد معقدة تضم آلاف التوجيهات. وفي محادثة خاصة قال Sysoev: «المواقع والأدلة والكتل الأخرى في إعداد الخادم العام كانت السمات التي لم أحبها قط في Apache، وهذا هو السبب في أنها لم تُنفَّذ قط في nginx.»
تتبع صياغة الإعدادات وتنسيقها وتعريفاتها ما يُعرف باتفاقية بأسلوب C. وقد سبق أن استُخدم هذا الأسلوب الخاص في إعداد ملفات الإعداد من قِبل مجموعة متنوعة من تطبيقات البرمجيات مفتوحة المصدر والتجارية. وبالتصميم، فإن الإعداد بأسلوب C مناسب تمامًا للوصوف المتداخلة، لأنه منطقي وسهل الإنشاء والقراءة والصيانة ويحبه كثير من المهندسين. كما يمكن أتمتة إعداد nginx بأسلوب C بسهولة.
ورغم أن بعض توجيهات nginx تشبه أجزاء معيّنة من إعداد Apache، فإن إعداد نسخة nginx تجربة مختلفة تمامًا. فعلى سبيل المثال، تدعم nginx قواعد إعادة الكتابة (rewrite)، غير أن ذلك يتطلب من المسؤول يدويًا تكييف إعداد إعادة كتابة قديم خاص بـ Apache ليطابق أسلوب nginx. كما يختلف تنفيذ محرك إعادة الكتابة أيضًا.
عمومًا، يوفّر إعداد nginx أيضًا دعمًا لعدد من الآليات الأصلية التي قد تكون مفيدة جدًا كجزء من إعداد خادم ويب نحيف. ومن المفيد أن نذكر باختصار المتغيّرات وتوجيه try_files، إذ كانا إلى حدٍّ ما فريدين في nginx. وقد طُوّرت المتغيّرات في nginx لتوفّر آلية إضافية وأقوى للتحكم في إعداد خادم الويب أثناء التشغيل. والمتغيّرات مُحسَّنة للتقييم السريع، وهي مُصرَّفة مسبقًا داخليًا إلى فهارس. ويجري التقييم عند الطلب؛ أي أن قيمة المتغيّر تُحسب عادةً مرة واحدة فقط وتُخزَّن طوال عمر طلب بعينه. ويمكن استخدام المتغيّرات مع توجيهات إعداد مختلفة، ما يوفّر مرونة إضافية في وصف سلوك المعالجة الشرطية للطلبات.
صُمِّم توجيه try_files في الأصل ليحلّ محل عبارات if الشرطية في الإعداد تدريجيًا وبطريقة أكثر ملاءمة، وقد رُوعي في تصميمه تجربة ومطابقة سريعين وفعّالين لمخططات مختلفة من URI إلى المحتوى. وإجمالًا، يعمل توجيه try_files جيدًا ويمكن أن يكون شديد الفعالية والفائدة. ويُوصى بأن يطّلع القارئ مراجعةً وافية على توجيه try_files ويلتزم باستخدامه متىما كان ذلك ملائمًا.
14.4. البنية الداخلية لـ nginx
كما ذُكر سابقًا، تتألف الشيفرة الأساسية لـ nginx من نواة وعدد من الوحدات. والنواة مسؤولة عن توفير أساس خادم الويب، ووظائف وكيل الويب العكسي وكيل البريد؛ فهي تتيح استخدام بروتوكولات الشبكة الأساسية، وتبني بيئة التشغيل الضرورية، وتضمن تفاعلًا سلسًا بين الوحدات المختلفة. غير أن معظم الميزات الخاصة بالبروتوكولات والتطبيقات تتولاها وحدات nginx، لا النواة.
داخليًا، تعالج nginx الاتصالات عبر خط أنابيب من الوحدات، أو سلسلة منها. وبعبارة أخرى، لكل عملية هناك وحدة تؤدّي العمل ذا الصلة؛ مثل الضغط، أو تعديل المحتوى، أو تنفيذ التضمينات من جهة الخادم، أو التواصل مع خوادم التطبيقات المصدر عبر بروتوكولات FastCGI أو uwsgi، أو التحادث مع memcached.
وهناك عدد من وحدات nginx تقع بين النواة والوحدات «الوظيفية» الحقيقية. وهذه الوحدات هي http وmail. وتوفّر هاتان الوحدتان مستوى إضافي من التجريد بين النواة والمكوّنات الأدنى مستوى. وفي هاتين الوحدتين يُنفَّذ التعامل مع تسلسل الأحداث المرتبط ببروتوكول طبقة تطبيق معيّن مثل HTTP أو SMTP أو IMAP. وإلى جانب نواة nginx، تكون هذه الوحدات ذات المستوى الأعلى مسؤولة عن الحفاظ على الترتيب الصحيح للاستدعاءات إلى الوحدات الوظيفية المعنية. ورغم أن بروتوكول HTTP مُنفَّذ حاليًا كجزء من الوحدة http، فإن هناك خططًا لفصله في وحدة وظيفية في المستقبل، بسبب الحاجة إلى دعم بروتوكولات أخرى مثل SPDY (انظر «SPDY: An experimental protocol for a faster web»).
يمكن تقسيم الوحدات الوظيفية إلى وحدات أحداث، ومعالجات مراحل، ومرشِّحات إخراج، ومعالجات متغيّرات، وبروتوكولات، ومصادر مصدر، وموازنات حمل. ومعظم هذه الوحدات يُكمل وظائف HTTP في nginx، وإن كانت وحدات الأحداث والبروتوكولات تُستخدم أيضًا في mail. وتوفّر وحدات الأحداث آلية إشعار بأحداث معيّنة تعتمد على نظام التشغيل، مثل kqueue أو epoll. وتعتمد وحدة الأحداث التي يستخدمها nginx على إمكانات نظام التشغيل وضبط البناء. وتتيح وحدات البروتوكول لـ nginx التواصل عبر HTTPS وTLS/SSL وSMTP وPOP3 وIMAP.
تبدو دورة نمطية لمعالجة طلب HTTP على النحو التالي.
- يرسل العميل طلب HTTP.
- تختار نواة nginx معالج المرحلة المناسب بناءً على الموقع المُهيّأ المطابق للطلب.
- إذا كان الإعداد يقرّ بذلك، يختار موازن الحمل خادم مصدر للوكيل.
- يؤدّي معالج المرحلة مهمته ويمرّر كل مخزن إخراج إلى المرشِّح الأول.
- يمرّر المرشِّح الأول المخرجات إلى المرشِّح الثاني.
- يمرّر المرشِّح الثاني المخرجات إلى الثالث (وهكذا دواليك).
- تُرسَل الاستجابة النهائية إلى العميل.
استدعاء وحدات nginx قابل للتخصيص إلى حدٍّ كبير لأنه يتم عبر سلسلة من دوال الاستدعاء الخلفي باستخدام مؤشرات إلى الدوال القابلة للتنفيذ. غير أن الجانب السلبي هنا أنه قد يضع عبئًا كبيرًا على المبرمجين الذين يريدون كتابة وحدات خاصة بهم، لأن عليهم تحديد كيفية تشغيل الوحدة وتوقيته بدقة. ويُعمل على تحسين واجهة برمجة تطبيقات nginx وتوثيق المطورين وإتاحته أكثر للتخفيف من ذلك.
وفيما يلي بعض الأمثلة على المواضع التي يمكن أن تتعلّق بها وحدة:
- قبل قراءة ملف الإعداد ومعالجته
- لكل توجيه إعداد في الموقع والخادم الذي يظهر فيه
- عند تهيئة الإعداد الرئيس
- عند تهيئة الخادم (أي المضيف/المنفذ)
- عند دمج إعداد الخادم مع الإعداد الرئيس
- عند تهيئة إعداد الموقع أو دمجه مع إعداد الخادم الأب
- عند بدء العملية الرئيسية أو خروجها
- عند بدء عملية عامل جديدة أو خروجها
- عند معالجة طلب
- عند ترشيح ترويسة الاستجابة ومتنها
- عند انتقاء طلب إلى خادم مصدر وبدء تنفيذه وإعادة تنفيذه
- عند معالجة الاستجابة الصادرة من خادم مصدر
- عند إنهاء التفاعل مع خادم مصدر
داخل worker، يبدو تسلسل الإجراءات المؤدّية إلى حلقة التشغيل التي يُولَّد عندها الاستجابة على النحو التالي:
- البدء في
ngx_worker_process_cycle(). - معالجة الأحداث بآليات خاصة بنظام التشغيل (مثل
epollأوkqueue). - قبول الأحداث وتوزيع الإجراءات ذات الصلة.
- معالجة/توكيل ترويسة الطلب ومتنه.
- توليد محتوى الاستجابة (الترويسة والمتن) وبثّه إلى العميل.
- إنهاء الطلب.
- إعادة تهيئة المؤقتات والأحداث.
تضمن حلقة التشغيل نفسها (الخطوتان 5 و6) التوليد التزايدي للاستجابة وبثّها إلى العميل.
قد تبدو معالجة طلب HTTP بمزيد من التفصيل على النحو التالي:
- تهيئة معالجة الطلب.
- معالجة الترويسة.
- معالجة المتن.
- استدعاء المعالج المرتبط.
- المرور بمراحل المعالجة على نحو متسلسل.
وهذا يقودنا إلى المراحل. حين تعالج nginx طلب HTTP، فإنها تمرره عبر عدد من مراحل المعالجة. وفي كل مرحلة توجد معالجات تُستدعى. وعمومًا، تعالج معالجات المراحل الطلب وتنتج المخرجات المعنية. وتُربط معالجات المراحل بالمواقع المعرَّفة في ملف الإعداد.
وتؤدّي معالجات المراحل عادةً أربع مهمات: جلب إعداد الموقع، وتوليد استجابة ملائمة، وإرسال الترويسة، وإرسال المتن. ومعالج المعالجة له وسيط واحد: بنية معيّنة تصف الطلب. وتحمل بنية الطلب الكثير من المعلومات المفيدة عن طلب العميل، مثل طريقة الطلب وURI والترويسة.
وحين تُقرأ ترويسة طلب HTTP، تُجري nginx بحثًا عن إعداد الخادم الافتراضي المرتبط. وإذا عُثر على الخادم الافتراضي، يمرّ الطلب عبر ست مراحل:
- مرحلة إعادة كتابة الخادم
- مرحلة الموقع
- مرحلة إعادة كتابة الموقع (وهي قد تعيد الطلب إلى المرحلة السابقة)
- مرحلة التحكم في الوصول
- مرحلة try_files
- مرحلة السجلّ
وفي محاولة توليد المحتوى الضروري استجابةً للطلب، تمرّر nginx الطلب إلى معالج محتوى ملائم. وبحسب إعداد الموقع بالضبط، قد تُجرّب nginx أولًا ما يُسمى بالمعالجات غير المشروطة، مثل perl وproxy_pass وflv وmp4 وغيرها. وإذا لم يطابق الطلب أيًّا من معالجات المحتوى أعلاه، فإن يلتقطه أحد المعالجات التالية، وبهذا الترتيب تحديدًا: random index ثم index ثم autoindex ثم gzip_static ثم static.
يمكن العثور في وثائق nginx على تفاصيل وحدات الفهرسة، لكن هذه هي الوحدات التي تتعامل مع الطلبات المنتهية بشرطة مائلة. وإذا لم تكن وحدة متخصصة مثل mp4 أو autoindex ملائمة، فإن المحتوى يُعد مجرّد ملف أو دليل على القرص (أي محتوى ساكن) وتقدّمه معالج المحتوى static. أما في حالة الدليل فيعيد nginx كتابة URI تلقائيًا بحيث تكون الشرطة المائلة اللاحقة موجودة دائمًا (ثم يصدر إعادة توجيه HTTP).
يُمرَّر محتوى معالجات المحتوى بعد ذلك إلى المرشِّحات. والمرشِّحات مرتبطة أيضًا بالمواقع، وقد يكون هناك عدة مرشِّحات مُهيّأة لموقع واحد. وتؤدّي المرشِّحات مهمة معالجة المخرجات التي ينتجها المعالج. ويحدَّد ترتيب تنفيذ المرشِّحات وقت التصريف. أما المرشِّحات الجاهزة خارج الصندوق فتُعرَّف مسبقًا، أما مرشِّح من طرف ثالث فيمكن ضبطه في مرحلة البناء. وفي تنفيذ nginx الحالي، لا المرشِّحات سوى التعديلات الصادرة، ولا توجد حاليًا آلية لكتابة مرشِّحات وإرفاقها لتحويل محتوى الإدخال. وسيظهر الترشيح على الإدخال في الإصدارات المستقبلية من nginx.
تتبع المرشِّحات نمط تصميم معيّنًا. يُستدعى المرشِّح فيبدأ عمله، ثم يستدعي المرشِّح التالي حتى يُستدعى المرشِّح الأخير في السلسلة. وبعد ذلك تُنهي nginx الاستجابة. وليس على المرشِّحات انتظار انتهاء المرشِّح السابق. إذ يمكن للمرشِّح التالي في السلسلة أن يبدأ عمله بمجرد توفّر المدخلات من المرشِّح السابق (الأمر الذي يشبه وظيفيًا أنابيب Unix كثيرًا). وبدورها، يمكن تمرير الاستجابة الجاري توليدها إلى العميل قبل استلام الاستجابة الكاملة من خادم المصدر.
هناك مرشِّحات للترويسة ومرشِّحات للمتن؛ تُدخل nginx ترويسة الاستجابة ومتنها إلى المرشِّحات المرتبطة بهما على نحو منفصل.
يتألف مرشِّح الترويسة من ثلاث خطوات أساسية:
- اتخاذ قرار بشأن العمل على هذه الاستجابة.
- العمل على الاستجابة.
- استدعاء المرشِّح التالي.
تُحوِّل مرشِّحات المتن المحتوى المولَّد. ومن أمثلة مرشِّحات المتن:
- التضمينات من جهة الخادم
- ترشيح XSLT
- ترشيح الصور (على سبيل المثال، تغيير حجم الصور أثناء التشغيل)
- تعديل مجموعة المحارف
- ضغط
gzip - الترميز المُجزَّأ
بعد سلسلة المرشِّحات، تُمرَّر الاستجابة إلى الكاتب. وإلى جانب الكاتب توجد بضعة مرشِّحات إضافية ذات غرض خاص، وهي مرشِّح copy ومرشِّح postpone. ومسؤولية مرشِّح copy هي ملء مخازن الذاكرة بمحتوى الاستجابة المعني الذي قد يكون مخزَّنًا في دليل مؤقت للوكيل. ويُستخدم مرشِّح postpone في الطلبات الفرعية.
الطلبات الفرعية (subrequests) آلية بالغة الأهمية في معالجة الطلب والاستجابة. وهي أيضًا أحد أقوى جوانب nginx. وباستخدام الطلبات الفرعية تستطيع nginx إرجاع نتائج من عنوان URL مختلف عن الذي طلبه العميل أصلًا. فبعض أطر عمل الويب تسمّي هذا «إعادة توجيه داخلية». غير أن nginx تذهب أبعد من ذلك — فليس المطلوب أن تنفّذ الطلبات الفرعية فحسب ودمج مخرجاتها في استجابة واحدة، بل يمكن أيضًا أن تكون الطلبات الفرعية متداخلة ومراتبية. إذ يمكن للطلب الفرعي أن ينفّذ طلبًا فرعيًا فرعيًا خاصًا به، ويمكن للطلب الفرعي الفرعي أن يبدأ طلبات فرعية فرعية فرعية. ويمكن أن تُوجَّه الطلبات الفرعية إلى ملفات على القرص الصلب أو معالجات أخرى أو خوادم مصدر. وتُعدّ الطلبات الفرعية أكثر ما تُفيد عند إدراج محتوى إضافي استنادًا إلى بيانات الاستجابة الأصلية. فعلى سبيل المثال، تستخدم وحدة SSI (server-side include) مرشِّحًا لتحليل محتوى المستند المُعاد، ثم تستبدل توجيهات include بمحتوى عناوين URL محددة. أو يمكن أن يكون ذلك مثالًا على صنع مرشِّح يتعامل مع المحتوى الكامل لمستند باعتباره عنوان URL يُسترجع، ثم يُلحق المستند الجديد بعنوان URL نفسه.
وجدير أيضًا بوصف المصادر وموازنات الحمل بإيجاز. تُستخدم المصادر لتنفيذ ما يمكن تحديده على أنه معالج محتوى هو وكيل عكسي (معالج proxy_pass). وتقوم وحدات المصدر في معظمها بتحضير الطلب لإرساله إلى خادم مصدر (أو «خلفية») واستقبال الاستجابة منه. ولا تُستدعى هنا مرشِّحات الإخراج. أما ما تفعله وحدة المصدر على وجه الدقة فهو ضبط دوال استدعاء خلفي تُستدعى حين يصبح خادم المصدر جاهزًا للكتابة إليه والقراءة منه. وهناك دوال استدعاء خلفي تنفّذ الوظائف التالية:
- صياغة مخزن للطلب (أو سلسلة مخازن) لإرساله إلى خادم المصدر
- إعادة تهيئة الاتصال بخادم المصدر أو تصفيره (وهذا يحدث قبل إعادة إنشاء الطلب مباشرةً)
- معالجة أول أجزاء استجابة المصدر وحفظ مؤشرات إلى الحمولة المستلَمة من خادم المصدر
- إيقاف الطلبات (وهذا يحدث حين ينهي العميل اتصاله بشكل مبكر)
- إنهاء الطلب حين تنتهي nginx من القراءة من خادم المصدر
- تشذيب متن الاستجابة (مثل إزالة تذييل trailer)
ترتبط وحدات موازنة الحمل بمعالج proxy_pass لتوفير القدرة على انتقاء خادم مصدر عندما يكون أكثر من خادم مصدر مؤهلًا. ويسجّل موازن الحمل توجيه ملف إعداد تتيحه، ويوفّر دوال تهيئة إضافية للمصدر (لحل أسماء المصادر في DNS مثلًا)، ويهيّئ هياكل الاتصالات، ويقرّر إلى أين تُوجَّه الطلبات، ويحدّث معلومات الإحصاءات. ويدعم nginx حاليًا نوعين معياريين لموازنة الحمل على خوادم المصدر: round-robin وip-hash.
تشمل آليات التعامل مع المصادر وموازنة الحمل خوارزميات لاكتشاف خوادم المصدر الفاشلة وإعادة توجيه الطلبات الجديدة إلى الخوادم المتبقية — وإن كان الكثير من العمل الإضافي مُخططًا لتحسين هذه الوظيفة. وعمومًا، هناك المزيد من العمل المُخطط لموازنات الحمل، وفي الإصدارات القادمة من nginx ستحسّن آليات توزيع الحمل على خوادم المصدر المختلفة، وكذلك فحوص السلامة (health checks)، تحسّنًا كبيرًا.
وهناك أيضًا عدد قليل من الوحدات الأخرى المثيرة للاهتمام التي تقدّم مجموعة إضافية من المتغيّرات لاستخدامها في ملف الإعداد. ورغم أن المتغيّرات في nginx تُنشأ وتُحدَّث عبر وحدات مختلفة، فإن هناك وحدتين مكرَّستين بالكامل للمتغيّرات: geo وmap. وتُستخدم الوحدة geo لتسهيل تتبّع العملاء بناءً على عناوين IP لديهم. ويمكن لهذه الوحدة إنشاء متغيّرات اعتباطية تعتمد على عنوان IP للعميل. أما الوحدة الأخرى، map، فتتيح إنشاء متغيّرات من متغيّرات أخرى، مما يوفّر على نحو جوهري القدرة على إجراء تعيينات مرنة لأسماء المضيفين ومتغيّرات التشغيل الأخرى. وقد تُسمّى هذه النوعية من الوحدات بمعالج المتغيّرات.
كانت آليات تخصيص الذاكرة المنفَّذة داخل worker واحد في nginx، إلى حدٍّ ما، مستوحاة من Apache. ويمكن تقديم وصف عالي المستوى لإدارة ذاكرة nginx على النحو التالي: لكل اتصال، تُخصَّص مخازن الذاكرة اللازمة ديناميكيًا وتُربط ببعضها وتُستخدم لتخزين الترويسة والمتن الخاصين بالطلب والاستجابة ومعالجتهما، ثم تُحرَّر عند تحرير الاتصال. ومن المهم جدًا ملاحظة أن nginx يحاول تجنب نسخ البيانات في الذاكرة قدر الإمكان، وأن معظم البيانات تُمرَّر عبر قيم المؤشرات لا عبر استدعاء memcpy.
ولنُعمق قليلًا: حين تُولِّد وحدة الاستجابة، يوضع المحتوى المُسترجَع في مخزن ذاكرة يُضاف بدوره إلى حلقة من روابط المخازن. وتعمل المعالجة اللاحقة على رابط سلسلة المخازن هذا أيضًا. وسلاسل المخازن معقدة إلى حدٍّ كبير في nginx لأن هناك عدة سيناريوهات معالجة تختلف باختلاف نوع الوحدة. فعلى سبيل المثال، قد تكون إدارة المخازن بدقة صعبة نوعًا ما عند تنفيذ وحدة مرشِّح متن. فهذه الوحدة لا تستطيع العمل إلا على مخزن واحد (رابط في السلسلة) في الوقت نفسه، ويجب أن تقرر هل تستبدل محتوى مخزن الإدخال، أم تستبدل المخزن بمخزن جديد مخصَّص، أم تُدرج مخزنًا جديدًا قبل أو بعد المخزن محل النظر. ولتعقيد الأمور، تتلقى الوحدة أحيانًا عدة مخازن فتكون أمامها سلسلة مخازن غير مكتملة عليها للعمل عليها. غير أن nginx لا توفّر حاليًا سوى واجهة برمجية منخفضة المستوى للتعامل مع سلاسل المخازن، لذا ينبغي لمطوّر وحدة من طرف ثالث أن يتقن تمامًا هذا الجزء الغامض من nginx قبل الشروع في أي تنفيذ فعلي.
وتجدر الإشارة إلى أن النهج أعلاه يخصّص مخازن ذاكرة طوال عمر الاتصال، ومن ثمّ تُحتفظ بعض الذاكرة الإضافية في الاتصالات طويلة العمر. وفي الوقت نفسه، لا تنفق nginx سوى 550 بايت من الذاكرة على اتصال keepalive خامل. ومن التحسينات الممكنة في الإصدارات المستقبلية من nginx إعادة استخدام مخازن الذاكرة ومشاركتها في الاتصالات طويلة العمر.
تتولى مهمة إدارة تخصيص الذاكرة موزِّع مجمّعات (pool allocator) الخاص بـ nginx. وتُستخدم مناطق الذاكرة المشتركة لاستيعاب كائن المزامنة (mutex)، وبيانات الذاكرة المؤقتة الوصفية، وذاكرة جلسات SSL المؤقتة، والمعلومات المرتبطة بتنظيم النطاق الترددي وإدارته (الحدود). وهناك موزِّع slab منفَّذ في nginx لإدارة تخصيص الذاكرة المشتركة. ولإتاحة الاستخدام الآمن المتزامن للذاكرة المشتركة، تتوافر عدد من آليات القفل (كائنات المزامنة والإشارات). ولمحاولة تنظيم هياكل البيانات المعقدة، توفّر nginx أيضًا تنفيذًا لشجرة حمراء-سوداء (red-black tree). وتُستخدم الأشجار الحمراء-السوداء للاحتفاظ ببيانات الذاكرة المؤقتة الوصفية في الذاكرة المشتركة، ولتتبّع تعريفات المواقع غير المبنية على تعابير نمطية (non-regex)، ولمهام أخرى قليلة.
ولأسف، لم يُشرَح كل ما سبق يومًا بأسلوب متسق وبسيط، مما يجعل مهمة تطوير إضافات الأطراف الثالثة لـ nginx معقدة إلى حدٍّ كبير. ورغم وجود بعض الوثائق الجيدة عن البنية الداخلية لـ nginx — مثل تلك التي أعدّها Evan Miller — إلا أن مثل هذه الوثائق تطلّبت جهدًا هائلًا في الهندسة العكسية، ولا يزال تنفيذ وحدات nginx لفنّ «فنًّا أسود» لدى كثيرين.
ورغم الصعوبات معينة المرتبطة بتطوير وحدات الأطراف الثالثة، يرى مجتمع مستخدمي nginx مؤخرًا كثيرًا من وحدات الأطراف الثالثة المفيدة. ومن ذلك على سبيل المثال وحدة مفسّر Lua مضمّن لـ nginx، ووحدات إضافية لموازنة الحمل، ودعم كامل لـ WebDAV، والتحكم المتقدم في الذاكرة المؤقتة، وأعمال أخرى مثيرة للاهتمام من الأطراف الثالثة يشجع عليها مؤلفو هذا الفصل وسيدعمونها في المستقبل.
14.5. الدروس المستفادة
حين بدأ Igor Sysoev في كتابة nginx، كانت معظم البرمجيات التي تتيح الإنترنت قد وُجدت بالفعل، وكانت بنية تلك البرمجيات تتبع عادةً تعريفات الأجهزة والخوادم والشبكات القديمة وأنظمة التشغيل القديمة، وبنية الإنترنت القديمة عمومًا. غير أن هذا لم يمنع Igor من الاعتقاد أنه قد يستطيع تحسين الأمور في مجال خوادم الويب. لذا، وإن كان الدرس الأول قد يبدو بديهيًا، فهو هذا: هما دائمًا مجال للتحسين.
وربما في ذهنه فكرة برمجيات ويب أفضل، أمضى Igor وقتًا طويلًا في تطوير البنية الأولية للشيفرة ودراسة مختلف طرق تحسين الشيفرة لمجموعة متنوعة من أنظمة التشغيل. وبعد عشر سنوات، يعمل على تطوير نموذج أولي للإصدار 2.0 من nginx، آخذًا في الحسبان سنوات من التطوير النشط للإصدار 1. ومن الواضح أن النموذج الأولي الأولي لبنية جديدة، وأن البنية الأولية للشيفرة، لهما أهمية قصوى لمستقبل منتج برمجي.
ومن النقاط الأخرى الجديرة بالذكر أن التطوير ينبغي أن يكون مُركَّزًا. فإصدار Windows من nginx ربما يكون مثالًا جيدًا على أنه يستحق تجنّب تشتيت جهد التطوير في أمر لا يمثّل الكفاءة الأساسية للمطوّر ولا التطبيق الهدف. وينطبق الأمر ذاته على محرك إعادة الكتابة الذي ظهر خلال عدة محاولات لتعزيز nginx مزيدًا من الميزات من أجل التوافق الخلفي مع الإعدادات القديمة القائمة.
وأخيرًا، وليس أقلها، يجدر بالذكر أنه رغم أن مجتمع مطوّري nginx ليس كبيرًا على الإطلاق، فإن وحدات وإضافات الأطراف الثالثة لـ nginx ظلّت على الدوام جزءًا مهمًا جدًا من شعبيته. وقد نالت الأعمال التي أنجزها Evan Miller وPiotr Sikora وValery Kholodkov وZhang Yichun (agentzh) وغيرهم من مهندسي البرمجيات الموهوبين تقديرًا كبيرًا من مجتمع مستخدمي nginx ومطوّريه الأصليين.