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

مشروع: إضافة طبقة تخزين مؤقت

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

مشروع: إضافة طبقة تخزين مؤقت

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

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

المرحلة الأولى — فهم عبء العمل

قبل كتابة سطر واحد من إعداد الكاش، قِس ما لديك فعلاً. تكشف عملية التوصيف لمنصتنا ما يلي:

  • 50 مليون طلب/يوم (~580 طلب/ث في المتوسط، ~2,500 طلب/ث في الذروة)
  • نسبة القراءة/الكتابة: 95:5 — 95% من العمليات هي قراءات
  • أكثر 5 نقاط نهاية في عدد استعلامات قاعدة البيانات: الخلاصة (الجدول الزمني الرئيسي)، تفاصيل المنشور، ملف المستخدم، اقتراحات البحث، الموضوعات الرائجة
  • أبطأ الاستعلامات: تجميع الخلاصة (~120 مللي ثانية)، البحث النصي الكامل (~80 مللي ثانية)، محرك التوصيات (~200 مللي ثانية)
  • متطلبات حداثة البيانات: الخلاصات تتحمل قِدَماً حتى 60 ث؛ الموضوعات الرائجة حتى 5 دقائق؛ ملفات المستخدمين حتى 30 ث
المبدأ الأساسي: يبدأ تصميم الكاش بالقياس لا بالافتراض. الاستعلام الذي يبدو بطيئاً قد يُستدعى نادراً؛ استعلام سريع يُستدعى ملايين المرات قد يكون عنق الزجاجة الحقيقي. قِس أولاً، ثم حسّن.

المرحلة الثانية — تحديد مرشحات الكاش

لا يستحق كل شيء التخزين المؤقت. طبّق مرشح الأسئلة الثلاثة على كل كائن بيانات:

  1. هل تُقرأ أكثر بكثير مما تُكتب؟ (نسبة قراءة/كتابة > 10:1 قاعدة جيدة)
  2. هل تكلفة حسابها أو جلبها مرتفعة؟ (تتضمن ربطات، أو تجميعات، أو استدعاءات API خارجية، أو استنتاج تعلم آلي)
  3. هل تتحمل نافذة قصيرة من القِدَم؟ (حتى 30 ثانية TTL تُلغي قطيع الرعد على صف ساخن)

تطبيق هذه المرشحات على منصتنا يُنتج خطة الكاش التالية:

  • الجدول الزمني الرئيسي (الخلاصة): يُحسب مسبقاً لكل مستخدم ويُخزَّن في Redis. يُقرأ آلاف المرات لكل جلسة؛ يُعاد بناؤه فقط عند نشر منشور جديد. TTL: 60 ث + إبطال مدفوع بالأحداث عند نشر منشور.
  • صفحة تفاصيل المنشور: لا تتغير بعد أول نشر (النص لا يتغير). تُخزَّن إلى أجل غير مسمى؛ تُبطَل فقط عند التعديل أو الحذف. TTL: 24 ساعة.
  • ملخص ملف المستخدم: الاسم، رابط الصورة الرمزية، عدد المتابعين. نادراً ما يتغير. TTL: 30 ث. يُخزَّن في طبقة التطبيق.
  • الموضوعات الرائجة: قائمة مُرتَّبة يُعيد حسابها عمل خلفية كل 5 دقائق. تُخزَّن أحدث نتيجة؛ تُستبدل عند كل إعادة حساب. TTL: 5 دقائق.
  • اقتراحات الإكمال التلقائي للبحث: جدول بحث صغير لكل بادئة، يُحسب ليلاً. يُخزَّن إلى أجل غير مسمى؛ يُبطَل عند إعادة البناء الليلية. يُخدَّم من حافة CDN لتوصيل عالمي بزمن استجابة معدوم.
  • الأصول الثابتة (JS، CSS، صور): لا تتغير لكل نشر (تجزئة المحتوى في اسم الملف). تُخزَّن في CDN مع Cache-Control: public, max-age=31536000, immutable.
