جدول المستخدمين وصل إلى مليار صف… وقاعدة بياناتي استسلمت: كيف أنقذني تقسيم البيانات (Sharding) من انهيار كامل؟

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

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

لكن الأمور ما مشيت. وصلت لمرحلة كان فيها سيرفر قاعدة البيانات بيصرخ وبيطلب الرحمة. المعالج (CPU) دائماً 100%، والذاكرة (RAM) على آخرها. وفي ليلة خميس، وقت الذروة، حصل ما كنت أخشاه: انهار كل شيء. التطبيق توقف تماماً، ورسائل الخطأ “Cannot connect to the database” صارت تملأ شاشات المراقبة. حسيت الدنيا لفت فيي، وشعرت بذاك العرق البارد اللي بينزل على ظهرك لما تعرف إنك في ورطة حقيقية. جدول users وحده كان قد وصل لمئات الملايين من السجلات، وكل عملية تسجيل دخول أو بحث بسيطة كانت بمثابة طرقة مسمار في نعش قاعدة البيانات. هنا أدركت أن الحلول التقليدية لن تجدي نفعاً، وكان لا بد من التفكير خارج الصندوق… أو بالأحرى، خارج السيرفر الواحد.

التشخيص: لماذا استسلمت قاعدة بياناتي؟

قبل أن نتحدث عن الحل، يجب أن نفهم أصل المشكلة. عندما تنمو قاعدة بياناتك بشكل هائل، تواجه عدة تحديات قاتلة، وهو ما يُعرف بـ “مشاكل التوسع” أو Scalability. في البداية، أول حل يخطر ببال أي مطور هو التوسع الرأسي (Vertical Scaling).

التوسع الرأسي: الحل المؤقت والمكلف

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

  • المرحلة الأولى: زدنا الـ RAM من 32GB إلى 64GB. ارتاحت الأمور لأسبوعين.
  • المرحلة الثانية: انتقلنا لمعالج بـ 32 نواة بدلاً من 16. تحسن الأداء لشهر آخر.
  • المرحلة الثالثة: اشترينا أغلى وأسرع أقراص NVMe SSD في السوق.

هذا الحل نجح مؤقتاً، لكنه كان مثل المسكنات. له مشكلتان أساسيتان: أولاً، هو مكلف جداً، فأسعار العتاد القوي ترتفع بشكل أُسّي. ثانياً، وهو الأهم، له سقف. سيأتي يوم لن تجد فيه سيرفر أقوى في السوق لتشتريه، مهما كان معك من مال. لقد وصلنا إلى هذا السقف.

عنق الزجاجة الحقيقي: جدول واحد عملاق

المشكلة لم تكن فقط في حجم البيانات الإجمالي، بل في تمركزها في جدول واحد ضخم (users). هذا يخلق عدة اختناقات:

  • ضغط الإدخال/الإخراج (I/O Contention): قرص التخزين له سرعة قراءة وكتابة محدودة. عندما تتنافس آلاف العمليات على نفس الجدول في نفس الوقت، يصطفون في طابور طويل.
  • الفهارس (Indexes) الضخمة: الفهرس الذي كان يُسرّع البحث عندما كان الجدول يحتوي على مليون صف، يصبح هو نفسه بطيئاً جداً عند مليار صف. تحديثه مع كل عملية INSERT جديدة يصبح عبئاً، والبحث فيه لم يعد فورياً.
  • قفل الجداول والصفوف (Locking): عمليات الكتابة والتحديث الكثيرة كانت تؤدي إلى قفل أجزاء من الجدول، مما يمنع عمليات القراءة من الوصول إليها، والعكس صحيح.

هنا كان لا بد من الانتقال للفلسفة الأخرى: التوسع الأفقي (Horizontal Scaling). بدل ما نكبّر الموتور، قررنا نجيب كمان سيارة. وهذا هو جوهر تقسيم البيانات أو الـ Sharding.

المنقذ: ما هو تقسيم البيانات (Sharding) باللهجة العامية؟

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

