كنا نحرق الذاكرة لحساب المستخدمين الفريدين: كيف أنقذتنا خوارزمية HyperLogLog من جحيم استهلاك الموارد؟

يا جماعة الخير، السلام عليكم ورحمة الله.

خليني أحكيلكم قصة صارت معي ومع فريقي قبل كم سنة، قصة فيها شوية دراما تقنية وسهر بالليل. كنا وقتها بنشتغل على نظام تحليلات لتطبيق جديد، وكان التطبيق -ما شاء الله- بينمو بسرعة الصاروخ. واحد من أهم المقاييس اللي كان لازم نعرضها في لوحة التحكم هو “عدد المستخدمين النشطين الفريدين” يومياً وشهرياً (DAU/MAU).

في البداية، الحل كان بسيط ومباشر. كل ما مستخدم يعمل أي إشي بالتطبيق، بنرمي الـ ID تبعه في قائمة، وبالآخر بنعملها `Set` عشان نشيل التكرار ونعد اللي ضلوا. أول شهر، شهرين، الأمور ماشية زي الحلاوة. لكن لما وصلنا أول مليون مستخدم، بلشت المصايب. السيرفر اللي عليه هاي العملية صار يستهلك ذاكرة (RAM) بشكل مجنون. كنا كل آخر يوم نشوف الذاكرة بتوصل 90% و 95%، وصارت تجينا تنبيهات نص الليل إنه “السيرفر رح يموت!”. صرنا زي اللي بيلعب لعبة “مين رح يوقع الأول”، إحنا ولا السيرفر. قعدت مع حالي وأنا بشرب فنجان قهوة الصبح بعد ليلة ما نمتها، وبحكي لحالي: “يا أبو عمر، معقول ما في حل أذكى من هيك؟ معقول عشان نعد شوية مستخدمين بدنا نحرق كل هاي الموارد؟”.

من هداك اليوم، بلشت رحلة البحث عن حل ينقذنا من هذا الجحيم. رحلة انتهت عند بطل قصتنا اليوم: خوارزمية HyperLogLog.

المشكلة الكلاسيكية: عد العناصر الفريدة (Cardinality Estimation)

قبل ما نغوص في الحل، خلينا نفهم أصل المشكلة. المشكلة تُعرف في علم الحاسوب بـ “تقدير الكردلة” أو Cardinality Estimation، والمقصود فيها ببساطة هو عد عدد العناصر الفريدة في مجموعة كبيرة جداً من البيانات قد تحتوي على تكرارات كثيرة.

الطريقة الساذجة: استخدام المجموعات (Sets)

الحل الأول اللي بيخطر على بال أي مبرمج هو استخدام بنية بيانات من نوع “مجموعة” أو `Set`. فكرتها بسيطة: هي بنية بيانات لا تسمح بالتكرار. كل عنصر جديد بدك تضيفه، الـ `Set` بتتأكد إذا هو موجود أو لأ. إذا مش موجود بتضيفه، وإذا موجود بتطنشه. وفي النهاية، حجم الـ `Set` هو عدد العناصر الفريدة.

لو بدنا نكتبها بالكود (بايثون كمثال)، بتكون بسيطة جداً:

# قائمة طويلة من معرفات المستخدمين مع تكرار
user_visits = ["user1", "user2", "user1", "user3", "user2", "user4", ...]

# استخدام Set لمعرفة العدد الفريد
unique_users = set(user_visits)
unique_count = len(unique_users)

print(f"العدد الدقيق للمستخدمين الفريدين: {unique_count}")

وين المشكلة إذن؟ المشكلة في الذاكرة. الـ `Set` لازم تخزن كل عنصر فريد في الذاكرة عشان تقدر تقارن العناصر الجديدة معه. هذا يعني إنه لو عندك 50 مليون مستخدم فريد، وكل ID بياخد 36 بايت (مثلاً لو كان UUID)، فإنت بتحكي عن:

50,000,000 * 36 bytes ≈ 1.8 GB

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

عندما لا يكون الجواب الدقيق هو المطلوب

وهنا يأتي السؤال الجوهري اللي غير طريقة تفكيرنا: هل أنا فعلاً بحاجة أعرف إنه عندي 50,123,456 مستخدم فريد بالضبط؟ ولا بكفيني أعرف إنه عندي “حوالي 50.1 مليون” مستخدم؟