نصيحة التصميم: قسّم الكاش حسب نوع الكائن لا حسب نقطة النهاية. نقطة نهاية واحدة (مثل صفحة تفاصيل المنشور) قد تجمع عدة كائنات مخزَّنة — نص المنشور، وملف المؤلف، وعدد التعليقات — لكل منها TTL وقواعد إبطال مختلفة.

المرحلة الثالثة — اختيار الاستراتيجيات لكل نوع كائن

أشكال البيانات المختلفة تستدعي استراتيجيات تخزين مؤقت مختلفة. إليك الربط لنظامنا:

  • الخلاصة / الجدول الزمني: Cache-Aside + Write-Through عند التوزيع. عند نشر مستخدم لمنشور، يُوزّع مسار الكتابة معرف المنشور على قائمة الخلاصة المخزَّنة لكل متابع في Redis (write-through). عند القراءة، إذا كان مفتاح الخلاصة مفقوداً (بدء بارد أو منتهي الصلاحية)، يُحمَّل من قاعدة البيانات ويُعاد تعبئته (cache-aside / تحميل كسول).
  • تفاصيل المنشور: Cache-Aside مع TTL طويل. أول قراءة تُعبّئ الكاش. القراءات اللاحقة تضرب الكاش. عند التعديل، يُحذف مفتاح الكاش صراحةً (إبطال عند الكتابة). وكذلك عند الحذف.
  • ملف المستخدم: Cache-Aside مع TTL قصير. TTL قصير (30 ث) يعني أن البيانات القديمة تُصلح نفسها بسرعة. إبطال صريح فقط للحقول الحرجة مثل تعليق الحساب.
  • الموضوعات الرائجة: Write-Around / تحديث خلفي. وظيفة مجدولة تُعيد حساب القائمة وتكتب مباشرة إلى الكاش كل 5 دقائق، متجاوزةً مسار القراءة العادي. العملاء يقرؤون دائماً من الكاش؛ لا توجد حالة إخفاق لهذا المفتاح بعد الإحماء.
  • الأصول الثابتة: كاش CDN مع TTL طويل + كسر الكاش باسم الملف المُجزَّأ. نشر جديد = اسم ملف جديد = إخفاق كاش مرة واحدة عالمياً؛ الرابط القديم يستمر في تخديم الملف القديم من حافة CDN حتى تنتهي صلاحيته طبيعياً.

المرحلة الرابعة — تصميم الطوبولوجيا الكاملة

بعد تحديد الاستراتيجية لكل نوع كائن، يمكننا الآن رسم المعمارية الكاملة. يحتوي النظام على ثلاث طبقات كاش متمايزة تعمل بتناسق:

Full caching topology for a read-heavy social content platform Browser HTTP cache CDN Edge static + pages PoPs worldwide assets / pages Load Balancer L7 routing CDN miss App Server 1 local mem cache App Server 2 local mem cache Redis Cluster feeds · profiles trending · sessions cache read/write Primary DB writes + cache miss cache miss writes Read Replica cache-miss reads replication Background Worker (cron/queue) warm trending Legend cache read/write cache miss → DB request routing background warm
طوبولوجيا الكاش الكاملة: كاش HTTP في المتصفح ← حافة CDN (أصول ثابتة + صفحات) ← كاش ذاكرة محلية في خادم التطبيق ← مجموعة Redis ← نسخة للقراءة ← قاعدة البيانات الرئيسية. عمال الخلفية يُسخّنون المفاتيح الحساسة للوقت مسبقاً.