الحل هو أن تقسم هذا القاموس إلى 28 مجلداً صغيراً، مجلد لحرف الألف، ومجلد لحرف الباء، وهكذا. الآن، إذا أردت البحث عن كلمة “برمجة”، ستذهب مباشرة إلى مجلد حرف الباء. لقد قمت للتو بعملية “تقسيم” أو Sharding.

في عالم قواعد البيانات، الـ Sharding هو نفس المبدأ: بدلاً من تخزين كل بياناتك في قاعدة بيانات واحدة ضخمة، نقوم بتوزيعها عبر عدة قواعد بيانات أصغر وأسرع ومستقلة. كل قاعدة بيانات من هذه القواعد تسمى Shard (قطعة أو شظية).

المكونات الأساسية لعملية الـ Sharding

  1. الـ Shards: هي قواعد بيانات منفصلة ومستقلة، كل واحدة تحتوي على جزء من البيانات. يمكن أن تكون كل Shard على سيرفر خاص بها.
  2. مفتاح التقسيم (Shard Key): هذا هو “المفتاح السري” أو القاعدة التي نستخدمها لنقرر أي قطعة من البيانات تذهب إلى أي Shard. في مثال القاموس، كان مفتاح التقسيم هو “الحرف الأول من الكلمة”. في تطبيقنا، اخترنا أن يكون user_id.
  3. موجه الاستعلامات (Query Router): هذا هو “شرطي المرور”. هو عبارة عن طبقة منطقية (عادة تكون جزءاً من الكود في تطبيقك) تعرف كيف توجه كل استعلام إلى الـ Shard الصحيح. عندما تريد البحث عن المستخدم رقم 12345، الموجه يعرف أن هذا المستخدم موجود في Shard رقم 2 مثلاً، فيرسل الاستعلام إلى هناك مباشرة.

المخطط: كيف طبقنا الـ Sharding خطوة بخطوة

القرار الأصعب في عملية الـ Sharding هو اختيار مفتاح التقسيم (Shard Key) واستراتيجية التقسيم. هذا القرار سيرافقك لسنوات، وتغييره لاحقاً مؤلم جداً.

1. اختيار مفتاح التقسيم (Shard Key)

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

  • مثال على مفتاح سيء: تقسيم المستخدمين حسب البلد. لو كان 80% من مستخدميك من بلد واحد، فسينتهي بهم المطاف في Shard واحد، بينما تبقى بقية الـ Shards شبه فارغة.
  • مثال على مفتاح جيد: user_id أو customer_id. هذه المعرفات فريدة وعادة ما تكون موزعة بشكل عشوائي، مما يضمن توزيعاً متساوياً للبيانات.

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

2. اختيار استراتيجية التقسيم

هناك عدة طرق لتوزيع البيانات، أشهرها اثنتان:

التقسيم القائم على النطاق (Range-Based Sharding)

يتم تقسيم البيانات بناءً على نطاقات من مفتاح التقسيم. مثلاً:

  • المستخدمون من ID 1 إلى 1,000,000 يذهبون إلى Shard 1.
  • المستخدمون من ID 1,000,001 إلى 2,000,000 يذهبون إلى Shard 2.

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

التقسيم القائم على الهاش (Hash-Based Sharding)

هنا، نطبق دالة هاش (Hash Function) على مفتاح التقسيم، ونتيجة الدالة تحدد الـ Shard. المعادلة البسيطة هي: shard_id = hash(shard_key) % number_of_shards.

ميزته: يوزع البيانات بشكل عشوائي ومتساوٍ جداً عبر كل الـ Shards، مما يمنع ظهور النقاط الساخنة بشكل شبه كامل.
عيبه: يجعل استعلامات النطاق صعبة جداً، لأن المستخدمين ذوي الأرقام المتسلسلة (مثل 101 و 102) سينتهون على الأغلب في Shards مختلفة.

