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

البروكسي العكسي والخوادم الأمامية

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

البروكسي العكسي والخوادم الأمامية

في الشركات الكبيرة، نادرًا ما يقدّم Nginx استجابات التطبيقات مباشرةً — بل يعمل كبوابة أمامية تُنهي اتصالات العملاء وتُعيد توجيه الطلبات إلى مجموعة من خوادم التطبيقات التي تعمل خلفه. هذا هو البروكسي العكسي: يتحدث العميل إلى Nginx، ويتحدث Nginx إلى تطبيقك، ولا يرى العميل التطبيق مطلقًا بشكل مباشر. كل شيء من البنية التحتية لواجهة Google إلى طبقة الحافة في Netflix يتبع هذا النمط.

إتقان proxy_pass وكتلة upstream هو المهارة الأهم في Nginx لمهندس DevOps. ستستخدمها مع تطبيقات Node.js، وخدمات Python/Django/Flask، وتطبيقات Laravel/PHP-FPM عبر FastCGI، وخلفيات gRPC، وشبكات الخدمات المصغّرة، وخوادم WebSocket.

proxy_pass: التوجيه الأساسي

يُخبر proxy_pass Nginx بوجهة توجيه الطلب المطابق. عند استقبال Nginx لاتصال فإنه:

  1. يقرأ ترويسات الطلب الكاملة من العميل (أو أول مخزن مؤقت من الجسم).
  2. يفتح اتصال TCP إلى الهدف الأمامي (أو يُعيد استخدام اتصال keepalive من مجموعته).
  3. يُعيد توجيه الطلب المُعاد بناؤه مع إضافة ترويسات البروكسي.
  4. يُعيد بث استجابة الخادم الأمامي إلى العميل مع التخزين المؤقت أو التمرير المباشر حسب إعداداتك.

الشرطة المائلة الأخيرة في proxy_pass مهمة جدًا. بدون شرطة مائلة أخيرة، يُرسَل URI الكامل (بما في ذلك بادئة location) إلى الخادم الأمامي. أما مع الشرطة المائلة (أو أي مسار آخر)، فإن Nginx يحذف البادئة المطابقة ويُلحق الباقي.

