كانت قاعدة بياناتنا تستنزفها الأشباح: كيف أنقذنا ‘مرشح بلوم’ (Bloom Filter) من جحيم الاستعلامات؟

بتذكرها زي كأنه امبارح. كنا في عز إطلاق ميزة جديدة في نظامنا، وفجأة، بدأت التنبيهات تصرخ زي المجنونة. فتحت لوحة المراقبة (Dashboard) وشفت مؤشر استخدام المعالج لقاعدة البيانات في السقف، أحمر قاني، قريب يوصل 100%. أول إشي خطر ببالي: “يا ساتر، شو المصيبة اللي عملناها؟”.

اجتمع الفريق كله، كل واحد فينا صار يدور في الكود ويحلل السجلات (logs). السيرفرات بتعاني، والمستخدمون بدؤوا يشتكون من بطء شديد في النظام. بعد ساعات من التوتر والبحث المكثف، واحد من زملائي صرخ: “يا جماعة الخير، لقيتها!”. المشكلة ما كانت في كود معقد أو استعلام ثقيل على بيانات موجودة، المصيبة كانت أكبر وأغبى من هيك بكثير.

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

ما هي الأشباح التي كانت تطارد قاعدة بياناتنا؟

لفهم المشكلة بعمق، دعنا نبتعد عن المصطلحات المعقدة. تخيل أن قاعدة البيانات الخاصة بك هي مكتبة ضخمة جداً تحتوي على ملايين الكتب. الآن، تخيل أن هناك طابوراً طويلاً من الناس، 90% منهم يسألون أمين المكتبة عن كتب لم تُطبع ولم تُكتب أصلاً! سيقضي أمين المكتبة المسكين كل وقته وهو يركض بين الرفوف ويبحث، ليعود في كل مرة ويقول: “آسف، الكتاب غير موجود”.

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

السيناريو القاتل: البحث عن “اللا شيء”

هذه المشكلة، التي أسميها “استعلامات الأشباح”، شائعة جداً في العديد من التطبيقات:

  • التحقق من توفر اسم المستخدم: عند تسجيل مستخدم جديد، يجب عليك التحقق من أن اسم المستخدم الذي اختاره غير مستخدم بالفعل.
  • قوائم الحظر (Blocklists): التحقق مما إذا كان عنوان IP أو بريد إلكتروني أو رابط URL موجوداً في قائمة ضخمة من العناصر المحظورة.
  • أنظمة التخزين المؤقت (Caching): قبل جلب بيانات من مصدر بطيء (مثل قاعدة بيانات أو API خارجية)، تتحقق أولاً مما إذا كانت موجودة في الكاش. إذا لم تكن موجودة، فهذا “Cache Miss” هو استعلام شبح بالنسبة للكاش.

في كل هذه الحالات، البحث عن عنصر غير موجود هو عملية مكلفة جداً بلا طائل.

المنقذ الذي لم نتوقعه: مرشح بلوم (Bloom Filter)

مرشح بلوم ليس قاعدة بيانات، وليس بنية بيانات تخزن العناصر نفسها. الأفضل أن تفكر فيه كـ “حارس أمن” ذكي جداً ولكنه “احتمالي” يقف على باب قاعدة البيانات. وظيفته بسيطة: الإجابة على سؤال واحد فقط: “هل *من المحتمل* أن يكون هذا العنصر موجوداً بالداخل؟”.

الجميل في هذا الحارس أنه سريع جداً ويستخدم ذاكرة قليلة جداً. لكن لديه “عقد عمل” غريب بعض الشيء:

  1. إذا قال الحارس: “مستحيل، هذا العنصر غير موجود بالتأكيد“، فهو صادق 100%. لا يمكن أن يخطئ في هذه الحالة (No False Negatives).
  2. إذا قال الحارس: “نعم، من المحتمل أنه موجود“، فهناك احتمال صغير أنه مخطئ وأن العنصر غير موجود بالفعل (Possibility of False Positives).

وهذه هي المقايضة العبقرية! نحن نسمح بنسبة ضئيلة من “الإيجابيات الكاذبة” (يعني السماح لعدد قليل جداً من استعلامات الأشباح بالمرور إلى قاعدة البيانات)، مقابل منع الأغلبية الساحقة منها (99% أو أكثر) بكفاءة مذهلة.

كيف يعمل هذا “الحارس” الذكي؟

يعتمد مرشح بلوم على مكونين رئيسيين:

  1. مصفوفة بتات (Bit Array): تخيل شريطاً طويلاً من خانات الـ 0، مثل `[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]`.
  2. عدة دوال هاش (Hash Functions): وهي دوال رياضية تأخذ أي مُدخل (مثل كلمة “omar”) وتحوله إلى رقم فريد يمثل موقعاً على مصفوفة البتات.

