الحلول · فصل ٦

الحل التاني — TTL Jitter ضد الـ Cache Avalanche

TTL jitter · cache avalanche · mass expiry · cold start · bulk load
🎯 الهدف: تفهم ليه الـ TTLs متخلصش كلها مع بعض، وتضيف jitter بسطر واحد.

المشكلة: كله بيخلص مع بعض

  • السيناريو: عملت deploy، أو الكاش اتمسح، أو job ملا 100,000 key مرة واحدة الساعة 3:00.
  • وكلهم بنفس الـ TTL: مثلًا EX 3600.
  • النتيجة: الساعة 4:00 بالظبط، كل الـ keys بتخلص في نفس الثانية، وكل الـ requests بتعمل miss مع بعض.
  • ده اسمه Cache Avalanche: انهيار. مش key واحد زي الـ stampede، ده آلاف keys مرة واحدة.
  • وبيتكرر: لأنهم اتملوا تاني مع بعض، فهيخلصوا تاني مع بعض بعد ساعة. موجة كل ساعة.

الحل: TTL عشوائي شوية

بدل ما كل الـ keys تاخد 3600 ثانية بالظبط، كل key ياخد رقم عشوائي بين 3240 و3960 مثلًا (±10٪).
jitter.mjs
// ±ratio حوالين الرقم الأساسي: jitter(300, 0.1) → بين 270 و330export function jitter(baseSec, ratio = 0.1) {  const delta = baseSec * ratio;  return Math.round(baseSec - delta + Math.random() * 2 * delta);}// الاستخدامawait redis.set(key, JSON.stringify(value), "EX", jitter(300));

جرّبتها بـ jitter(300, 0.2) عشر مرات وطلعت:

output
338 294 274 251 247 316 325 335 266 254
  • لو 100,000 key اتملوا مع بعض بـ jitter(300, 0.2): هيخلصوا على مدى دقيقتين (من 240 لـ 360 ثانية) بدل ثانية واحدة.
  • الحمل على الداتابيز: بدل 100,000 miss في ثانية، هتبقى حوالي 830 في الثانية على مدى الدقيقتين دول.

تختار النسبة إزاي؟

  • نسبة 10٪ إلى 20٪: كفاية في أغلب الحالات.
  • كل ما الـ TTL أطول: نفس النسبة بتفرّق على وقت أطول، فـ 10٪ من ساعة = 6 دقايق كاملة.
  • لو الـ TTL قصير جدًا: (مثلًا 10 ثواني) ممكن تحتاج نسبة أكبر أو رقم ثابت زي ± 3 ثواني.
  • خلي بالك من الحد الأقصى: لو البيانات لازم متبقاش أقدم من 5 دقايق، خلي الـ jitter ينقص بس: بين 4 و5 دقايق.
jitter-down.mjs
// مش هيعدّي الـ base أبدًا: بين base × (1 - ratio) و baseexport const jitterDown = (baseSec, ratio = 0.2) =>  Math.round(baseSec * (1 - Math.random() * ratio));

الـ Jitter مش حل للـ hot key

  • الـ jitter بيفرّق keys كتير عن بعض. بيحل الـ avalanche.
  • بس key واحد سخن؟ مهما كان الـ TTL بتاعه عشوائي، في الآخر هيخلص في لحظة، وكل الـ 10,000 request هيعملوا miss.
  • فإنت محتاج الاتنين: jitter لكل الـ keys، وlock (فصل ٥) أو refresh قبل الـ expiry (فصل ٧) للـ hot keys.
المشكلةالحل الأساسي
Stampede / Breakdownkey واحد سخن خلصlock / single-flight / stale-while-revalidate
Avalanchekeys كتير خلصت مع بعض أو الكاش وقعjitter + warming + حماية الداتابيز

إمتى الـ Avalanche بيحصل؟

  • بعد deploy أو restart للكاش: الكاش فاضي (cold start)، فكل حاجة miss في نفس الوقت. حلها: cache warming (فصل ٧) + lock + rate limit على الداتابيز.
  • بعد bulk load: job ملا آلاف keys مع بعض. حلها: jitter.
  • بعد ما غيّرت version الـ keys: product:v1 بقى product:v2، فكل المنتجات miss مرة واحدة. حلها: اعمل الـ deploy تدريجي، أو سخّن الـ keys الجديدة الأول.
  • لما Redis نفسه وقع: كل الـ requests رايحة للداتابيز. ده أخطر واحد (فصل ٩).

متنساش الـ Jitter في حاجات تانية

  • الـ retries: لو كل الـ clients عملوا retry بعد ثانية بالظبط، هيرجعوا كلهم مع بعض. خلي الـ backoff عشوائي (فصل ٨).
  • الـ cron jobs: لو 100 instance عاملين refresh كل دقيقة بالظبط، ضيف تأخير عشوائي.
  • الـ polling بتاع المستنيين على الـ lock: زي sleep(delay / 2 + Math.random() * delay) في فصل ٥.

خلّصت الفصل لما

🧠 اختبر نفسك
١) ملّيت 50,000 key بـ EX 600 بالظبط. إيه اللي هيحصل بعد 10 دقايق؟
كلهم هيخلصوا في نفس الثانية، فـ 50,000 miss مرة واحدة على الداتابيز (avalanche)، وهيتكرر كل 10 دقايق.
٢) jitter(600, 0.1) بيطلّع أرقام بين كام وكام؟
بين 540 و660 ثانية.
٣) ضفت jitter لكل الـ TTLs. الـ hot key اتحل؟
لأ. الـ hot key لوحده لما يخلص الكل هيعمل miss. محتاج lock أو refresh قبل الـ expiry.