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

التخزين المؤقت والضغط

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

التخزين المؤقت والضغط

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

التخزين المؤقت للوكيل: كيف يخزن Nginx استجابات الخادم الأصلي

حين يقف Nginx أمام خادم أصلي (تطبيق Node.js، أو مجموعة PHP-FPM، أو تطبيق Rails، أو خدمة مصغرة)، يستطيع تخزين استجابة الخادم الأصلي على القرص أو في الذاكرة، ثم يعيد تقديم تلك النسخة المخزنة للعملاء التاليين دون أن يُلامس الخادم الأصلي البتة. هذا ما يُعرف بـالتخزين المؤقت للوكيل، ويُعدَّل عبر التوجيهَين proxy_cache_path وproxy_cache.

يُوضع إعلان التخزين داخل كتلة http {} لأنه مورد مشترك بين جميع كتل الخوادم. تُنشئ معامل levels هيكلًا ثنائي المستوى من الأدلة حتى لا يعاني نظام الملفات من ملايين الملفات في دليل واحد. يُخصص keys_zone قطاعًا من الذاكرة المشتركة يحمل الاسم المحدد لفهرس البيانات الوصفية للتخزين — لا لأجسام الاستجابات ذاتها. يحدّ max_size من استهلاك القرص وينشّط آلية الإخلاء الأقل استخدامًا عند الوصول إليه. أما inactive فتُنهي صلاحية الإدخالات التي لم يُطلب عليها أحد مؤخرًا.

# /etc/nginx/nginx.conf (كتلة http) proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app_cache:10m max_size=2g inactive=60m use_temp_path=off; # الكتابة مباشرة في دليل التخزين — يتجنب خطوة النسخ server { listen 80; server_name api.example.com; location /api/ { proxy_pass http://app_upstream; proxy_cache app_cache; proxy_cache_valid 200 302 10m; # تخزين 200/302 لمدة 10 دقائق proxy_cache_valid 404 1m; # تخزين 404 لمدة دقيقة (التخزين السلبي) # إظهار حالة التخزين للعملاء — لا غنى عنه عند التشخيص add_header X-Cache-Status $upstream_cache_status; # خدمة نسخة قديمة إن كان الخادم الأصلي بطيئًا proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_background_update on; proxy_cache_lock on; # حماية من القطيع الرعبي — طلب واحد فقط يُجدد # التخزين لمفتاح معين في آنٍ واحد } }
$upstream_cache_status هي أداتك الأولى في التشخيص. القيم الممكنة: HIT إصابة، MISS إخفاق، BYPASS تجاوز، EXPIRED منتهية الصلاحية، STALE قديمة، UPDATING قيد التحديث، REVALIDATED مُعاد التحقق منها، UNCACHEABLE غير قابلة للتخزين. راقب هذا الرأس في الإنتاج للتحقق من أن التخزين يعمل فعلًا قبل الاحتفال.

مفاتيح التخزين: ما الذي يُشكّل إدخالًا فريدًا في التخزين

يبني Nginx مفتاح التخزين افتراضيًا من كامل URI مع سلسلة الاستعلام: $scheme$proxy_host$request_uri. وهذا صحيح لمعظم واجهات REST. أما نقاط النهاية المصادق عليها، فتنويع المفتاح ليشمل هوية المستخدم ضرورة — أو الأفضل تعطيل التخزين كليًا عليها. وإن كان المحتوى يتباين بين الهاتف والحاسوب، أدرج تصنيف User-Agent ضمن المفتاح.

proxy_cache_key "$scheme$host$uri$is_args$args"; # تجاوز التخزين عند وجود رأس Authorization (طلبات مصادقة) proxy_cache_bypass $http_authorization; proxy_no_cache $http_authorization; # تجاوز التخزين إن أرسل الخادم الأصلي Cache-Control: no-store / no-cache proxy_cache_bypass $upstream_http_cache_control; proxy_no_cache $upstream_http_pragma;

إبطال التخزين

إبطال التخزين مسألة معروفة بصعوبتها. عمليًا، ثمة ثلاثة مناهج على مستوى Nginx: الأول، ترك الإدخالات تنتهي صلاحيتها وفق TTL المحدد في proxy_cache_valid — مناسب للمحتوى الذي يتغير وفق جدول معروف. الثاني، استخدام وحدة ngx_cache_purge (متضمنة في Nginx Plus التجاري، أو تُرفق بنسخة المصدر المفتوح) لإرسال طلب PURGE. الثالث، وسم الإدخالات بمعرّفات عبر رأس Surrogate-Key أو Cache-Tag وإبطالها دفعةً واحدة — نمط شائع في Cloudflare وFastly وبيئات Varnish.

للحالة الشائعة لنشر CI/CD: نفّذ طلب تنظيف من خط الأنابيب مباشرةً بعد بدء تشغيل الإصدار الجديد. لا تعتمد على انتهاء TTL حين تنشر تغييرًا جوهريًا.

Nginx proxy cache request flow Client Browser / API Nginx Proxy Cache Check cache key Disk Cache /var/cache/nginx Upstream App Server Node / PHP / etc. Request HIT Read/Write MISS — proxy Response + store مسار الإصابة مسار الإخفاق
تدفق تخزين الوكيل في Nginx: الإصابة (HIT) تُعيد النسخة المخزنة فورًا؛ الإخفاق (MISS) يُحيل الطلب للخادم الأصلي ويخزن الاستجابة.