عند إضافة عنصر (مثلاً، “user1”):

  • نمرر العنصر على كل دالة هاش (لنقل 3 دوال).
  • الدالة الأولى تعطينا الموقع 2، والثانية الموقع 5، والثالثة الموقع 8.
  • نذهب إلى مصفوفة البتات ونغير قيمة الخانات في هذه المواقع إلى 1. فتصبح المصفوفة: `[0, 0, 1, 0, 0, 1, 0, 0, 1, 0]`.

عند التحقق من وجود عنصر (مثلاً، “ghost_user”):

  • نمرر العنصر على نفس دوال الهاش الثلاث.
  • لنفترض أنها أعطتنا المواقع 1، و5، و9.
  • نذهب لفحص الخانات في هذه المواقع:
    • الخانة 1 قيمتها 0.
    • الخانة 5 قيمتها 1.
    • الخانة 9 قيمتها 0.
  • بما أننا وجدنا خانة واحدة على الأقل قيمتها 0، فإننا نجزم بشكل قاطع 100% أن “ghost_user” لم تتم إضافته من قبل. وبهذا، نرفض الاستعلام ونمنعه من الوصول لقاعدة البيانات. وهذا هو سحر مرشح بلوم!

شرح بسيط للإيجابية الكاذبة (False Positive): قد يأتي عنصر جديد، وبالصدفة البحتة، تكون مواقع الهاش الخاصة به كلها قد تم تحويلها إلى 1 بواسطة عناصر أخرى. في هذه الحالة، سيقول المرشح “ربما يكون موجوداً” بالخطأ. لكن باختيار حجم مصفوفة مناسب وعدد دوال هاش جيد، يمكننا جعل هذا الاحتمال ضئيلاً جداً (مثلاً 0.1%).

تطبيق الحل عمليًا: من النظرية إلى الكود

بعد أن شرحت الفكرة للفريق، تحمسنا جميعاً. قررنا وضع مرشح بلوم في الذاكرة (In-memory) أمام مسار الكود الذي يتحقق من وجود العناصر. قبل أن يرسل الكود استعلام `SELECT` إلى قاعدة البيانات، يسأل مرشح بلوم أولاً.

مثال بسيط باستخدام بايثون

لست بحاجة لإعادة اختراع العجلة. هناك مكتبات جاهزة ومُحسَّنة يمكنك استخدامها. هنا مثال باستخدام مكتبة `pybloom_live` في بايثون لتوضيح الفكرة:


# First, install the library: pip install pybloom_live
from pybloom_live import BloomFilter

# Create a Bloom Filter
# capacity: a rough estimate of how many items you'll add
# error_rate: the acceptable false positive rate (e.g., 1%)
# Let's say we expect 1 million usernames and want an error rate of 0.01
bloom = BloomFilter(capacity=1000000, error_rate=0.01)

# --- Phase 1: Populate the filter with existing usernames from DB ---
# This is typically done once at startup or periodically
existing_users = ["ahmad", "fatima", "omar", "sara"]
for user in existing_users:
    bloom.add(user)

print(f"Filter populated. Size in bytes: {bloom.num_bytes}")

# --- Phase 2: Use the filter in our application logic ---

def is_username_available(username):
    # Step 1: Check the Bloom Filter first (very fast, in-memory)
    if username in bloom:
        # The filter says the username *might* exist.
        # This could be a true positive or a rare false positive.
        # So, we MUST confirm with the database (the expensive check).
        print(f"Bloom filter says '{username}' MIGHT exist. Checking DB...")
        # In a real app, you would query the database here.
        # return db.users.exists(username)
        # For our example, we'll just check our list.
        if username in existing_users:
            return False # It really exists
        else:
            # This was a false positive!
            print(f"  -> It was a false positive! '{username}' is actually available.")
            return True
    else:
        # The filter says the username DEFINITELY does not exist.
        # We can be 100% sure and avoid a DB query. This is the big win!
        print(f"Bloom filter says '{username}' DEFINITELY does not exist. No DB check needed!")
        return True

# --- Let's test it ---
print("nTesting known users:")
print(f"Is 'omar' available? {is_username_available('omar')}") # Should check DB
print(f"Is 'sara' available? {is_username_available('sara')}") # Should check DB

print("nTesting new (non-existent) users:")
print(f"Is 'ghost_user1' available? {is_username_available('ghost_user1')}") # Should be blocked by filter
print(f"Is 'another_ghost' available? {is_username_available('another_ghost')}") # Should be blocked by filter

نصيحة أبو عمر: كيف تختار حجم المرشح وعدد دوال الهاش؟

هذا هو السؤال الأهم. الأمر كله عبارة عن موازنة. لا تختر الأرقام بشكل عشوائي. المعادلة بسيطة:

  • حجم المرشح (m): كلما كان أكبر، قلّت فرصة “تصادم” الهاشات، وبالتالي قلّت نسبة الإيجابيات الكاذبة، ولكنه يستهلك ذاكرة أكبر.
  • عدد دوال الهاش (k): عدد أكبر يعطي “بصمة” أكثر تميزاً للعنصر، ولكنه يجعل عمليات الإضافة والتحقق أبطأ قليلاً.

