هذا المقال يتطلب مستوى متوسط
Cache Stampede: لما الـ cache يخلص فاتورة DB بتقفز 14x في ثواني
لو الـ Redis cache بتاعك بيخلص فجأة، ولقيت 4,200 طلب بيضربوا الـ PostgreSQL في ثانيتين، الـ DB مش غلطانة. الظاهرة دي اسمها Cache Stampede، وحلها مش زيادة الذاكرة ولا scaling أفقي للـ DB. الحل في 8 سطور Python، وبيمنع الـ outage قبل ما يحصل.
المشكلة باختصار
تخيّل عندك endpoint بيرجّع قائمة "أكثر المنتجات مبيعاً". الـ query على PostgreSQL بياخد 380 مللي ثانية. عشان كده بتخزّن النتيجة في Redis لمدة 5 دقايق. كل تمام، الـ endpoint بيرد في 12 مللي ثانية. لكن في الثانية اللي بيخلص فيها الـ TTL، لو عندك 800 طلب متزامن، كلهم بيلاقوا الـ cache فاضي ويشتغلوا الـ query في نفس اللحظة. الـ DB بتشتغل 800 marble ثقيلة بدل واحدة. P99 latency بيقفز من 14ms لـ 6 ثواني، والمستخدمين بيشوفوا 504.
مثال للمبتدئ: شباك التذاكر
تخيّل حفلة موسيقية بـ 5,000 مقعد. شباك التذاكر فاضي طول النهار، مفيش زباين. الساعة 12 بالليل بالظبط، التذاكر بتفتح للحجز. 12,000 شخص ضغطوا على زرار "احجز" في نفس اللحظة. مش بس الموقع بيقع. السيرفر اللي ورا الموقع بياخد ضربة 12,000 طلب وهو متصمّم لـ 100 طلب في الثانية. كل واحد بيستنّى 4 دقايق علشان يلاقي رسالة خطأ.
Cache Stampede هو نفس الكلام بالظبط. الـ cache هو شباك التذاكر السريع. لما يخلص (الـ TTL ينتهي)، كل الطلبات المتراكمة بتروح للـ DB البطيئة في نفس اللحظة. الـ DB مش معمولة للحمل ده.
التعريف العلمي
Cache Stampede (وأحياناً اسمه Thundering Herd أو Dog-piling) هي ظاهرة بتحصل لما يكون فيه N طلب متزامن على نفس الـ cache key، فالـ key تنتهي صلاحيته (expire)، فالـ N طلب كلهم يحاولوا يولّدوا القيمة من المصدر الأصلي (الـ DB غالباً) في نفس اللحظة. الورقة المرجعية للحل اسمها "Optimal Probabilistic Cache Stampede Prevention" لـ Vattani و Chierichetti و Lowenstein، نُشرت في VLDB 2015، وقدّمت طريقة XFetch لتوزيع التجديد عبر الزمن إحصائياً.
السبب الجذري: ليه TTL ثابت = كارثة
الـ TTL الثابت بيخلق "نقطة انتهاء جماعية". لو 800 طلب وصلوا في الثانية اللي قبل آخر ثانية ولقوا الـ cache مكتوب من 4 دقايق و 59 ثانية، مفيش مشكلة. لو وصلوا في الثانية اللي بعدها، 800 طلب بيكتشفوا غياب القيمة في نفس اللحظة، وكلهم بيشغّلوا الـ DB query.
الافتراض الخفي إن الطلبات موزعة بالتساوي عبر الزمن. الافتراض ده غلط في أي تطبيق production. الـ traffic عادة بيجي في bursts، خصوصاً وقت الـ traffic peaks زي الساعة 9 الصبح أو 8 المساء.
الحل الأول: Single-Flight Lock
الفكرة: أول طلب بيكتشف غياب الـ cache بياخد قفل (lock) في Redis بـ SET NX. باقي الطلبات لمّا يلاقوا اللوك ما يشتغلوش الـ query، يستنّوا 50ms ويعيدوا قراءة الـ cache. لو الـ cache رجعت، يرجّعوها للمستخدم.