server { listen 80; server_name api.example.com; # بدون شرطة مائلة أخيرة: # GET /api/users → يستقبل الخادم الأمامي /api/users location /api/ { proxy_pass http://127.0.0.1:3000; } # مع شرطة مائلة أخيرة (إعادة كتابة URI): # GET /api/users → يستقبل الخادم الأمامي /users location /api/ { proxy_pass http://127.0.0.1:3000/; } }
مزلق إنتاجي — مفاجآت حذف URI: سلوك الشرطة المائلة الأخيرة يُربك المهندسين باستمرار. إذا كان تطبيق Node.js أو Django يتوقع مسارات بدون بادئة /api، فأنت بحاجة إلى الشرطة المائلة. أما إذا كان يتوقع المسار الكامل، فاحذفها. الأخطاء الناتجة عن التطابق الخاطئ تُنتج خطأ 404 يبدو وكأنه خطأ في توجيه Nginx لكنه في الحقيقة رفض التطبيق للـ URI المُعاد كتابته. اختبر دائمًا باستخدام curl -v وراقب ما يستقبله التطبيق فعلًا.

ترويسات البروكسي الأساسية

عندما يُعيد Nginx توجيه طلب، فإنه يُنشئ اتصال TCP جديدًا إلى الخادم الأمامي. بشكل افتراضي يرى الخادم الأمامي عنوان IP الاسترجاعي لـ Nginx كعميل — لا عنوان IP الزائر الحقيقي. يجب عليك تمرير السياق الأصلي بشكل صريح في الترويسات.

location / { proxy_pass http://127.0.0.1:8080; # تمرير IP العميل الحقيقي حتى تتمكن سجلات التطبيق ومحددات المعدل من رؤيته proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; # إخبار التطبيق بالبروتوكول الذي استخدمه العميل (http أو https) proxy_set_header X-Forwarded-Proto $scheme; # تمرير ترويسة Host الأصلية حتى يُنشئ التطبيق عناوين URL صحيحة proxy_set_header Host $host; # يستخدم Nginx HTTP/1.0 مع الخوادم الأمامية بشكل افتراضي — أجبره على 1.1 لـ keepalive والنقل المجزأ proxy_http_version 1.1; # مطلوب عند استخدام اتصالات keepalive مع الخوادم الأمامية proxy_set_header Connection ""; }
X-Forwarded-For مقابل X-Real-IP: X-Forwarded-For قائمة مفصولة بفواصل تنمو كلما مر الطلب عبر بروكسيات (client, proxy1, proxy2). يجب على تطبيقك قراءة عنوان IP الأقصى يسارًا الذي يثق به — لا الأقصى يمينًا الذي يمكن للمهاجم تزويره. أما X-Real-IP فهي ترويسة ذات قيمة واحدة يضبطها Nginx على $remote_addr (عنوان IP الاتصال الذي استقبله Nginx، وهو IP العميل الحقيقي عندما يكون Nginx أول بروكسي). في معظم الإعدادات ذات الطبقة الواحدة، يكون استخدام X-Real-IP في كود التطبيق أبسط.

مجموعات الخوادم الأمامية: التوسع إلى أكثر من خادم

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

Nginx upstream pool routing requests to multiple app servers Client Browser / CLI Nginx Reverse Proxy upstream pool App Server 1 :8001 App Server 2 :8002 App Server 3 :8003 Database Shared round-robin
مجموعة Nginx الأمامية تُوزع طلبات العملاء عبر ثلاث نُسخ من خادم التطبيق.
# تعريف المجموعة — يمكن لـ Nginx موازنة التحميل عبر جميع الخوادم المُدرجة upstream app_servers { # الخوارزمية الافتراضية: round-robin (كل طلب يذهب إلى الخادم التالي) server 10.0.1.10:8000; server 10.0.1.11:8000; server 10.0.1.12:8000; # الإبقاء على 32 اتصالًا خاملًا مفتوحًا لكل عامل نحو كل خادم أمامي # (يتجنب overhead مصافحة TCP عند كل طلب) keepalive 32; } server { listen 80; server_name app.example.com; location / { proxy_pass http://app_servers; # الاسم يطابق كتلة upstream proxy_http_version 1.1; proxy_set_header Connection ""; 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_connect_timeout 5s; # المدة الزمنية للانتظار حتى يرسل الخادم الأمامي ترويسات الاستجابة proxy_read_timeout 60s; # المدة الزمنية للانتظار حتى يستقبل الخادم الأمامي جسم الطلب proxy_send_timeout 60s; } }

معاملات خادم upstream

كل توجيه server داخل upstream يقبل معاملات اختيارية تمنحك تحكمًا دقيقًا في توزيع حركة المرور ومعالجة الأعطال:

  • weight=N — أرسل N أضعاف الطلبات إلى هذا الخادم مقارنةً بخوادم الوزن 1. يُستخدم للأجهزة غير المتجانسة.
  • max_fails=N — علّم الخادم على أنه غير متاح بعد N فشل متتالٍ خلال نافذة fail_timeout (الافتراضي: فشل واحد، نافذة 10 ث).
  • fail_timeout=Xs — المدة التي يبقى فيها الخادم مُعلَّمًا على أنه غير متاح قبل إعادة محاولة Nginx.
  • backup — استخدم هذا الخادم فقط عند توقف جميع الخوادم الأساسية. نمط الاحتياط الساخن الكلاسيكي.
  • down — علّم هذا الخادم دائمًا على أنه غير متاح (مفيد أثناء عمليات النشر المتدرج دون تعديل الإعداد الحي).
upstream app_servers { server 10.0.1.10:8000 weight=3; # أساسي، مضيف عالي المواصفات server 10.0.1.11:8000 weight=1; # أساسي، مضيف منخفض المواصفات server 10.0.1.12:8000 backup; # يُستخدم فقط إذا فشل كلا الخادمين الأساسيين server 10.0.1.13:8000 max_fails=3 fail_timeout=30s; # نافذة فشل مخصصة keepalive 32; }
نصيحة إنتاجية — keepalive الأمامي ليس اختياريًا على نطاق واسع: بدون keepalive، يفتح Nginx اتصال TCP جديدًا إلى الخادم الأمامي لكل طلب يتم توجيهه. عند 1000 طلب/ث يعني ذلك 1000 مصافحة TCP في الثانية: ترتفع الكمون وتستنزف واصفات الملفات. اضبط keepalive 32 (أو أعلى بناءً على تزامنك) واقرنه دائمًا بـ proxy_http_version 1.1 وproxy_set_header Connection "" — الأخير يمسح ترويسة Connection: close التي يرسلها HTTP/1.0 بشكل افتراضي، والتي قد تُغلق الاتصال بعد كل استجابة.

توجيه WebSocket

تبدأ WebSockets كاتصال HTTP/1.1 ثم تجري مصافحة الترقية — يرسل العميل Connection: Upgrade وUpgrade: websocket، ويستجيب الخادم بـ 101 Switching Protocols. بعد ذلك يصبح الاتصال قناة TCP مستمرة وثنائية الاتجاه تعيش لدقائق أو ساعات.

بشكل افتراضي Nginx هو بروكسي طلب-استجابة. يُسقط ترويسة Upgrade ويُغلق الاتصال، مما يُعطل WebSockets كليًا. يجب عليك تعيين ترويسات الترقية وتمريرها بشكل صريح:

http { # يسمح map لـ Nginx بضبط ترويسة Upgrade بشكل شرطي. # إذا أرسل العميل ترويسة Upgrade، مررها؛ وإلا أرسل سلسلة فارغة. map $http_upgrade $connection_upgrade { default upgrade; "" close; } server { listen 80; server_name ws.example.com; location /ws/ { proxy_pass http://websocket_backend; # الترويستان اللتان تُتيحان ترقيات WebSocket proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; # اتصالات WebSocket طويلة الأمد — ارفع مهلة القراءة # 0 = لا مهلة (استخدم بحذر؛ يُفضل قيمة كبيرة مثل 1h) proxy_read_timeout 3600s; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } upstream websocket_backend { # لـ WebSockets، يُثبّت ip_hash العميل على نفس الخادم # (مهم إذا كان التطبيق يخزن حالة الجلسة في العملية) ip_hash; server 10.0.1.20:3000; server 10.0.1.21:3000; } }
الجلسات اللاصقة مقابل التصميم عديم الحالة: يُثبّت ip_hash كل عنوان IP للعميل على نفس الخادم الأمامي طوال فترة الاتصال. يحل هذا مشكلة حالة WebSocket في العملية لكنه يُعطل موازنة التحميل الجغرافي ويفشل بشكل سيئ في التعامل مع الأعطال (إذا مات عقدة، تعيد اتصال جميع عملائها المُثبَّتين وقد تصطدم بخادم بارد). الحل الجاهز للإنتاج هو تخارج حالة الجلسة إلى Redis (مهايئ socket.io، Action Cable، إلخ) حتى يتمكن أي خادم أمامي من معالجة أي اتصال — ثم استخدام round-robin بسيط.

دمج المواقع الثابتة والمُوجَّهة

يدمج كتلة خادم إنتاجية كاملة عادةً كلا النمطين: يقدم Nginx الأصول الثابتة بسرعة السلك ويُوجّه كل شيء آخر إلى مجموعة التطبيقات. هذا النمط الذي ستراه في كل دليل نشر لـ Laravel وRails وDjango وNext.js:

  • الأصول الثابتة (CSS، JS، الصور): يقدمها Nginx مباشرةً مع ترويسات تخزين مؤقت طويل الأمد.
  • مسارات API والصفحات: تُوجَّه إلى مجموعة الخوادم الأمامية.
  • نقطة نهاية WebSocket (/ws/): تُوجَّه مع ترويسات الترقية ومهلة قراءة طويلة.
  • نقطة نهاية الفحص الصحي: تُوجَّه إلى التطبيق أو يُجيب عليها Nginx مباشرةً.

هذا الفصل يعني أن خوادم تطبيقاتك لا تُهدر أي وحدة معالجة مركزية على ملفات لا تتغير أبدًا — فرق ملموس على نطاق واسع عندما تمثل الأصول الثابتة 80–90% من حجم الطلبات.