في معظم حالات أنظمة التحليلات ولوحات التحكم، الجواب التقريبي “كافٍ وزيادة”. الإدارة ما بيهمها آخر كم رقم، بيهمها الصورة الكبيرة: هل قاعدة المستخدمين بتنمو؟ بأي سرعة؟ هل وصلنا للهدف؟

هنا دخلنا عالم جديد اسمه “بنى البيانات الاحتمالية” (Probabilistic Data Structures). هاي بنى بيانات ذكية جداً بتضحي بجزء صغير جداً من الدقة (بتحكيلك الجواب التقريبي) مقابل توفير هائل جداً في الموارد (الذاكرة والمعالج).

بطل قصتنا: خوارزمية HyperLogLog (HLL)

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

كيف تعمل هذه الخوارزمية… بدون تعقيد رياضي؟

تخيل معي السيناريو التالي: طلبت منك ترمي قطعة نقدية وتسجل النتائج. لو حكيتلك إني رميتها وطلعلي “وجه” من أول مرة، ما بتقدر تخمن إشي. لكن لو حكيتلك إني ضليت أرمي لحد ما طلعلي “وجه”، وأطول سلسلة “كتابة” متتالية شفتها قبل ما يطلع “وجه” كانت 4 مرات (كتابة، كتابة، كتابة، كتابة، ثم وجه)، إنت رح تخمن إني غالباً رميت عدد معين من المرات. كل ما زادت سلسلة الـ “كتابة” المتتالية، كل ما كان الحدث نادر أكثر، وبالتالي إنت رح تستنتج إني جربت عدد مرات أكثر.

الـ HLL بتعمل إشي مشابه. هي بتعمل التالي لكل عنصر (مثلاً user ID):

  1. الهاش (Hashing): بتاخد الـ user ID وبتحوله لرقم ثنائي (مجموعة من الأصفار والواحدات) باستخدام دالة هاش (Hash Function). النتيجة بتكون عشوائية الشكل.
  2. البحث عن نمط نادر: بتبحث في الرقم الثنائي الناتج عن أطول سلسلة من الأصفار البادئة (leading zeros). مثلاً، الرقم 0001011... فيه صفرين في البداية. الرقم 0000110... فيه أربعة.
  3. التسجيل والتخمين: كل ما نشوف عدد أصفار بادئة أطول، هذا يعتبر حدث أندر. الخوارزمية بتسجل “أطول سلسلة أصفار بادئة” شافتها على الإطلاق عبر كل العناصر.

في أبسط صورها، إذا كانت أطول سلسلة أصفار بادئة شافتها الخوارزمية هي k، فإنها تخمن أن عدد العناصر الفريدة هو تقريباً 2k. فمثلاً، لو أقصى عدد أصفار كان 10، فالتخمين هو 210 ≈ 1024 عنصر فريد.

من التخمين إلى الدقة: التقسيم إلى سلال (Bucketing)

طبعاً، الاعتماد على قيمة واحدة عظمى (أطول سلسلة) ممكن يكون مضلل لو بالصدفة إجاك عنصر واحد أعطاك سلسلة طويلة جداً. هنا تأتي عبقرية HLL الحقيقية.

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

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

الميزة السحرية: الـ HLL بتستخدم مساحة ذاكرة ثابتة وصغيرة جداً. سواء عديت 1000 عنصر أو 10 مليار عنصر، هي رح تضل تستخدم نفس حجم الذاكرة (عادةً حوالي 12 كيلوبايت فقط!). كل اللي بتعمله هو تحديث الأرقام الصغيرة المسجلة في سلالها.

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

الحكي النظري جميل، بس “فرجيني الكود يا أبو عمر!”. خلينا نشوف مثال عملي بلغة بايثون، باستخدام مكتبة جاهزة عشان ما نعيد اختراع العجلة.

مثال باستخدام لغة بايثون ومكتبة pyhll

# pip install pyhll
import sys
from hll import HLL
import random

# لنقم بإنشاء بيانات وهمية: 1 مليون زيارة من 100 ألف مستخدم فريد
# هذا يعني أن كل مستخدم في المتوسط قام بـ 10 زيارات
TOTAL_VISITS = 1_000_000
UNIQUE_USERS = 100_000

# قائمة المستخدمين الفريدين
unique_user_ids = [f"user_{i}" for i in range(UNIQUE_USERS)]

# قائمة الزيارات الكاملة مع تكرار
all_visits = [random.choice(unique_user_ids) for _ in range(TOTAL_VISITS)]