طبقات الكاش الثلاث النشطة هي:

  • الطبقة الأولى — حافة CDN: تخدم الأصول الثابتة (JS، CSS، صور) والصفحات القابلة للتخزين المؤقت (مثل صفحات المنشورات العامة مع Cache-Control: s-maxage=60). زمن الاستجابة: <20 مللي ثانية في أي مكان على الأرض. نسبة الإصابة للأصول الثابتة: ~99%. نسبة الإصابة للصفحات: ~70–80%.
  • الطبقة الثانية — كاش ذاكرة محلية في التطبيق (L1): كل خادم تطبيق يحمل كاشاً صغيراً داخل العملية (مثلاً 256 ميجابايت). يتجنب حتى رحلة شبكة Redis للمفاتيح الساخنة للغاية (مثل الإعداد العالمي، وأعلام الميزات، وأبرز موضوع رائج). TTL: 5–30 ث. البيانات خاصة بكل عملية، لذا الاتساق مريح — استخدمها فقط للبيانات التي يقبل فيها التباين القصير بين العقد.
  • الطبقة الثالثة — مجموعة Redis (L2): الكاش الموزع المركزي المشترك بين جميع خوادم التطبيق. يحمل خلاصات المستخدمين، وكائنات المنشورات، وملفات المستخدمين، ورموز الجلسات، وعدادات الحد المعدلي. زمن الاستجابة: ~0.5–2 مللي ثانية. هدف نسبة الإصابة: >90%.

المرحلة الخامسة — تصميم الإبطال

تخبرنا الطوبولوجيا أين تعيش الأشياء؛ تصميم الإبطال يخبرنا متى وكيف نجعلها قديمة. إليك خطة الإبطال لكل كائن:

