قواعد البيانات والتخزين

التقسيم والتجزئة (Partitioning & Sharding)

18 دقيقة الدرس 6 من 10

التقسيم والتجزئة (Partitioning & Sharding)

لكل خادم قاعدة بيانات سقف لا يتجاوزه. في مرحلة معينة — سواء أكانت 500 غيغابايت من البيانات، أو 50,000 عملية كتابة في الثانية، أو فحص جدول يستغرق 40 ثانية — يعجز الجهاز ببساطة عن المواكبة. التقسيم (Partitioning) والتجزئة (Sharding) هما التقنيتان الأساسيتان لكسر هذا السقف، وذلك بتوزيع البيانات عبر وحدات تخزين متعددة بحيث لا تحتوي أي عقدة منفردة على كل شيء.

إن فهم أين وكيف تُقسّم البيانات — واختيار مفتاح التقسيم المناسب — من أهم القرارات ذات الأثر البالغ في تصميم الأنظمة واسعة النطاق. أخطئ في هذا القرار وستستبدل اختناقاً باختناق آخر. أصبه وسيتوسع نظامك بشكل شبه خطي.

التقسيم مقابل التجزئة — المصطلحات

كثيراً ما يُستخدم المصطلحان بالتبادل، لكن لكل منهما معنى دقيق:

  • التقسيم (Partitioning) — تقسيم الجدول إلى أجزاء متعددة. قد تقع هذه الأجزاء على نفس الخادم الفعلي (داخل نسخة قاعدة بيانات واحدة) أو على خوادم مختلفة. تقسيمات PostgreSQL وMySQL وOracle أمثلة على التقسيم داخل الخادم الواحد.
  • التجزئة (Sharding) — شكل محدد من التقسيم الأفقي حيث تقع كل قطعة (shard) على خادم مختلف (أو مجموعة خوادم). إعداد من 10 shards يعني 10 نسخ مستقلة من قاعدة البيانات، كل منها تمتلك نحو 1/10 من البيانات.

عملياً: التجزئة دائماً تقسيم أفقي؛ لكن التقسيم الأفقي ليس بالضرورة تجزئة. في مقابلات تصميم الأنظمة والنقاشات الهندسية، تعني "التجزئة" شبه دائماً توزيع البيانات عبر مضيفي قواعد بيانات متعددة.

التقسيم الأفقي مقابل الرأسي

قبل الغوص في استراتيجيات التجزئة، افهم محوري التقسيم:

  • التقسيم الأفقي (التجزئة) — تقسيم الصفوف. كل قسم يحتوي على مجموعة فرعية من الصفوف لكن بنفس الأعمدة. جدول المستخدمين بمليار صف قد يُقسَّم بحيث يحتوي Shard 1 على المستخدمين 1–100 مليون، وShard 2 على 100–200 مليون، وهكذا.
  • التقسيم الرأسي — تقسيم الأعمدة. نقل الأعمدة الساخنة (كثيرة الوصول) إلى جدول وأعمدة الأرشيف (النصوص الضخمة، حقول التدقيق) إلى جدول آخر. يُقلّص عرض الصف في المسار الساخن ويُحسّن كفاءة الكاش، وعادةً يُطبَّق داخل نسخة قاعدة بيانات واحدة.
يركّز هذا الدرس على التقسيم الأفقي / التجزئة لأنه التقنية التي تُمكّن النظام من تجاوز طاقة الكتابة والتخزين لأي جهاز منفرد.

لماذا التجزئة أصلاً؟

تحل التجزئة أربع مشاكل تظهر مع نمو مجموعة البيانات وحركة المرور:

  1. حدود التخزين — يسع قرص SSD منفرد نحو 30–100 تيرابايت. إن بلغت بياناتك 500 تيرابايت، لا يمكن لجهاز واحد احتواءها.
  2. حدود معدل الكتابة — يعالج خادم قاعدة البيانات الرئيسي نحو 10,000–100,000 عملية كتابة/ثانية حسب حجم العمل. Twitter في ذروة نشاطه كان يستقبل نحو 6,000 تغريدة/ثانية، فيما كان التخزين الداخلي يستوعب كتابات توزيع الخلاصات أضعاف ذلك.
  3. تنافس الأقفال — على عقدة واحدة، تُنشئ الصفوف الساخنة نقاط احتقان في الأقفال. توزيع هذه الصفوف عبر shards يُقلّل التنافس على كل منها.
  4. زمن الاستجابة تحت الضغط — يتصاعد وقت تنفيذ الاستعلام مع نمو الجداول. Shards أصغر تعني فهارس أصغر وفحصاً أسرع.
