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

تقديم المحتوى الثابت وخلفيات PHP والتطبيقات

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

تقديم المحتوى الثابت وخلفيات PHP والتطبيقات

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

كيف يحدد Nginx ما يفعله بالطلب

كل طلب وارد يصطدم بأولًا بـمحرك مطابقة الموقع (Location Matching) في Nginx. يقيّم Nginx عنوان URI مقابل كتل location وفق ترتيب أولوية صارم: المطابقة التامة (=)، ثم البادئة المفضّلة (^~)، ثم التعابير النمطية (~ و~*)، وأخيرًا مطابقة البادئة العادية. الكتلة الفائزة تحدد ما إذا كانت المعالجة محلية (ملف ثابت) أم بالإحالة إلى خلفية ديناميكية.

الموجهان اللذان يتحكمان في التوزيع هما:

  • root / alias — يربطان عنوان URI بمسار على القرص ويُقدّم Nginx الملف مباشرةً. لا عمليات تطبيق متورطة إطلاقًا.
  • fastcgi_pass / proxy_pass — يُحيلان الطلب إلى عملية منفصلة (PHP-FPM أو Gunicorn أو Node.js أو Laravel Octane) ويُعيدان تدفق الاستجابة إلى العميل.
لماذا نفصل الثابت عن الديناميكي؟ الملفات الثابتة — CSS وJS والصور والخطوط — لا تتغير بين طلب وآخر. بإمكان Nginx تقديمها بكامل سرعة الشبكة (مئات الآلاف من الطلبات في الثانية على نواة واحدة) دون أي عملية PHP أو Node. خلط الثابت بالديناميكي في الخلفية يُهدر المعالج والذاكرة على عمليات لا تُضيف قيمةً.

تقديم المحتوى الثابت

النمط المعياري للموقع الثابت أو الجزء الأصولي من تطبيق ويب يبدو هكذا. يُحدد الموجه root المسار الأساسي؛ يُضيف Nginx عنوان URI إليه للحصول على المسار الكامل للملف.