قرارنا: اخترنا استراتيجية الهاش (Hash-Based) باستخدام user_id كمفتاح تقسيم. هذا كان الخيار الأنسب لتطبيقنا الاجتماعي الذي يتمحور حول عمليات فردية لكل مستخدم.

3. مثال كود بسيط لطبقة التوجيه

لم نستخدم حلاً جاهزاً في البداية، بل بنينا طبقة توجيه بسيطة داخل الكود البرمجي للتطبيق (مكتوب بلغة Python). الفكرة كانت كالتالي:


# constants.py
NUM_SHARDS = 4 # بدأنا بـ 4 سيرفرات (Shards)

# db_manager.py
import hashlib

# قائمة الاتصالات بقواعد البيانات المختلفة
DB_CONNECTIONS = [
    create_connection_to_shard_0(),
    create_connection_to_shard_1(),
    create_connection_to_shard_2(),
    create_connection_to_shard_3(),
]

def get_shard_id_for_user(user_id):
    """
    يستخدم دالة هاش لتحديد رقم الـ Shard المناسب للمستخدم.
    نستخدم SHA1 لضمان توزيع جيد، ثم نأخذ باقي القسمة على عدد الـ Shards.
    """
    # نحول الـ user_id إلى bytes لاستخدامها في دالة الهاش
    user_id_bytes = str(user_id).encode('utf-8')
    
    # نستخدم هاش SHA1 ونحوله إلى رقم صحيح
    hashed_value = int(hashlib.sha1(user_id_bytes).hexdigest(), 16)
    
    # نحسب رقم الـ Shard
    shard_id = hashed_value % NUM_SHARDS
    return shard_id

def get_db_connection_for_user(user_id):
    """
    ترجع كائن الاتصال بقاعدة البيانات الصحيحة بناءً على user_id.
    """
    shard_id = get_shard_id_for_user(user_id)
    return DB_CONNECTIONS[shard_id]

# user_repository.py
def get_user_by_id(user_id):
    # احصل على الاتصال الصحيح
    db_conn = get_db_connection_for_user(user_id)
    
    # نفذ الاستعلام على الـ Shard الصحيح فقط
    cursor = db_conn.cursor()
    cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
    return cursor.fetchone()

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

الندوب والحكمة: تحديات ودروس مستفادة

الرحلة لم تكن سهلة، والـ Sharding ليس حلاً سحرياً بدون مشاكل. “من الآخر، هو سلاح ذو حدين”. واجهتنا تحديات كبيرة تعلمنا منها الكثير:

1. الاستعلامات التي تربط جداول عبر Shards مختلفة (Cross-Shard Joins)

هذه كانت المصيبة الكبرى. ماذا لو أردت أن تجلب كل الطلبات (من جدول orders) لمستخدم معين (من جدول users)، ولكن جدول الطلبات مقسم حسب order_id وجدول المستخدمين مقسم حسب user_id؟ لا يمكنك عمل JOIN بسيط بين سيرفرين مختلفين.

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

2. إعادة الموازنة (Rebalancing)

ماذا يحدث عندما تمتلئ الـ 4 Shards وتحتاج لإضافة Shard خامس؟ إذا كنت تستخدم معادلة % 4 البسيطة، فإن تغييرها إلى % 5 يعني أن كل مفاتيحك تقريباً ستغير مكانها! ستحتاج إلى نقل كل البيانات في قاعدة بياناتك، وهي عملية معقدة وخطيرة.

الدرس المستفاد: كان يجب أن نخطط لهذا من البداية. التقنيات المتقدمة مثل الهاش المتسق (Consistent Hashing) تحل هذه المشكلة، حيث أن إضافة سيرفر جديد يتطلب نقل جزء صغير فقط من البيانات.

3. المعاملات (Transactions)

ضمان تنفيذ معاملة (Transaction) بنجاح عبر عدة Shards هو أمر معقد جداً ويتطلب بروتوكولات مثل (Two-Phase Commit). نصيحتنا كانت: صمم نموذج بياناتك بحيث أن 99% من معاملاتك تحدث داخل Shard واحد. هذا ممكن إذا اخترت مفتاح التقسيم (Shard Key) بعناية ليكون محور بياناتك.

