خوادم الويب والوكلاء العكسيون

معمارية خادم الويب

18 دقيقة الدرس 1 من 28

معمارية خادم الويب

كل طلب HTTP يخدمه تطبيقك يمر عبر خادم ويب. فهم كيفية تعامل هذا الخادم مع الاتصالات — وخاصة في ظل التزامن العالي — هو حجر الأساس لضبط أداء الإنتاج، والتخطيط للطاقة الاستيعابية، وتشخيص ارتفاعات الحركة. يقارن هذا الدرس النموذجَين المعماريَّين السائدَين اللذين يستخدمهما Nginx و Apache، ويشرح سبب أهمية الاختيار بينهما على نطاق واسع.

المشكلة الجوهرية: التزامن

يجب أن يتعامل خادم الويب مع آلاف الاتصالات المتزامنة. والسؤال هو: كيف يدير الخادم كل هذه الاتصالات دون استنفاد موارد النظام؟ برزت إجابتان مختلفتان جذرياً على مر العقود، لكلٍّ منهما مقايضاته الخاصة في الإنتاجية، واستخدام الذاكرة، وأنماط الفشل.

نموذج العملية لكل اتصال (Apache prefork)

يعتمد وحدة المعالجة المتعددة الافتراضية في Apache وهي prefork MPM على توليد مجموعة من العمليات العاملة عند بدء التشغيل. عندما يصل طلب، تلتقطه إحدى العمليات الخاملة وتمتلكه حصرياً حتى إرسال الاستجابة — بما في ذلك وقت الانتظار الكامل لاستجابة الخادم الخلفي وإنجاز استعلامات قاعدة البيانات أو تنزيل العميل البطيء للمحتوى.

Apache prefork process-per-connection model Client 1 Client 2 Client 3 Client N… Apache Master Process prefork MPM forks workers at startup MaxRequestWorkers Worker Process 1 busy (Client 1) Worker Process 2 busy (Client 2) Worker Process 3 busy (Client 3) No free worker → queue / refuse ~20-50 MB each process in RAM Each connection blocks one OS process for its entire duration
نموذج Apache prefork MPM: عملية نظام تشغيل واحدة لكل اتصال، محدودة بـ MaxRequestWorkers.

تحمل كل عملية عاملة مكدس وحدات Apache كاملاً في الذاكرة — عادةً 20 إلى 50 ميغابايت لكل عملية. مع إعداد MaxRequestWorkers 256 (وهو إعداد افتراضي شائع)، يمكن أن يستهلك الخادم من 5 إلى 12 غيغابايت من ذاكرة الوصول العشوائي قبل أن يُطلق عميل بطيء واحد مشكلة C10K الكلاسيكية: ينفد الخادم من العمليات وتُصطف الاتصالات الجديدة أو تُرفض بينما تجلس العمليات الموجودة خاملةً منتظرةً الإدخال/الإخراج.

نمط فشل "العامل العالق": يحتجز خادم خلفي بطيء (قاعدة بيانات، أو واجهة برمجية بطيئة) عاملاً في Apache محجوباً في استدعاء نظام. في هذه الأثناء تمتلئ ذاكرة الوصول العشوائي بعمليات خاملة لكنها محجوبة. تتجاوز حركة المرور حد MaxRequestWorkers؛ تفيض قائمة الانتظار الخاصة بنواة نظام التشغيل؛ ويتلقى العملاء رسالة رفض الاتصال. هذا ليس خللاً في الكود — بل هو حد معماري لنموذج العملية تحت حمل I/O مكثف.

النموذج القائم على الأحداث (Nginx)

بُني Nginx خصيصاً عام 2004 لحل مشكلة C10K. يستخدم معمارية مختلفة جذرياً: عدد صغير وثابت من عمليات العمال (عادةً واحدة لكل نواة CPU)، تشغّل كل منها حلقة أحداث غير محجوبة. بدلاً من تخصيص عملية نظام تشغيل لكل اتصال، يُعدّد عامل واحد آلاف الاتصالات باستخدام واجهات برمجة إشعارات الإدخال/الإخراج الخاصة بالنواة: epoll على Linux أو kqueue على BSD/macOS.