server { listen 80; server_name assets.example.com; root /var/www/myapp/public; index index.html; location / { # جرّب الملف المطابق، ثم فهرس المجلد، ثم أرجع 404. try_files $uri $uri/ =404; } # رؤوس تخزين مؤقت طويلة الأمد للأصول المُبصومة (مثل app.a3f92c.js) location ~* \.(js|css|woff2|woff|ttf|svg|png|jpg|webp|ico|gif)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; # أوقف تسجيل الأصول — يُقلل كتابات القرص } }

تفاصيل جوهرية يجب استيعابها:

  • try_files $uri $uri/ =404 — يتحقق Nginx من وجود الملف ثم المجلد (مع الفهرس)، ويُعيد 404 صريحًا بدلًا من الانتقال لكتلة أخرى. أنهِ دائمًا كتل الثابت بهذه الطريقة.
  • expires 1y; add_header Cache-Control "public, immutable" — للأصول المُبصومة بالمحتوى (اسم الملف يحتوي على هاش)، وجّه المتصفحات وشبكات CDN لتخزينها للأبد. يتغير هاش الملف عند تغيير المحتوى، لذا لا خطر من ذاكرة تخزين قديمة.
  • access_log off — طلبات الأصول قد تُمثل 80% من سجلات الوصول مع قيمة تشخيصية تكاد تعدم. إيقافها يُقلل كتابات القرص ويُبقي سجلاتك مقروءة.

الإحالة إلى خلفية PHP عبر FastCGI

لا يتحدث PHP بروتوكول HTTP مباشرةً — بل يتحدث FastCGI، وهو بروتوكول ثنائي مُصمَّم لعمليات العمّال طويلة الأمد. يُشغّل PHP-FPM (مدير عمليات FastCGI) مجموعة من عمّال PHP يستمعون على Unix socket (أو منفذ TCP). يُترجم Nginx كل طلب HTTP إلى رسالة FastCGI، يُرسلها إلى FPM، ويُعيد تدفق الاستجابة إلى العميل.

server { listen 80; server_name app.example.com; root /var/www/myapp/public; index index.php index.html; # الأصول الثابتة — يُقدمها Nginx مباشرةً، لا علاقة لـ FPM بها location ~* \.(js|css|png|jpg|webp|svg|woff2|ico)$ { expires 1y; add_header Cache-Control "public, immutable"; try_files $uri =404; } # باقي الطلبات: جرّب الملف، ثم أعد الكتابة إلى index.php (إطار العمل / SPA) location / { try_files $uri $uri/ /index.php?$query_string; } # أحل ملفات PHP إلى PHP-FPM عبر Unix socket (أسرع من TCP على المضيف نفسه) location ~ \.php$ { # الأمان: ارفض طلبات ملفات PHP غير الموجودة على القرص try_files $uri =404; fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_index index.php; # معاملات FastCGI المعيارية — ضرورية لرؤية PHP لمتغيرات الخادم include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; # خزّن الاستجابة في ذاكرة Nginx قبل إرسالها للعميل fastcgi_buffer_size 128k; fastcgi_buffers 4 256k; } }
فخ إنتاجي — اجتياز المسار عبر SCRIPT_FILENAME: بدون حارس try_files $uri =404 داخل location ~ \.php$، سيُمرر Nginx طلبات ملفات PHP غير موجودة إلى FPM على أي حال. قد ينفّذ FPM عندها ملف PHP مضمّنًا في صورة مُرفوعة (مثل uploads/shell.jpg/../../evil.php). هذه كانت ثغرة اجتياز المسار الشهيرة في Nginx مع PHP-FPM. الحارس try_files غير قابل للتفاوض في الإنتاج.

الإحالة إلى خلفية التطبيق عبر proxy_pass

تتحدث خلفيات Node.js وPython (Gunicorn/uvicorn) وRuby (Puma) وGo وJava بروتوكول HTTP مباشرةً. يُحيل Nginx إليها باستخدام proxy_pass — موجه أبسط ومستوى أعلى من fastcgi_pass.

upstream app_backend { # عدة عمّال للتوزيع الدوري للحمل (يتوسع الدرس 6 في هذا) server 127.0.0.1:3000; server 127.0.0.1:3001; keepalive 32; # أعد استخدام الاتصالات بالخلفية — يتجنب مصافحة TCP لكل طلب } server { listen 80; server_name api.example.com; location / { proxy_pass http://app_backend; # أخبر الخلفية بعنوان IP العميل الحقيقي والبروتوكول 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_send_timeout 60s; proxy_read_timeout 60s; # إعدادات التخزين المؤقت — تُتيح لـ Nginx استيعاب العملاء البطيئين proxy_buffering on; proxy_buffer_size 16k; proxy_buffers 4 32k; } }
اضبط دائمًا X-Forwarded-For وX-Forwarded-Proto. بدونهما يرى تطبيقك عنوان IP الاسترجاع الخاص بـ Nginx كعميل ويظن أن كل الحركة HTTP. هذا يُعطّل التحديد بالمعدل المبني على IP، وتحديد الموقع الجغرافي، وسجلات التدقيق، ومنطق إعادة التوجيه إلى HTTPS داخل أطر العمل كـ Laravel وDjango. اضبط إطار عملك للوثوق برؤوس البروكسي — في Laravel: TRUSTED_PROXIES=* أو اذكر عناوين IP لـ Nginx.

المسار الكامل للطلب — تمثيل بصري

يُظهر الرسم البياني التالي المسارين جنبًا إلى جنب: طلب أصل ثابت لا يغادر Nginx إطلاقًا، وطلب ديناميكي يمر عبر FPM أو خلفية بروكسي قبل وصول الاستجابة إلى المتصفح.

Static vs Dynamic Request Path through Nginx Browser Client HTTP Nginx Location Matching Engine :80 / :443 Static Path Disk /var/www/public File bytes Dynamic Path FastCGI / proxy_pass App Backend PHP-FPM / Node / Gunicorn / Go Database MySQL / Redis HTTP response Static (Nginx serves from disk) Dynamic (forwarded to backend)
الطلبات الثابتة يُقدمها Nginx بالكامل من القرص؛ الطلبات الديناميكية تُحال إلى عملية خلفية (PHP-FPM أو Node أو Gunicorn) قد تستعلم بدورها قاعدة بيانات.

أشيع حالات الفشل وكيفية تشخيصها

  • 502 Bad Gateway — وصل Nginx إلى PHP-FPM أو خلفية التطبيق لكن الخلفية رفضت الاتصال أو تعطلت. تحقق بـ sudo systemctl status php8.3-fpm وsudo tail -f /var/log/php8.3-fpm.log. أشيع الأسباب: مجموعة FPM مستنفدة (كل العمال مشغولون) أو مسار الـ socket في إعداداتك لا يطابق المسار الفعلي في /etc/php/8.3/fpm/pool.d/www.conf.
  • 504 Gateway Timeout — الخلفية حية لكنها تستغرق وقتًا طويلًا. انتهت مهلة proxy_read_timeout أو fastcgi_read_timeout. إما أن الطلب بطيء بطبيعته (استعلام DB طويل، استدعاء API خارجي) أو الخلفية في حالة توقف. تحقق من سجلات الاستعلام البطيء وتتبع التطبيق أولًا.
  • 404 على ملفات PHP — غالبًا عدم تطابق root. شغّل nginx -T | grep root وقارن المسار الكامل بما على القرص فعلًا عبر ls -la.
  • تغييرات الملفات الثابتة غير ظاهرة — المتصفح أو CDN يخزّن النسخة القديمة. إن لم تكن الأصول مُبصومة، غيّر expires إلى off أو 1h أثناء التطوير.
أداة تشخيص: curl -I http://localhost/path/to/file من الخادم نفسه يتجاوز طبقات DNS وCDN ويُظهر بالضبط ما يُعيده Nginx. أضف -v للرؤوس الكاملة. اجمعه مع sudo nginx -t للتحقق من الإعداد قبل إعادة التشغيل ومع sudo tail -f /var/log/nginx/error.log لمراقبة الأخطاء مباشرةً.