كنا نحرق الذاكرة لحساب المستخدمين الفريدين: كيف أنقذتنا خوارزمية 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 هي مثال رائع على التفكير “خارج الصندوق” وكيف يمكن لمفهوم إحصائي بسيط أن يحل مشاكل معقدة في عالم التكنولوجيا، ويوفر علينا الكثير من المال، والوقت، وليالي السهر الطويلة أمام شاشات مراقبة السيرفرات.

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

أبو عمر

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

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

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

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

آخر المدونات

ذكاء اصطناعي

كنا نبحث عن الإبرة في كومة قش: كيف أنقذتنا ‘قواعد بيانات المتجهات’ من جحيم البحث الدلالي البدائي؟

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

27 أبريل، 2026 قراءة المزيد
تسويق رقمي

كنا نلاحق الكلمات الطويلة يدوياً: كيف أنقذنا التحسين البرمجي لمحركات البحث (Programmatic SEO) من جحيم الفرص الضائعة؟

أتذكر جيداً أيام الملاحقة اليدوية للكلمات المفتاحية الطويلة، جهدٌ ضائع ووقتٌ مهدر. في هذه المقالة، أشارككم قصة كيف غيّر "التحسين البرمجي لمحركات البحث" (Programmatic SEO)...

26 أبريل، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

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

أشارككم قصة حقيقية من قلب مشاريع البرمجة، كيف كان تطبيقنا يعمل بكفاءة لكنه "ميت سريرياً". اكتشفوا معنا عالم "التفاعلات الدقيقة" (Microinteractions)، تلك اللمسات السحرية التي...

26 أبريل، 2026 قراءة المزيد
برمجة وقواعد بيانات

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

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

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

كانت إدارة واجهاتنا البرمجية فوضى: كيف أنقذتنا ‘بوابة الواجهات البرمجية’ (API Gateway) من جحيم التكرار وانعدام الأمان؟

أروي لكم قصة من قلب المعركة البرمجية، كيف انتقلنا من فوضى الخدمات المصغرة (Microservices) المتناثرة إلى نظام متكامل وآمن. هذه ليست مجرد مقالة تقنية، بل...

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

كنا ندفع ثمن الخوادم حتى وهي نائمة: كيف أنقذتنا ‘الحوسبة اللاخادمية’ (Serverless) من جحيم إدارة البنية التحتية؟

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

26 أبريل، 2026 قراءة المزيد
التوسع والأداء العالي والأحمال

طلبات الذروة كانت تكسر نظامنا: كيف أنقذتنا ‘طوابير الرسائل’ (Message Queues) من جحيم الانهيارات المفاجئة؟

أشارككم قصة حقيقية من قلب المعركة التقنية، عندما كادت طلبات العملاء في موسم الذروة أن تدمر نظامنا بالكامل. اكتشفوا كيف كانت "طوابير الرسائل" (Message Queues)...

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