التجزئة خيار أخير لا خطوة أولى. تُضيف تعقيداً تشغيلياً هائلاً: الاستعلامات العابرة للـshards، المعاملات الموزعة، إعادة التوازن، وتغييرات المخطط — كلها تصبح مشاكل صعبة. استنفد كل خيار آخر أولاً — التوسع الرأسي، ونسخ القراءة، والكاش، وتحسين الاستعلامات، والفهارس الأفضل — قبل اللجوء للتجزئة.

استراتيجيات التجزئة

توجد أربع استراتيجيات رئيسية لتحديد Shard الصف المعين. كل منها تُقايض خصائص مختلفة.

1. التقسيم بالنطاق (Range-Based)

تُسنَد الصفوف لـShards بناءً على نطاق قيمة المفتاح. مثلاً: المستخدمون ذوو id من 1 إلى 1,000,000 يذهبون لـShard A؛ من 1,000,001 إلى 2,000,000 لـShard B.

  • ميزة: استعلامات النطاق فعّالة — كل بيانات نطاق تاريخي أو نطاق معرّف موجودة على Shard واحد.
  • عيب: يُنشئ نقاط ساخنة (Hot Spots). إن كانت المعرّفات تتزايد تسلسلياً، تصب جميع الكتابات الجديدة دائماً في الـShard الأخير.

2. التقسيم بالدالة التجزيئية (Hash-Based)

طبّق دالة hash على مفتاح التقسيم: shard = hash(user_id) % N. التوزيع شبه منتظم، لا يتلقى أي Shard كل حركة المرور الجديدة.

  • ميزة: توزيع متساوٍ للحمل، لا نقاط ساخنة في أعباء العمل العشوائية.
  • عيب: استعلامات النطاق مكلفة — بيانات نطاق المعرّفات مبعثرة عبر كل الـShards. تغيير N (إضافة أو حذف Shard) يستلزم إعادة التجزئة لكل البيانات — مُكلف للغاية.

3. التجزئة المتسقة (Consistent Hashing)

نسخة متقدمة من التقسيم التجزيئي. ضع الـShards على نقاط في حلقة مفاهيمية (فضاء hash من 0 إلى 2³²). يُعيَّن كل مفتاح للـShard الأقرب في اتجاه عقارب الساعة. عند إضافة Shard أو حذفه، تنتقل فقط البيانات الموجودة على القوس المجاور — تقريباً 1/N من البيانات لا كلها.

  • تستخدمه: DynamoDB وCassandra وAmazon S3 وChord DHT.
  • ميزة: حركة بيانات ضئيلة عند تغيير الـShards؛ إعادة توازن أنيقة.
  • عيب: أعقد تنفيذاً؛ دون العقد الافتراضية (vnodes) قد يكون التوزيع غير متساوٍ.

4. التقسيم بالدليل (Directory-Based)

يحتفظ خدمة بحث ("الدليل") بتعيين من مفتاح إلى Shard. تستعلم طبقة التوجيه من الدليل للعثور على الـShard الصحيح لكل طلب.

  • ميزة: مرونة قصوى — يمكن نقل أي مفتاح لأي Shard في أي وقت.
  • عيب: الدليل نقطة فشل مفردة واختناق محتمل. التخزين المؤقت للدليل يساعد لكن يُضيف خطر القِدَم.
Sharding strategies comparison: Range vs Hash vs Consistent Hash Range Partitioning shard = range(key) Shard A id: 1 — 1,000,000 Shard B id: 1,000,001 — 2,000,000 Shard C id: 2,000,001 — 3,000,000 ⚠ New writes → Shard C (hot spot) ✓ Range queries fast ✕ Sequential hot spots ✓ Simple to reason about Hash Partitioning shard = hash(key) % N Shard 0 hash(key)%3 == 0 Shard 1 hash(key)%3 == 1 Shard 2 hash(key)%3 == 2 ✓ Even distribution ✕ Range queries hit ALL shards ✕ Resharding = rehash all data ✓ No sequential hot spots Consistent Hashing key → ring → nearest shard S-A S-B S-C S-D key Add/remove shard: only ~1/N keys move ✓ Minimal rebalancing ✓ Used in Cassandra, Dynamo ✕ Complexity; needs vnodes
مقارنة ثلاث استراتيجيات للتجزئة جنباً إلى جنب: التقسيم بالنطاق (بسيط لكن عرضة للنقاط الساخنة)، التقسيم التجزيئي (توزيع متساوٍ لكن تغيير الحجم مكلف)، والتجزئة المتسقة (إعادة توازن ضئيلة، تستخدمها Cassandra ودايناموDB).

اختيار مفتاح التقسيم

