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

موازنة الحمل مع Nginx

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

موازنة الحمل مع Nginx

عندما يعجز خادم تطبيق واحد عن مواكبة حجم الطلبات — أو حين تحتاج إلى نشر بلا توقف مع تحمّل الأعطال — تضيف خوادم إضافية وتضع موازن حمل أمامها. يؤدي Nginx هذه المهمة ببراعة في نفس العملية التي تنهي فيها TLS وتخدم ملفاتك الثابتة. فهم خوارزميات upstream وآليات الفحص الصحي وخيارات تثبيت الجلسات هو الفارق بين موازن حمل يعمل في بيئة التجربة وآخر يصمد أمام حركة مرور يوم الجمعة السوداء.

كتلة upstream

كل شيء يبدأ بكتلة upstream. تسمّي المجموعة، وتُدرج الخوادم، وتختار الخوارزمية، ثم توجّه بـ proxy_pass إلى اسم تلك المجموعة من أي كتلة location.

http { upstream app_pool { # الخوارزمية الافتراضية: round-robin (لا حاجة لتوجيه) server 10.0.1.10:8080; server 10.0.1.11:8080; server 10.0.1.12:8080; } server { listen 80; server_name example.com; location / { proxy_pass http://app_pool; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection ""; # الاتصال الدائم مع upstreams } } }
اضبط دائماً proxy_http_version 1.1 وأفرغ Connection. الإصدار HTTP/1.0 (الافتراضي) يغلق اتصال TCP بعد كل طلب. HTTP/1.1 مع رأس Connection فارغ يأمر Nginx بإبقاء الاتصال مع upstream حياً لإعادة استخدام المقابس عبر الطلبات اللاحقة — مما يُحدث فارقاً كبيراً في الإنتاجية على نطاق واسع.

خوارزميات موازنة Upstream

تأتي النسخة المفتوحة من Nginx مع أربع خوارزميات. تضيف Nginx Plus المزيد (least_time وrandom مع خيارَين وzone-aware). اختر بناءً على طبيعة حمل عملك:

  • round-robin (الافتراضي) — يذهب كل طلب جديد إلى الخادم التالي في القائمة دورياً بالتساوي. يعمل بشكل جيد حين تكون الطلبات متجانسة في التكلفة والخوادم متطابقة. أضف معامل weight لتوجيه نسبة أكبر نحو العقد الأقوى.
  • least_conn — يذهب الطلب الجديد إلى الخادم الذي لديه أقل اتصالات نشطة. الخيار الصحيح للاتصالات طويلة الأمد (WebSockets، البث، استعلامات قواعد البيانات البطيئة) حيث تُراكم round-robin الطلبات على خادم واحد بينما تجلس الأخرى خاملة.
  • ip_hash — يحسب hash على IP العميل (أول ثلاثة أوكتيتات لـ IPv4) ويوجّه ذلك العميل دائماً إلى نفس upstream. شكل أساسي من الجلسات اللاصقة بدون ملفات تعريف ارتباط إضافية. ينكسر عندما يكون العملاء خلف NAT مشترك أو CDN.
  • hash $variable [consistent] — Hash على أي متغير Nginx: URI، قيمة cookie، رأس طلب. مع consistent يستخدم حلقة hash متسقة (Ketama) بحيث تضيف أو تزيل خادماً يعيد تعيين جزء صغير فقط من المفاتيح — مفيد للتوكيل إلى ذاكرات تخزين مؤقت upstream.
upstream api_pool { least_conn; server 10.0.1.10:3000 weight=3; # حصة مرور أكبر بثلاث مرات server 10.0.1.11:3000 weight=1; server 10.0.1.12:3000 backup; # يُستخدم فقط عند توقف الخوادم الأساسية } upstream cache_pool { hash $request_uri consistent; # نفس URI يصل دائماً إلى نفس عقدة Varnish server 10.0.2.10:6081; server 10.0.2.11:6081; }

الفحوصات الصحية السلبية

يُجري Nginx المفتوح فحوصات صحية سلبية: يراقب حركة المرور الحية ويُعلّم الخادم على أنه غير صحي فقط بعد أن يفشل في طلبات حقيقية. المعاملات الرئيسية تعيش في توجيه الخادم داخل upstream:

  • max_fails=3 — عدد الإخفاقات المتتالية قبل اعتبار الخادم معطلاً (الافتراضي 1).
  • fail_timeout=30s — مدة إيقاف الطلبات بعد الوصول للحد الأقصى، وأيضاً النافزة الزمنية التي تُحسب فيها max_fails (الافتراضي 10 ثوانٍ).
upstream app_pool { server 10.0.1.10:8080 max_fails=3 fail_timeout=30s; server 10.0.1.11:8080 max_fails=3 fail_timeout=30s; server 10.0.1.12:8080 max_fails=3 fail_timeout=30s; } # أكمل الفحوصات السلبية بتحديد الأخطاء التي تُعدّ إخفاقات: proxy_next_upstream error timeout http_502 http_503 http_504; proxy_next_upstream_tries 2; # أعد المحاولة مرة واحدة على الأكثر في upstream مختلف
الإعداد الافتراضي للإنتاج: proxy_next_upstream. بدونه تصل استجابة 502 من backend يتعطل مباشرة للمستخدم. بوجوده يُعيد Nginx المحاولة بشفافية على خادم آخر. قيّد المحاولات بالطلبات غير المتغيّرة أو كن حذراً: إعادة محاولة POST التزم بقاعدة البيانات ستضاعف الكتابة.

الفحوصات الصحية النشطة (Nginx Plus / OpenResty / وحدة فحص Upstream)

الفحوصات السلبية تكتشف الإخفاقات فقط على حركة المرور الحية. الفحوصات النشطة تختبر upstreams على فترات في الخلفية، فيُزال الخادم من الدوران قبل أن يصطدم به المستخدم. في Nginx المفتوح تحقق هذا بـ ngx_http_upstream_check_module (مُدمج وقت الترجمة) أو بأدوات مثل Consul + consul-template لإعادة كتابة كتلة upstream. توفره Nginx Plus بشكل أصلي عبر توجيه health_check:

# صيغة Nginx Plus (مرجع — Nginx OSS يحتاج وحدة طرف ثالث) upstream app_pool { zone app_pool 64k; # منطقة ذاكرة مشتركة مطلوبة للفحوصات النشطة server 10.0.1.10:8080; server 10.0.1.11:8080; server 10.0.1.12:8080; } server { location / { proxy_pass http://app_pool; health_check interval=5s fails=2 passes=3 uri=/healthz; } }

الجلسات اللاصقة

التطبيقات عديمة الحالة — حيث يمكن لأي خادم معالجة أي طلب — هي الأفضل دائماً في التصميم السحابي الأصلي. لكن التطبيقات القديمة كثيراً ما تخزّن بيانات الجلسة في ذاكرة العملية، مما يجعل توجيه العميل لنفس الخادم دائماً أمراً إلزامياً. يوضح الرسم البياني أدناه النموذجين:

Sticky Sessions vs Stateless Load Balancing Sticky Sessions (ip_hash / cookie) Client A Client B Client C Nginx Load Balancer Server 1 (A+B session) Server 2 (idle) Server 3 (C session) Risk: server failure = session loss Stateless (Shared Session Store) Client A Client B Client C Nginx Load Balancer Server 1 Server 2 Server 3 Redis / DB Session Store أي خادم يمكنه خدمة أي عميل أعطال الخوادم شفافة للمستخدم
يسار: الجلسات اللاصقة تثبّت العملاء على خوادم محددة — توقف الخادم يُفقد الجلسات. يمين: التصميم عديم الحالة يخزّن الجلسات خارجياً فيمكن لأي خادم خدمة أي عميل.

توفر Nginx Plus توجيه sticky cookie. في Nginx المفتوح تستخدم ip_hash أو hash على قيمة cookie مستخرجة بـ $cookie_sessionid:

upstream app_sticky { # الخيار 1: ip_hash — بسيط، بلا ملفات تعريف ارتباط إضافية، ينكسر خلف CDN/NAT ip_hash; server 10.0.1.10:8080; server 10.0.1.11:8080; server 10.0.1.12:8080; } upstream app_cookie_sticky { # الخيار 2: hash على cookie جلسة التطبيق — أدق من ip_hash hash $cookie_PHPSESSID consistent; server 10.0.1.10:8080; server 10.0.1.11:8080; server 10.0.1.12:8080; } # Nginx Plus — sticky cookie أصلي (لا مكافئ في OSS): # sticky cookie srv_id expires=1h domain=.example.com path=/;
الجلسات اللاصقة تخفي قنبلة موقوتة في قابلية التوسع. إذا توقف خادم ما، يحصل كل عميل مثبّت عليه على خطأ جلسة بغض النظر عن الخوادم الصحية الأخرى. على نطاق الشركات الكبرى، الحل الصحيح دائماً هو تخزين حالة الجلسة خارجياً في Redis أو مجموعة قواعد بيانات. الجلسات اللاصقة أداة ترحيل — استخدمها لإطلاق إصدار غير جاهز بالكامل، ثم خطّط لإزالتها.

Keepalive للـ Upstream وتجميع الاتصالات

بالنسبة للخدمات عالية الإنتاجية، تتراكم تكلفة بناء اتصالات TCP. توجيه keepalive في كتلة upstream يأمر Nginx بتخزين مجموعة من الاتصالات الخاملة لكل upstream وإعادة استخدامها عبر الطلبات. هذا مختلف عن keepalive للعميل ويُقلل الكمون بشكل كبير على الخدمات التي تُجري آلاف الطلبات في الثانية.

upstream app_pool { least_conn; server 10.0.1.10:8080; server 10.0.1.11:8080; server 10.0.1.12:8080; keepalive 32; # خزّن حتى 32 اتصالاً خاملاً لكل worker keepalive_timeout 60s; # أغلق الاتصالات الخاملة بعد 60 ثانية keepalive_requests 1000; # أعد تدوير الاتصال بعد 1000 طلب }

مراقبة سلوك موازن الحمل

وحدة stub_status في Nginx تعرض صفحة حالة بسيطة. للحصول على مقاييس أكثر تفصيلاً على مستوى upstream — الاتصالات النشطة لكل خادم، الحالة الصحية، الطلبات الموجَّهة — تحتاج Nginx Plus أو وحدة طرف ثالث مثل nginx-module-vts. في الإنتاج، تُرسل معظم الفرق سجلات Nginx إلى خط معالجة المقاييس (Prometheus + nginx-prometheus-exporter أو Datadog) وتُنبّه على معدلات 5xx للـ upstream وبيرسنتيلات وقت الاستجابة بدلاً من استطلاع صفحة الحالة.

أضف $upstream_addr إلى سجل الوصول. يُسجّل خادم backend الذي عالج كل طلب، مما يجعل التحقق من عمل التوزيع أمراً سهلاً وربط الأخطاء بـ upstream محدد خلال حوادث الإنتاج. أضف $upstream_response_time إلى جانبه للكشف عن الخوادم ذات الكمون الشاذ.