Invalidation flow for write operations — post publish and profile update Invalidation: What Happens When a User Publishes a Post User publishes POST /posts App Server validate + write DB Primary DB row inserted Message Queue post.published event publish event Fanout Worker reads follower list Redis Cluster invalidate / update feeds DEL feed:* CDN Purge API purge cached pages purge /feed/* Result after invalidation Next feed request: Redis miss → DB read → repopulate cache Subsequent feed requests: Redis hit (<1 ms). Stale window: <2 s.
تدفق الإبطال عند نشر منشور: التطبيق يكتب إلى قاعدة البيانات، ينشر حدثاً في طابور رسائل، ويقوم عامل التوزيع بحذف مفاتيح Redis للخلاصات القديمة وإزالة صفحات CDN — محققاً الاتساق في غضون ثانيتين.

قرارات التصميم الرئيسية في مخطط الإبطال هذا:

  • الإبطال المدفوع بالأحداث عبر طابور الرسائل يفصل مسار الكتابة عن إدارة الكاش. طلب HTTP الذي يعالج POST لا يُحجب في انتظار عمليات Redis DEL عبر آلاف مفاتيح المتابعين — يحدث التوزيع بشكل غير متزامن.
  • احذف ولا تُحدّث عند التوزيع. لجداول زمنية للمستخدمين تضم آلاف المتابعين، دفع المنشور الجديد إلى كل خلاصة في Redis بشكل متزامن قد يستغرق ثوانٍ. حذف مفتاح الخلاصة آني. إعادة التعبئة تحدث بشكل كسول عند القراءة التالية.
  • إزالة CDN للصفحات العامة. صفحة تفاصيل منشور مخزَّنة في حافة CDN يجب إزالتها عند تعديل المنشور أو حذفه. يوفر كبار موفري CDN (Cloudflare، Fastly، Akamai) API للإزالة لهذا الغرض تحديداً. خطط لزمن استجابة إضافي لاستدعاء إزالة CDN (~50–200 مللي ثانية) في مسار الكتابة.

المرحلة السادسة — التحقق بالأرقام

تصميم الكاش لا يكون موثوقاً إلا إذا استطعت إظهار الأثر المتوقع. لنجري الحسابات لمنصتنا:

  • قبل: ذروة 2,500 طلب/ث، متوسط 5 استعلامات قاعدة بيانات لكل طلب = 12,500 استعلام/ث يضربون قاعدة البيانات. مع قاعدة بيانات رئيسية واحدة مقيّدة بـ~8,000 استعلام/ث، النظام يتجاوز الطاقة الاستيعابية بالفعل.
  • بعد (نسبة إصابة CDN 70% للصفحات الديناميكية): 2,500 × 0.30 = 750 طلب/ث تصل إلى خوادم التطبيق.
  • بعد (نسبة إصابة Redis 92% عبر جميع أنواع الكائنات): 750 × 0.08 = 60 طلب/ث تصل إلى قاعدة البيانات لإخفاقات الكاش، بالإضافة إلى ~125 طلب/ث للكتابات = ~185 استعلام/ث إجمالاً. قاعدة البيانات الآن عند <3% من طاقتها الاستيعابية.
  • زمن الاستجابة: إصابة CDN: ~15 مللي ثانية. إصابة Redis: ~3 مللي ثانية. إخفاق الكاش: ~45 مللي ثانية. المتوسط الموزون P50: ~5 مللي ثانية (منخفض من ~140 مللي ثانية بدون كاش).
نسب الإصابة الواقعية: تحقيق نسبة إصابة Redis 92% يتطلب أن تتناسب أكثر 1,000 كائن طلباً مع ميزانية ذاكرة Redis بمريحية. بمتوسط حجم 1 كيلوبايت للكائن، 1,000 كائن ساخن = 1 ميجابايت — تافه. التحدي يكمن في ضمان صحة انتهاء الصلاحية والإبطال، لا في حجم الذاكرة.
لا تتخطى خطوة المراقبة. بعد نشر طبقة الكاش، قِس Redis باستخدام INFO stats (keyspace_hits / keyspace_misses) وأنشئ تنبيهاً إذا انخفضت نسبة الإصابة عن 85%. انخفاض مفاجئ يدل على قطيع رعد، أو تغيير في نمط المفاتيح، أو TTL مضبوط بشكل خاطئ يُبطل بعدوانية مفرطة.

الخلاصة: عملية تصميم الكاش الكاملة

إضافة طبقة تخزين مؤقت إلى نظام كثيف القراءة ليست قراراً واحداً — بل هي عملية هندسية من ست خطوات:

  1. قِس عبء العمل — قِس نسبة القراءة/الكتابة، وأهم الاستعلامات، وزمن الاستجابة لكل نقطة نهاية.
  2. صفّ مرشحات الكاش — نسبة قراءة عالية، مكلف الإنتاج، يتحمل قِدَماً قصيراً.
  3. خصّص استراتيجية لكل نوع كائن — cache-aside، أو write-through، أو تحديث خلفي بناءً على أنماط الوصول ومتطلبات الاتساق.
  4. صمّم الطوبولوجيا المُطبَّقة — متصفح ← CDN ← L1 محلية ← L2 موزع ← قاعدة البيانات. كل طبقة تخفف الحِمل عن التالية.
  5. صمّم الإبطال صراحةً — المدفوع بالأحداث أفضل من TTL فقط للبيانات القابلة للتغيير؛ توزيع منفصل للكتابات ذات التوزيع العالي.
  6. تحقق بالأرقام وراقب في الإنتاج — نسبة الإصابة، وزمن الاستجابة P50/P99، واستعلامات قاعدة البيانات/ث قبل وبعد.

النتيجة لمنصة المحتوى الاجتماعي لدينا: نظام كان يُشبع قاعدة بياناته في ذروة الحِمل يعمل الآن عند أقل من 3% من طاقة قاعدة البيانات مع متوسط P50 لزمن الاستجابة يبلغ 5 مللي ثانية — تحسين بمعامل 28 — مع الحفاظ على الاتساق النهائي القوي في نافذة قِدَم تبلغ ثانيتين للخلاصات و30 ثانية للملفات الشخصية.

اكتمل الدرس!

تهانينا! لقد أكملت جميع الدروس في هذا البرنامج التعليمي.