Nginx event-driven worker model Client 1 Client 2 Client 3 Client 4 Client 5 …10K+ Nginx Master Process reads config, signals Worker 1 CPU core 0 Event Loop (epoll) conn 1 → read req conn 2 → proxy write conn 5 → send body Worker 2 CPU core 1 Event Loop (epoll) conn 3 → TLS handshake conn 4 → idle keep-alive Upstream App / DB / API ~2-4 MB per worker fixed count = CPU cores Each worker handles thousands of connections without blocking
نموذج Nginx القائم على الأحداث: مجموعة ثابتة من العمال (واحد لكل نواة) يعالجون جميع الاتصالات عبر epoll.

عندما ينتظر Nginx استجابةً من خادم خلفي، لا ينام العامل — بل يسجّل المقبس مع epoll ويعالج على الفور اتصالاً آخر جاهزاً. تُخطر نواة نظام التشغيل العاملَ فور وصول البيانات. هذا يعني أن 10,000 اتصال keep-alive تكلّف نفس قدر وحدة المعالجة المركزية التي تكلفه 10 اتصالات نشطة تقريباً، لأن الاتصالات الخاملة لا تستهلك دورات تقريباً.

تهيئة نموذج العامل في Nginx

المفاتيح الرئيسية في /etc/nginx/nginx.conf:

# /etc/nginx/nginx.conf — top-level (main context) # Match the number of CPU cores available worker_processes auto; # auto = detected at runtime # Max simultaneous connections PER worker # Total capacity = worker_processes * worker_connections events { worker_connections 4096; # default 512 — raise on high-traffic servers use epoll; # explicit on Linux; auto-selected anyway multi_accept on; # accept all pending connections at once }

على جهاز رباعي النواة مع worker_connections 4096، يمكن لـ Nginx نظرياً التعامل مع 16,384 اتصالاً متزامناً بأربع عمليات نظام تشغيل تستهلك ربما 16 إلى 20 ميغابايت من ذاكرة الوصول العشوائي. أما إعداد Apache prefork لخدمة نفس الحمل فسيحتاج آلاف العمليات وجيجابايتات من ذاكرة الوصول العشوائي.

خط الأساس لضبط الإنتاج: اضبط worker_processes auto وارفع worker_connections إلى 4096 أو 8192 على خوادم تتجاوز 1 غيغابايت ذاكرة. ارفع أيضاً حد واصف الملفات لنظام التشغيل — كل اتصال يحتاج واصف ملف. أضف worker_rlimit_nofile 65535; إلى nginx.conf واضبط LimitNOFILE=65535 في وحدة systemd أو /etc/security/limits.conf.

أين لا يزال Apache يتفوق: Worker MPM و mod_php

Apache ليس متقادماً. إن worker MPM وevent MPM هجينان قائمان على الخيوط يقللان استخدام الذاكرة بشكل ملحوظ مقارنةً بـ prefork. والأهم من ذلك أن mod_php يضمّ مُفسِّر PHP مباشرةً في عملية Apache — تكامل محكم يتجنب الحمل الزائد لمدير عمليات FastCGI المنفصلة. لا تزال بيئات الاستضافة المشتركة وأكوام PHP القديمة كثيرة تعتمد هذا النموذج. أما للأعباء الجديدة فقد أصبح نمط Nginx + PHP-FPM هو المعيار السائد (يُغطَّى في الدرس الثالث).

النقاط الرئيسية

  • الإدخال/الإخراج المحجوب (Apache prefork): بسيط، معزول، لكنه يستهلك ذاكرة كثيرة ومحدود بعدد العمليات تحت حالات انتظار I/O.
  • حلقة الأحداث غير المحجوبة (Nginx): تتوسع لعشرات الآلاف من الاتصالات بذاكرة وصول عشوائي ضئيلة؛ العمل المكثف على CPU لا يزال يحجب العامل.
  • تجعل معمارية Nginx منه الخيار السائد للبروكسي العكسي وإنهاء TLS وخدمة الملفات الثابتة — جميعها أعباء يهيمن عليها انتظار I/O وليس CPU.
  • كلا الخادمَين أدوات صحيحة: اختر بناءً على خصائص حمل العمل وليس الانتماء القبلي.
ورقة C10K (دان كيغل، 1999) هي التحليل الأصلي الذي ألهم Nginx و Node.js. إن كنت تريد أن تفهم لماذا يهم الإدخال/الإخراج غير المحجوب على نطاق واسع، فلا تزال تستحق القراءة. الأنماط المعمارية التي تصفها تظل أساس كل خادم حديث عالي التزامن.