هل الـ Sharding هو الحل دائماً؟

بالتأكيد لا. “مش كل وجع راس دواهُ بنادول”. الـ Sharding يضيف طبقة هائلة من التعقيد على نظامك. لا تفكر فيه إلا بعد أن تستنفد كل الحلول الأخرى.

قبل أن تقرر عمل Sharding، اسأل نفسك:

  1. هل الفهارس (Indexes) في قاعدة بياناتي مثالية؟ ربما فهرس خاطئ هو سبب البطء.
  2. هل استعلاماتي مكتوبة بأفضل طريقة؟ استخدم EXPLAIN ANALYZE لفهم أين يكمن البطء.
  3. هل أستخدم التخزين المؤقت (Caching) بشكل كافٍ؟ استخدام Redis أو Memcached يمكن أن يقلل 90% من ضغط القراءة على قاعدة بياناتك.
  4. هل أحتاج حقاً لكل هذه البيانات فورياً؟ ربما يمكن نقل البيانات القديمة إلى مخزن بيانات أرخص (Data Warehouse).
  5. هل جربت استخدام نسخ القراءة (Read Replicas)؟ إذا كان الضغط الأكبر هو من عمليات القراءة، يمكنك إنشاء نسخ من قاعدة بياناتك للقراءة فقط لتوزيع الحمل.

إذا كانت إجابتك “نعم” على كل هذه الأسئلة وما زلت تعاني، عندها فقط، ابدأ بالتفكير جدياً في الـ Sharding.

خلاصة الحكي ونصيحة من أخوك أبو عمر ☕

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

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

فكر في مفتاح التقسيم (Shard Key) المحتمل لتطبيقك من اليوم الأول!

حتى لو لم تكن تخطط لتطبيق الـ Sharding لسنوات، فإن تصميم تطبيقك ونموذج بياناتك مع الأخذ في الاعتبار وجود معرف فريد ومحوري (مثل tenant_id أو organization_id أو user_id) في كل مكان، سيجعل حياتك أسهل بألف مرة عندما يحين وقت التوسع الحقيقي. هذا التفكير المسبق هو الفارق بين عملية انتقال سلسة وكابوس تقني يستمر لشهور.

تذكر دائماً، بناء الأنظمة القابلة للتوسع لا يتعلق فقط بكتابة الكود، بل بالتفكير الاستراتيجي للمستقبل. 😉

أبو عمر

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

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

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

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

آخر المدونات

التكنلوجيا المالية Fintech

نقرة واحدة، خصم مزدوج: كيف أنقذني مفتاح ‘عدم التكرار’ (Idempotency Key) من غضب العملاء وكوابيس التسويات المالية؟

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

7 مارس، 2026 قراءة المزيد
أتمتة العمليات

من كابوس يوم الخميس إلى الإبداع المستمر: كيف حررتني أنابيب CI/CD من جحيم “يوم النشر”؟

أشارككم قصتي مع "يوم الخميس المشؤوم"، اليوم الذي كان مخصصًا لنشر التحديثات يدويًا، وكيف أنقذتني أتمتة العمليات باستخدام أنابيب CI/CD. اكتشفوا معي كيف تحولت من...

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

طلبتُ حقلًا واحدًا، فأرسل لي الـ API قاعدة البيانات بأكملها: كيف أنقذني GraphQL من إهدار الباندويث والبيانات غير اللازمة؟

أشارككم قصة حقيقية من مسيرتي كمطور، حين كاد تطبيق جوال أن يفشل بسبب بطء استجابة الـ API. أستعرض كيف أنقذتني تقنية GraphQL من مشاكل إحضار...

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

اختبارات التكامل قتلت إنتاجيتي: كيف أنقذني ‘اختبار العقود’ من جحيم انتظار الفرق الأخرى

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

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