مفتاح التقسيم (ويُعرف أيضاً بـمفتاح الـShard) هو القرار الأهم في نظام متجزأ. يحدد توزيع البيانات، وتوجيه الاستعلامات، ومدى الحاجة إلى عمليات عابرة للـShards.

يجب أن يستوفي المفتاح الجيد هذه المعايير في آنٍ واحد:

  1. أعداد كبيرة من القيم المميزة (High Cardinality) — يجب أن تتوفر قيم مميزة كافية لتوزيع البيانات عبر كل الـShards. حقل بوليّ مثل is_active له قيمتان فقط؛ لا يمكنك امتلاك أكثر من Shard-ين. user_id بملايين القيم المميزة أفضل بكثير.
  2. التوزيع المتساوي — يجب ألا يُركّز المفتاح معظم الصفوف في بضعة Shards. إن كان 80% من حركة المرور لمستخدمين VIP وقسّمت حسب فئة المستخدم، سيتحمل Shard واحد معظم الحمل.
  3. تركيز الاستعلامات (Query Locality) — يجب أن تكون الاستعلامات الأكثر شيوعاً قابلة للإجابة من Shard واحد. إن كان استعلامك الأكثر شيوعاً "أعطني كل طلبات مستخدم ما"، فالتجزئة بـuser_id تجمع طلبات كل مستخدم في Shard واحد. التجزئة بـorder_date بدلاً من ذلك تبعثر طلبات المستخدم في كل مكان.
  4. تجنب النمو أحادي الاتجاه — المعرّفات المتزايدة تلقائياً والطوابع الزمنية تتدفق في اتجاه واحد، محشورةً الكتابات الجديدة في الـShard الأخير. مفاتيح UUID الإصدار 4 أو المشتقة من hash تتجنب هذا.
مثال من الواقع — Instagram: تجزّئ Instagram جدول الوسائط بـمعرّف وسائط يُضمّن طابع التاريخ ومعرّف الـShard في الأجزاء العليا من عدد صحيح 64-bit (تقنية مشابهة لمعرّفات Twitter Snowflake). يمنحهم ذلك: (أ) معرّفات قابلة للترتيب زمنياً دون مولّد تسلسل مركزي، (ب) توزيعاً متساوياً لأن المكوّن الزمني يملأ كل الـShards، (ج) توجيهاً فورياً — الـShard مُرمَّز في المعرّف، لا حاجة لبحث.

مشكلة النقاط الساخنة بالتفصيل

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

السبب 1 — الكيانات المشهورة: في منصة اجتماعية مجزَّأة بـuser_id، مشهور يتابعه 100 مليون شخص ومنشوراته تُطلق كتابات تشعب لـ100 مليون خلاصة ستُحدث ضغطاً كتابياً هائلاً على الـShard الذي يحتوي بيانات ذلك الشخص.

الحل: كشف المشاهير/المفاتيح الساخنة؛ تخزين بياناتهم في Shard مخصص أو نسخ بياناتهم عالية القراءة على كل الـShards.

السبب 2 — النقاط الساخنة الزمنية: جدول سلاسل زمنية مجزَّأ حسب التاريخ: Shard اليوم يستوعب كل الكتابات بينما كل Shard آخر خامل.

الحل: استخدام hash لـ(timestamp + ملح عشوائي) كمفتاح Shard، أو سجل كتابة مسبقة يوزّع بيانات السلاسل الزمنية بالتساوي.

السبب 3 — Hash غير متساوٍ: حتى مع مفتاح hash، إن كان توزيع فضاء المفاتيح منحازاً في جوهره (90% من المستخدمين في دولة واحدة ورمز الدولة جزء من المفتاح)، ستكون بعض الـShards أكبر.

الحل: التجزئة المتسقة مع العقد الافتراضية (vnodes) — كل Shard فعلي يمتلك أقواساً صغيرة متعددة على الحلقة، مما يُلطّف الانحياز.

العمليات العابرة للـShards — الجانب الصعب

بمجرد توزيع البيانات عبر Shards، تصبح العمليات التي تمس Shards متعددة مكلفة:

  • الاستعلامات العابرة للـShards — استعلام SELECT يشمل Shards متعددة يجب إرساله لكل Shard بالتوازي ودمج النتائج في طبقة التطبيق (scatter-gather). يُضيف تأخيراً وحملاً.
  • الدمج (JOIN) عبر الـShards — لا يدعمه محرك قاعدة البيانات؛ يجب تنفيذه في التطبيق. لهذا يُركّز تصميم التجزئة على تركيز الاستعلامات.
  • المعاملات عبر الـShards — معاملات ACID حقيقية عبر Shards تتطلب بروتوكول معاملات موزّعة (2PC — التأكيد ثنائي المرحلة)، وهو بطيء ويُدخل خطر فشل المنسّق. معظم الأنظمة المجزّأة تتجنب المعاملات الموزعة بإعادة تصميم نموذج البيانات للحفاظ على الصفوف المرتبطة في Shard واحد.
