Database Engineering

البقاء على قيد الحياة أثناء ذروة حركة المرور: دليل تقني لمنع هجمات طوابير ذاكرة التخزين المؤقت في Redis

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

بالنسبة للمطورين من المستوى المتوسط إلى المتقدم، لا يُعد تعيين وقت انتهاء صلاحية قصير حلاً قابلاً للتطبيق بسبب متطلبات معدل النجاح العالية للتطبيقات الحديثة. بدلاً من ذلك، يجب علينا تنفيذ استراتيجيات قوية للتخفيف من هذا الخطر. في هذا المنشور، سنستكشف تقنيتين متكاملتين: القفل الموزع والانتقائية الاحتمالية المبكرة.

تشريح هجمة ذاكرة التخزين المؤقت

لحل المشكلة، يجب علينا أولاً فهم آلية عملها. فكر في سيناريو حيث يكون لصفحة منتج شائعة وقت انتهاء صلاحية (TTL) للذاكرة المؤقتة يبلغ 60 ثانية. عند t=60s، تنتهي صلاحية المفتاح. إذا طلب 10,000 مستخدم هذه الصفحة في نفس المللي ثانية بالضبط، فسيقوم جميع خيوط التنفيذ الـ 10,000 بفحص الذاكرة المؤقتة، ولن يجدوا شيئاً، ويتابعون نحو قاعدة البيانات. حتى إذا كانت لديك قاعدة بيانات يمكنها التعامل مع 1,000 استجابة في الثانية، فإن 10,000 استجابة متزامنة ستسبب تراكم الطلبات، مما قد يؤدي إلى تعطل التطبيق.

الهدف هو ضمان أن يكون طلب واحد فقط مسؤولاً عن تجديد البيانات، بينما تنتظر الطلبات الأخرى أو تتلقى نسخة قديمة من البيانات.

الاستراتيجية 1: القفل الموزع باستخدام Redlock

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

عند تنفيذ ذلك في Redis، من الضروري استخدام عمليات ذرية لتجنب ظروف السباق. يُعد أمر SET مع وسوم NX (تعيين إذا لم يكن موجوداً) و EX (الانتهاء) الطريقة القياسية لتنفيذ القفل بشكل آمن.

import redis

def get_data_with_lock(redis_client, key, data_fetcher):
    # محاولة الحصول على القفل باستخدام NX وانتهاء الصلاحية لمنع القفل الميت
    # EX يحدد وقت انتهاء الصلاحية بالثواني، مما يضمن تحرير القفل إذا تعطل العملية
    if redis_client.set(lock_key, "locked", nx=True, ex=5):
        try:
            # يدخل خيط واحد فقط هنا
            data = data_fetcher()
            redis_client.set(key, data, ex=60) # تعيين وقت انتهاء صلاحية الذاكرة المؤقتة
            return data
        finally:
            # تحرير القفل
            redis_client.delete(lock_key)
    else:
        # خيط آخر يقوم بتحديث الذاكرة المؤقتة
        # الخيار أ: الانتظار وإعادة المحاولة (محجوز)
        # الخيار ب: إرجاع ذاكرة تخزين مؤقت قديمة أو خطأ (غير محجوز)
        return redis_client.get(key)

على الرغم من فعاليتها، فإن هذا النهج يقدم تأخيراً لجميع الخيوط المنتظرة. في سيناريوهات التزامن العالي، يمكن للخيوط المحجوزة أن تستهلك موارد كبيرة. الحل الأكثر أناقة لأحمال العمل التي تعتمد على القراءة هو الانتقائية الاحتمالية.

الاستراتيجية 2: الانتقائية الاحتمالية المبكرة

بدلاً من السماح بانتهاء صلاحية الذاكرة المؤقتة فجأة عند t=60، يمكننا استخدام نهج احتمالي. نحدد وقت انتهاء الصلاحية (TTL) للذاكرة المؤقتة كمدى متغير، بحيث يكون من المرجح أن يحدث الانتهاء قبل الوقت الاسمي بقليل. هذا يوزع طلبات التجديد عبر نافذة زمنية، مما يسوي الحمل على قاعدة البيانات.

يمكننا أيضاً تنفيذ آلية "التحديث الكسول". عند الوصول إلى مفتاح، نتحقق مما إذا كان يقترب من انتهاء الصلاحية (على سبيل المثال، ضمن آخر 10٪ من وقت انتهاء الصلاحية الخاص به). إذا كان كذلك، نقوم بتشغيل خيط أو مهمة في الخلفية لتجديد البيانات بشكل غير متزامن، بينما يعيد الطلب الحالي البيانات الموجودة (القديمة قليلاً).

import time
import random

def get_data_probabilistic(redis_client, key, data_fetcher):
    data = redis_client.get(key)
    
    if not data:
        # فشل في الذاكرة المؤقتة: تجديد وتعيين
        new_data = data_fetcher()
        # إضافة اهتزاز عشوائي صغير لوقت انتهاء الصلاحية (على سبيل المثال، 60 ثانية إلى 70 ثانية)
        ttl = random.randint(60, 70)
        redis_client.set(key, new_data, ex=ttl)
        return new_data
    
    # نجاح في الذاكرة المؤقتة: التحقق مما إذا كان يجب التحديث مبكراً
    ttl_remaining = redis_client.ttl(key)
    
    # إذا كان وقت انتهاء الصلاحية المتبقي أقل من 10٪ من أقصى وقت انتهاء صلاحية أصلي، قم بالتحديث غير المتزامن
    if ttl_remaining < 7: 
        # في بيئة الإنتاج، استخدم طابور رسائل أو مشغل مهام غير متزامن هنا
        schedule_async_refresh(key, data_fetcher)
        
    return data

def schedule_async_refresh(key, data_fetcher):
    # يجب أن تعمل هذه الوظيفة في خيط/عملية منفصلة
    # لتجنب حجب خيط الطلب الرئيسي
    new_data = data_fetcher()
    redis_client.set(key, new_data, ex=60)

الخاتمة

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

بالنسبة للعديد من أنظمة الإنتاج، يكون النهج الهجين هو الأفضل: استخدام أوقات انتهاء صلاحية قصيرة مع اهتزاز لتوزيع الانتهاء، مقترناً بتحديثات خلفية غير متزامنة. من خلال تنفيذ هذه الاستراتيجيات، تضمن بقاء بنيتك التحتية مرنة تحت أحمال العمل الثقيلة، والحفاظ على تجربة مستخدم سلسة حتى أثناء ذروات حركة المرور.

Share: