التخزين المؤقّت وشبكات التوصيل

أين تخزّن مؤقتاً: الطبقات المختلفة

18 دقيقة الدرس 2 من 10

أين تخزّن مؤقتاً: الطبقات المختلفة

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

The Four Caching Layers: Client, CDN, Application, Database Client CDN Edge App Server Cache Server Database Browser HTTP Cache CDN PoP Edge Cache App Server In-Process Redis / Memcached Primary DB Query Cache miss miss miss miss 0 ms (ذاكرة) 1–5 ms (أقرب PoP) ~0.1 ms (RAM محلي) 0.5–2 ms (شبكة) 5–50 ms (قرص/فهرس) — زمن الاستجابة النموذجي عند الإصابة لكل طبقة — Per User Per Region Per Process Shared Per DB خاص مشترك محلي فقط على مستوى الكلستر داخلي Cache-Control Service Worker Cloudflare CloudFront Guava Cache Caffeine Redis Memcached InnoDB Buffer Pool الطلب يمرّ: Client → CDN → App Server → Cache Server → Database الإصابة في أيّ طبقة توقف الرحلة — الإصابة المبكرة أرخص
طبقات التخزين المؤقت الأربع من الأقرب إلى المستخدم (Client) حتى الأعمق (Database)، مع زمن الاستجابة النموذجي والنطاق وأبرز الأدوات في كل طبقة.

الطبقة الأولى — ذاكرة التخزين المؤقت على جهاز العميل

أسرع ذاكرة تخزين مؤقت هي تلك التي لا تغادر جهاز المستخدم أصلاً. تخزّن المتصفحات استجابات HTTP في ذاكرة تخزين محلية (قرص أو RAM) تتحكم بها بالكامل ترويسات الاستجابة التي يُرسلها خادمك:

  • Cache-Control: max-age=86400 — يجوز للمتصفح تقديم هذه الاستجابة لمدة تصل إلى 24 ساعة دون الذهاب إلى الشبكة إطلاقاً.
  • ETag / Last-Modified — إعادة التحقق الشرطية: يسأل المتصفح "هل تغيّر هذا المحتوى؟" ولا يُنزّل الجسم إلا إذا ردّ الخادم بالإيجاب (الاستجابة 304 Not Modified توفّر عرض النطاق حتى على المورد القديم).

التخزين المؤقت على جهاز العميل خاص بكل مستخدم. ملف CSS مخزّن في متصفح مستخدم واحد لا يفيد زائراً آخر في زيارته الأولى. لكن للزوار العائدين يلغي رحلات الشبكة بالكامل — زمن الاستجابة يصبح صفراً عند الإصابة فعلياً. تُشير Google إلى أن نحو 85 % من موارد الويب قابلة للتخزين المؤقت، ومع ذلك كثير من المواقع ترسل Cache-Control: no-store على كل شيء بشكل افتراضي، مما يُهدر أداءً هائلاً.

استخدم بصمة محتوى للأصول الثابتة. سمّ الملفات كـ app.a3f91c.js (هاش المحتوى في اسم الملف). ثم قدّمها بـ Cache-Control: max-age=31536000, immutable. حين يتغير الملف تتغير رابطه — فيجلب المتصفح الإصدار الجديد فوراً، بينما يظل الإصدار القديم مخزّناً إلى أجل غير مسمى لمن يملكه بالفعل.

الطبقة الثانية — CDN / ذاكرة التخزين على الحافة

تدير شبكة توصيل المحتوى (CDN) مئات نقاط الحضور (PoPs) حول العالم. حين يطلب مستخدم في سنغافورة موقعك المستضاف في فرانكفورت، يُقدّم CDN الاستجابة المخزّنة مؤقتاً من نقطة حضور في سنغافورة — مما يُخفّض زمن الاستجابة من ~200 مللي ثانية إلى ~5 مللي ثانية.

ذاكرات CDN مشتركة: كل مستخدم يصل إلى نقطة الحضور نفسها يستفيد من ذاكرة دافئة أعدّها زائر سابق. وهي مثالية للأصول والصفحات المتطابقة لجميع المستخدمين: الصور، الخطوط، حزم JavaScript، وصفحات HTML غير الشخصية، واستجابات API القابلة للمشاركة.

تشغّل CDNs الكبرى — Cloudflare وAWS CloudFront وFastly — منطقاً على الحافة (edge functions, edge workers) يتيح لك التخصيص الخفيف أو اختبار A/B دون رحلة ذهاب-إياب إلى خادم الأصل.

CDN مقابل ذاكرة المتصفح. ذاكرة المتصفح خاصة (مستخدم واحد). ذاكرة CDN مشتركة (مستخدمون كثيرون من منطقة واحدة). كلتاهما تستخدم توجيهات Cache-Control ذاتها، لكن CDNs تحترم أيضاً s-maxage (TTL للذاكرة المشتركة) الذي يتيح تعيين TTL أقصر للمتصفحات مع الاحتفاظ بـ TTL أطول عند الـ CDN.

الطبقة الثالثة — ذاكرة التخزين على مستوى التطبيق