Query routing in a sharded system: scatter-gather vs single-shard lookup Single-Shard Lookup (fast) Application Shard Router hash(user_id) % 3 = 1 query user_id=42 Shard 1 1 network hop → result returned Latency: ~1 ms Scatter-Gather (slow) Application Shard Router hits ALL shards (no key) query: last 7 days Shard 0 Shard 1 Shard 2 Merge results in app layer Latency: ~10-100 ms (fan-out)
البحث في Shard واحد (يساراً) يُوجَّه مباشرةً باستخدام مفتاح التقسيم — سريع، نقفزة شبكية واحدة. الـScatter-Gather (يميناً) يتفرع لكل Shard حين لا يوجد مفتاح تقسيم — بطيء ومكلف على نطاق واسع.

إعادة توازن الـShards

مع نمو البيانات أو إضافة/حذف عقد Shard، تحتاج إلى نقل البيانات بين الـShards. وهذا ما يُعرف بـإعادة التوازن (Rebalancing) وهو تحدٍّ تشغيلي كبير:

  • مع hash % N البسيطة، إضافة Shard واحد يُغيّر المعادلة لتقريباً جميع المفاتيح — يجب نقل تقريباً كل صف.
  • مع التجزئة المتسقة، تهاجر فقط المفاتيح الموجودة على القوس المجاور للعقدة الجديدة — تقريباً 1/N من البيانات.
  • أثناء إعادة التوازن، يجب الاستمرار في خدمة القراءات والكتابات. النمط الشائع: بدء نقل البيانات في الخلفية، استخدام طبقة توجيه تقرأ من الموقعين القديم والجديد حتى تأكيد اكتمال النقل، ثم تحويل كل حركة المرور.

التجزئة في الواقع

  • MySQL (Vitess) — بنت YouTube نظام Vitess لتجزئة MySQL أفقياً. كل Shard نسخة MySQL قياسية؛ Vitess يتولى التوجيه وإعادة التوازن والـScatter-Gather. مفتوح المصدر الآن ويستخدمه GitHub وPlanetScale وآخرون.
  • Cassandra — تستخدم التجزئة المتسقة مع vnodes بشكل أصيل. كل عقدة تمتلك نطاقات رموز متعددة على الحلقة. عامل النسخ يتحكم في كم من العقد تحتفظ بنسخة من كل نطاق رموز.
  • MongoDB — تُوزّع المجموعات في مجموعات الـShards حسب مفتاح الـShard. يتولى mongos الـScatter-Gather للاستعلامات التي لا يمكن توجيهها لـShard واحد.
  • DynamoDB — تتولى AWS التجزئة بشفافية. اختيار مفتاح التقسيم ما زال مهماً لتجنب القسمات الساخنة، لكن البنية التحتية تدير إعادة التوازن تلقائياً.
لا تحتاج معظم التطبيقات لتجزئة قاعدة بياناتها بنفسها. قواعد البيانات المُدارة كـAurora وCloud Spanner وCockroachDB توفر قابلية التوسع الأفقي مع إخفاء تفاصيل التجزئة. هدف هذا الدرس أن تفهم الآليات لتستطيع الاستدلال على المقايضات، وتصميم مفاتيح التقسيم بشكل صحيح، وفهم نقاشات تصميم الأنظمة بعمق — لا بالضرورة تنفيذ التجزئة الخام من الصفر.

الخلاصة

  • التقسيم الأفقي (التجزئة) يُوزّع الصفوف عبر عقد قاعدة بيانات متعددة، مما يُمكّن التخزين ومعدل الكتابة من النمو متجاوزَين طاقة جهاز واحد.
  • الاستراتيجيات الأربع الرئيسية هي: النطاق (بسيط، نقاط ساخنة)، الـHash (توزيع متساوٍ، تغيير الحجم مكلف)، التجزئة المتسقة (إعادة توازن ضئيلة، تستخدمها Cassandra ودايناموDB)، والدليل (مرونة، تضيف حمل البحث).
  • اختيار مفتاح التقسيم الجيد يتطلب أعداداً كبيرة من القيم المميزة، وتوزيعاً متساوياً، وتركيزاً للاستعلامات، وتجنب النمو أحادي الاتجاه.
  • العمليات العابرة للـShards (الدمج، المعاملات، استعلامات النطاق) مكلفة ويجب تقليلها بالتصميم.
  • جزّئ عند الضرورة فحسب — التعقيد التشغيلي عالٍ؛ استنفد التوسع الرأسي والنسخ والكاش والفهرسة أولاً.