الخبر السار هو أنك لست بحاجة لحساب هذا بنفسك. معظم المكتبات الجيدة، مثل التي في المثال أعلاه، تطلب منك شيئين فقط:

  1. السعة المتوقعة (n): كم عدد العناصر التي تتوقع إضافتها للمرشح؟ (مثلاً، مليون اسم مستخدم).
  2. معدل الخطأ المقبول (p): ما هي نسبة الإيجابيات الكاذبة التي يمكنك تحملها؟ (مثلاً، 1% أو 0.1%).

وبناءً على هذين الرقمين، تقوم المكتبة بحساب الحجم الأمثل للمصفوفة (m) وعدد دوال الهاش (k) تلقائياً. هذه هي الطريقة العملية والصحيحة.

النتائج كانت “إشي بيرفع الراس”!

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

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

الخلاصة: متى تستخدم مرشح بلوم؟ 🤔

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

  • عندما تحتاج إلى التحقق من وجود عنصر بشكل متكرر ضمن مجموعة كبيرة جداً من البيانات.
  • عندما تكون تكلفة التحقق “الحقيقي” (مثل استعلام قاعدة بيانات أو استدعاء API) باهظة جداً.
  • عندما يمكنك تحمل نسبة ضئيلة جداً من الإيجابيات الكاذبة (False Positives). أي عندما لا يسبب خطأ “ربما موجود” كارثة، بل فقط استعلاماً إضافياً غير ضروري في بعض الحالات النادرة.
  • عندما تكون متأكداً أنك لن تحتاج إلى حذف عناصر من المجموعة (مرشح بلوم القياسي لا يدعم الحذف، على الرغم من وجود أنواع أخرى مثل Counting Bloom Filter تدعم ذلك بتكلفة ذاكرة أعلى).

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

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

برمجة وقواعد بيانات

تحديثات قاعدة البيانات بدون توقف: كيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من جحيم التوقفات المجدولة؟

هل سئمت من إيقاف الخدمة مع كل تحديث لهيكلة قاعدة البيانات؟ أشارككم قصة حقيقية وكيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من ليالي النشر الطويلة والمُجهدة،...

4 يونيو، 2026 قراءة المزيد
الشبكات والـ APIs

كانت إعادة المحاولة كارثة: كيف أنقذتنا مفاتيح عدم تكرار العمليات (Idempotency Keys) من جحيم الفواتير المزدوجة؟

أشارككم قصة حقيقية من الخنادق البرمجية، يوم كاد خطأ بسيط في إعادة محاولة طلبات الدفع أن يكلفنا سمعتنا وأموال عملائنا. اكتشفوا معنا كيف كانت مفاتيح...

4 يونيو، 2026 قراءة المزيد
الحوسبة السحابية

من التوقف التام إلى النجاة: كيف أنقذتنا استراتيجية “الضوء المرشد” (Pilot Light) يوم انقطعت السحابة؟

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

4 يونيو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

كانت مهمتي البرمجية للاختبار مجرد كود: كيف أنقذني توثيق القرارات من جحيم الصمت بعد المقابلة؟

أشارككم قصة حقيقية من بداياتي، وكيف تعلمت بالطريقة الصعبة أن المهمة البرمجية ليست مجرد كتابة كود، بل هي فرصة لإظهار طريقة تفكيرك. اكتشف كيف يمكن...

4 يونيو، 2026 قراءة المزيد
التكنلوجيا المالية Fintech

من الانتظار لأيام إلى الدفع في ثوانٍ: كيف أنقذتنا شبكات الدفع الفوري من جحيم التحويلات البنكية؟

أسرد لكم من واقع تجربتي كـ "أبو عمر"، كيف عانينا من بطء وتكلفة التحويلات البنكية الدولية، وكيف جاءت شبكات الدفع الفوري ومعيار ISO 20022 لتكون...

4 يونيو، 2026 قراءة المزيد
البنية التحتية وإدارة السيرفرات

كان كل خادم لدينا ‘ندفة ثلج’ فريدة: كيف أنقذنا ‘الكود كبنية تحتية’ (IaC) من جحيم الانجراف اليدوي؟

في هذه المقالة، أشارككم قصة حقيقية من قلب المعركة التقنية مع "خوادم ندفات الثلج" الفوضوية. سنغوص في مفهوم "الكود كبنية تحتية" (IaC) وكيف أن أدوات...

4 يونيو، 2026 قراءة المزيد
اختبارات الاداء والجودة

كانت تغطية الاختبارات 100% لكن الأخطاء تتسرب: كيف أنقذنا “الاختبار الطفري” من جحيم الثقة الزائفة؟

كنا نظن أن تغطية الاختبار بنسبة 100% هي درعنا الواقي، لكن الأخطاء كانت تتسلل إلى الإنتاج كاللصوص في ليل بهيم. اكتشف كيف أنقذنا "الاختبار الطفري"...

4 يونيو، 2026 قراءة المزيد
البودكاست