حين يصل الطلب إلى خوادمك تملك خيارين فرعيين:

  1. ذاكرة التخزين داخل العملية (In-Process) — خريطة هاش داخل العملية الجارية. مكتبات مثل Caffeine وGuava Cache (Java) أو functools.lru_cache (Python) تخزّن أزواج مفتاح-قيمة في كومة التطبيق نفسه. الإصابة تكلف بضع مئات من النانو ثانية — تكاد تكون مجانية. المقايضة: كل خادم تطبيق يمتلك ذاكرته المستقلة. على 10 خوادم قد تُخزَّن البيانات ذاتها 10 مرات، والإلغاء صعب عبر الخوادم.
  2. ذاكرة التخزين عبر بروكسي عكسي — أدوات كـ Varnish أو Nginx يمكنها الجلوس أمام تطبيقك وتخزين استجابات HTTP كاملة. التطبيق نفسه لا يعمل أصلاً عند الإصابة.

ذاكرات In-Process الأنسب لـ بيانات المرجع الثابتة أو نادرة التغيير — رموز البلدان، الأعلام، كائنات الإعداد — حيث تكلفة البيانات القديمة منخفضة وتكرار القراءة عالٍ جداً.

ذاكرة In-Process + التوسع الأفقي = تناسق مكسور. إذا حدّث المستخدم A سجلاً ووصل طلبه للخادم 1، قد يقدّم الخادم 2 بيانات مخزّنة قديمة للمستخدم B حتى انتهاء TTL. للبيانات القابلة للتعديل ذات متطلبات التناسق القوية، تجاوز طبقة In-Process واذهب مباشرة إلى ذاكرة مشتركة.

الطبقة الرابعة — ذاكرة التخزين الموزعة (Redis / Memcached)

طبقة تخزين مؤقت مخصصة — عادةً Redis أو Memcached — تجلس بين تطبيقك وقاعدة البيانات. بخلاف ذاكرات In-Process، تشترك جميع خوادم التطبيق في نفس كلستر التخزين المؤقت. مفتاح يكتبه أي خادم يصبح قابلاً للقراءة فوراً من أي خادم آخر.

هذه هي ذاكرة التخزين الأكثر استخداماً في الإنتاج:

  • الإصابة تكلف ~0.5–2 مللي ثانية (استدعاء شبكي) بدلاً من 5–50 مللي ثانية لاستعلام قاعدة بيانات.
  • على مقياس تويتر (بيانات 2013)، بيانات تغريدة رائجة تُجلب عشرات الآلاف من المرات في الثانية — لا يمكن لقاعدة البيانات تلبية ذلك؛ Memcached يستطيع.
  • Redis يضيف هياكل بيانات غنية (مجموعات مرتبة، تدفقات، pub/sub) تجعله يتجاوز مجرد التخزين المؤقت — تحديد المعدل، تخزين الجلسات، لوحات المتصدرين.

الطبقة الخامسة — ذاكرة التخزين على مستوى قاعدة البيانات

محرك قاعدة البيانات نفسه يمتلك ذاكرات تخزين مؤقت يمكنك ضبطها دون تغيير أي كود في التطبيق. InnoDB Buffer Pool في MySQL هو منطقة RAM تحتفظ بالصفحات التي جرى الوصول إليها مؤخراً (صفوف + صفحات فهارس). حين تكون صفحة استعلام موجودة بالفعل في مجمع المخزن المؤقت، لا تتم أي عمليات I/O للقرص — يُعيد المحرك النتائج من الذاكرة. ضبط هذا الحجم بصواب (عادةً 70–80 % من RAM المتاح على خادم DB مخصص) هو أحد أعلى تحسينات أداء قاعدة البيانات مردوداً.

تقدّم بعض قواعد البيانات أيضاً ذاكرة نتائج الاستعلام (مثل Query Cache الذي أُزيل من MySQL، وplan cache في PostgreSQL). هذه إلى حدٍّ كبير متروكة في المحركات الحديثة لأنها تسببت في مشاكل تعارض الأقفال — طبقات التخزين الصريحة أعلاه تمنحك تحكماً أكبر.

اختيار الطبقة (أو الطبقات) المناسبة

في الواقع العملي تستخدم طبقات متعددة في آنٍ واحد. طلب صورة منتج قد يسير هكذا: ذاكرة المتصفح (0 مللي ثانية) ← إخفاق ← ذاكرة CDN (4 مللي ثانية) ← إخفاق ← Redis (1 مللي ثانية) ← إخفاق ← مجمع مخزن DB (2 مللي ثانية) ← إخفاق ← قراءة من القرص (10 مللي ثانية). الهدف أن تُوقَف الغالبية العظمى من الطلبات عند إحدى الطبقات المبكرة والأرخص.

Request Flow: Cache Hit vs Cache Miss at each layer مسار الطلب — التوقف عند الطبقة الصحيحة User Browser Client HTTP Cache CDN Edge PoP App Server In-process Redis Shared Cache Database Primary DB miss miss miss miss HIT: 0 ms HIT: 1–5 ms Redis HIT: 0.5–2 ms DB query: 5–50 ms ← أسرع/أرخص ——————————— أبطأ/أغلى →
يمرّ الطلب عبر الطبقات من اليسار إلى اليمين حتى الإصابة. الإصابة في أي طبقة تُعيد الاستجابة فوراً. كل طبقة تُجاوَز تُضيف زمن استجابة.
الطبقات تكاملية لا متنافسة. الأصول الثابتة تنتمي لطبقتَي العميل والـ CDN. استجابات API المحسوبة تنتمي لطبقة Redis. البيانات المرجعية التي تُحمَّل مع كل طلب تنتمي للذاكرة داخل العملية. خطط استعلام قاعدة البيانات تنتمي لـ InnoDB Buffer Pool. النظام الناضج يستخدم الجميع.

في الدرس التالي سنتعمق في كيفية دخول البيانات وخروجها من كل ذاكرة — استراتيجيات Read-Through وWrite-Through وWrite-Behind.