ضغط Gzip

يُقلل الضغط حجم جسم الاستجابة قبل مغادرتها Nginx، مستبدلًا قدرًا بسيطًا من المعالج بعرض نطاق ترددي أقل وزمن تحميل مُتصوَّر أسرع. للحمولات النصية (HTML، JSON، CSS، JS) يمكن توقع انخفاض بنسبة 60–80% في الحجم. وحدة gzip المدمجة في Nginx مفعّلة افتراضيًا في معظم التوزيعات.

# /etc/nginx/conf.d/compression.conf (أو داخل كتلة http) gzip on; gzip_comp_level 5; # 1-9؛ 5 هو نقطة التوازن — العائد يتراجع فوق 6 gzip_min_length 256; # تجاهل الاستجابات الصغيرة؛ الضغط لا يستحق العناء gzip_proxied any; # ضغط الاستجابات حتى للطلبات المُوجَّهة عبر وكيل gzip_vary on; # إضافة Vary: Accept-Encoding حتى تخزن الشبكات CDN الإصدارين gzip_types text/plain text/css text/javascript application/javascript application/json application/xml application/rss+xml image/svg+xml font/ttf font/otf application/font-woff application/font-woff2; # لا تضغط الأنواع المضغوطة أصلًا — يُهدر المعالج ويزيد الحجم أحيانًا # .jpg .jpeg .png .gif .mp4 .webm .pdf .gz .zip غائبة عن gzip_types أعلاه
لا تضغط مطلقًا أنواع الملفات المضغوطة أصلًا. JPEG وPNG وMP4 وWebM وملفات zip مضغوطة بطبيعتها. تشغيلها عبر gzip عند وقت الطلب يُهدر المعالج وقد ينتج استجابةً أكبر حجمًا. احرص دائمًا على استثناء الأنواع الثنائية من gzip_types.

ضغط Brotli

حقق Brotli (جوجل، 2015) نسب ضغط أفضل بـ15–25% مقارنةً بـgzip بتكلفة معالجة مماثلة للمحتوى النصي. وتدعمه جميع المتصفحات الرئيسية منذ عام 2017. على جانب Nginx، تحتاج وحدة ngx_brotli التي لا تُجمَّع ضمن الحزمة الافتراضية في معظم التوزيعات. على Ubuntu/Debian يمكنك تثبيت libnginx-mod-brotli أو البناء من المصدر. أما في بيئات Cloudflare، فيُطبَّق ضغط Brotli تلقائيًا على حافة CDN.

# تحميل الوحدة الديناميكية (المسار يختلف حسب التوزيعة — تحقق بـ nginx -V) load_module modules/ngx_http_brotli_filter_module.so; load_module modules/ngx_http_brotli_static_module.so; # داخل http {}: brotli on; brotli_comp_level 6; # 0-11؛ 6 مستوى جيد للاستخدام العام brotli_types text/plain text/css text/javascript application/javascript application/json image/svg+xml; # تقديم ملفات .br المضغوطة مسبقًا حين توجد بجانب الملف الأصلي # (ممتاز للأصول الثابتة المبنية بواسطة Webpack / Vite / esbuild) brotli_static on;
اضغط الأصول الثابتة مسبقًا عند وقت البناء. تشغيل brotli --best (أو CompressionPlugin في Webpack) أثناء CI ينتج ملفات .br و.gz بجانب الملفات الأصلية. مع تفعيل gzip_static on وbrotli_static on، يقدم Nginx هذه الملفات المضغوطة مسبقًا دون أي عمل للمعالج — مما يُخفف استهلاك المعالج على الحافة بشكل جذري عند أحمال المرور العالية. هذا ممارسة معيارية في شركات كـShopify وGitHub لخطوط أنابيب أصولهم الثابتة.

الجمع بين التخزين المؤقت والضغط

حين يكون كلاهما مفعَّلَين، يضغط Nginx الاستجابة بعد استردادها من الخادم الأصلي وقبل كتابتها في التخزين، لكن فقط إن كان العميل يقبل الترميز. يعني هذا أن النسخة المخزنة مضغوطة بالفعل — واستجابات الإصابة التالية تُقدَّم دون أي عمل للمعالج. تحقق من ذلك بـcurl -H "Accept-Encoding: gzip" -I https://api.example.com/endpoint وراقب رأسَي Content-Encoding وX-Cache-Status معًا.

قائمة تحقق الإنتاج الأساسية: (1) تأكد من X-Cache-Status: HIT في رؤوس الاستجابة. (2) تأكد من Content-Encoding: br أو gzip. (3) تأكد من نجاح nginx -t قبل إعادة التحميل. (4) راقب /var/log/nginx/error.log بحثًا عن أخطاء أذونات التخزين — يجب أن يمتلك مستخدم العامل دليل التخزين. (5) اضبط proxy_cache_bypass $cookie_session لأي نقاط نهاية جلسة مصادَق عليها لمنع تلوث بيانات المستخدمين.