# --- الطريقة الساذجة: استخدام Set ---
print("--- الطريقة الساذجة (Set) ---")
set_approach = set()
for user_id in all_visits:
    set_approach.add(user_id)

exact_count = len(set_approach)
set_memory = sys.getsizeof(set_approach)
print(f"العدد الدقيق: {exact_count}")
print(f"حجم الذاكرة المستخدمة للـ Set (تقريبي): {set_memory / 1024:.2f} KB")


# --- الطريقة الذكية: استخدام HyperLogLog ---
print("n--- طريقة HyperLogLog ---")
# p=14 يعني 2^14 = 16384 سلة، وهذا يعطي خطأ معياري حوالي 0.4%
hll_approach = HLL(14) 
for user_id in all_visits:
    hll_approach.add(user_id.encode('utf-8')) # HLL تعمل على بايتات

estimated_count = hll_approach.cardinality()
hll_memory = sys.getsizeof(hll_approach)
error_percentage = (abs(estimated_count - exact_count) / exact_count) * 100

print(f"العدد التقديري: {int(estimated_count)}")
print(f"حجم الذاكرة المستخدمة للـ HLL (تقريبي): {hll_memory / 1024:.2f} KB")
print(f"نسبة الخطأ: {error_percentage:.2f}%")

عند تشغيل هذا الكود، ستحصل على نتائج مشابهة للتالي:

--- الطريقة الساذجة (Set) ---
العدد الدقيق: 100000
حجم الذاكرة المستخدمة للـ Set (تقريبي): 4096.09 KB (حوالي 4 ميجابايت)

--- طريقة HyperLogLog ---
العدد التقديري: 99854
حجم الذاكرة المستخدمة للـ HLL (تقريبي): 16.05 KB (حوالي 16 كيلوبايت)
نسبة الخطأ: 0.15%

النتائج تتحدث عن نفسها. حصلنا على تقدير دقيق بنسبة 99.85% باستخدام أقل من 0.5% من الذاكرة التي استهلكتها طريقة الـ Set! تخيل هذا الفرق على نطاق مليارات الزيارات.

نصائح من “أبو عمر”: متى وكيف تستخدم HLL؟

بعد ما شفنا قوتها، هاي شوية نصائح من خبرتي العملية:

اعرف متطلباتك جيداً

HLL ليست حلاً لكل المشاكل. القاعدة بسيطة: إذا كان الجواب التقريبي مقبولاً، استخدم HLL. إذا كنت تحتاج إلى دقة 100% (مثلاً في أنظمة الفوترة المالية أو أي نظام يعتمد على العد الدقيق)، فابتعد عنها واستخدم الطرق التقليدية وتحمل تكلفتها.

  • مناسبة لـ: تحليلات الويب، عد الزوار الفريدين، عد مصطلحات البحث الفريدة، مراقبة أداء الأنظمة، أي شيء يبدأ بـ COUNT(DISTINCT ...) في عالم البيانات الضخمة.
  • غير مناسبة لـ: أنظمة البنوك، أنظمة الفوترة، حسابات المستخدمين النهائية.

الذاكرة مقابل الدقة

دقة HLL قابلة للتعديل. في المثال السابق، استخدمنا HLL(14). هذا الرقم (p) يحدد عدد السلال (2p). كلما زاد الرقم p، زادت الدقة، وزادت الذاكرة المستخدمة (لكنها تبقى صغيرة جداً). القاعدة العامة هي أن الخطأ المعياري (standard error) يكون تقريباً 1.04 / sqrt(2p). لا تختر رقماً كبيراً جداً بدون داعٍ.

دمج البيانات أصبح سهلاً

واحدة من أجمل ميزات HLL هي قابلية الدمج (Mergeable). تخيل أن لديك 10 سيرفرات وكل سيرفر يحسب المستخدمين الفريدين الذين مروا من خلاله باستخدام HLL. في نهاية اليوم، يمكنك جمع هياكل HLL العشرة من كل السيرفرات ودمجها في هيكل واحد. هذا الهيكل المدموج سيعطيك تقديراً لعدد المستخدمين الفريدين على مستوى كل السيرفرات معاً! هذا مستحيل عملياً باستخدام `Sets`.

الخلاصة: لا تحرق الذاكرة إذا كان التقدير كافياً 😉

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

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

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

أبو عمر

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

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

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

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

آخر المدونات

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

تحديثات قاعدة البيانات بدون توقف: كيف أنقذنا نمط التوسيع والتعاقد (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 قراءة المزيد
البودكاست