الداتا الموزعة · فصل ٦

الـ Partitioning: قطّع الداتا ووزّعها

Partitioning (Sharding)
🎯 الهدف: تقسم داتا أكبر من ماكينة واحدة من غير hot spots، وتعرف تعمل rebalancing صح.

الفكرة

كل record بيتبع partition واحد بالظبط، وكل partition كأنه داتابيز صغيرة. الأسماء بتختلف: shard (MongoDB, Elasticsearch)، region (HBase)، tablet (Bigtable)، vnode (Cassandra, Riak)، vBucket (Couchbase). الهدف الأساسي: Scalability — توزيع الداتا والكويريز على ماكينات كتير.

وعادة بيتعمل مع الـ replication: كل partition له نسخ على كذا نود، وكل نود ممكن تبقى leader لبعض الـ partitions و follower لغيرها.

تقسيم الـ Key-Value Data

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

📚 By Key Range

زي مجلدات الموسوعة: A–B، C–D… كل partition مدى متصل من المفاتيح. (حدود المدى مش لازم متساوية — بتتظبط حسب الداتا.)
✅ Range queries ممتازة (المفاتيح مترتبة جوه الـ partition).
❌ Hot spots: لو المفتاح timestamp، كل كتابات النهارده تروح لـ partition واحد! الحل: حط حاجة قبل الوقت (اسم السينسور + الوقت).
Bigtable, HBase, RethinkDB, MongoDB (قبل 2.4)

#️⃣ By Hash of Key

طبّق hash (مش لازم يكون قوي cryptographically، بس لازم ثابت — مش زي hashCode() في Java اللي ممكن يختلف بين processes)، وكل partition ياخد مدى من الـ hashes.
✅ توزيع عادل.
❌ ضاع الترتيب → range queries لازم تتبعت لكل الـ partitions.
Cassandra, Riak, Voldemort, MongoDB (hash mode)

Skew و Hot Spots

حتى الـ hash مش بيحل كل حاجة: لو key واحد عليه حمل خرافي (سيليبريتي عنده ملايين متابعين بيعلقوا على بوسته)، كل الطلبات هتروح لنفس المكان. أغلب الداتابيزات مش بتحلها أوتوماتيك. الحل اليدوي: ضيف رقم عشوائي من 2 خانات لآخر المفتاح الساخن → يتوزع على 100 مفتاح. بس القراءة بقت لازم تجمع من الـ 100، ولازم تعرف أنهي مفاتيح محتاجة ده (book-keeping).

Partitioning والـ Secondary Indexes

الـ primary key سهل. بس السؤال: “هات كل العربيات اللون الأحمر” — الـ index ده نقسمه إزاي؟

📄 Document-partitioned (Local index)🏷️ Term-partitioned (Global index)
الفكرةكل partition عنده index لداتاه هو بس.index واحد global بس مقسوم بقيمة الـ term نفسه (color:red في partition، color:silver في partition تاني).
الكتابة✅ سهلة — partition واحد.❌ أبطأ — كتابة document واحد ممكن تعدل كذا partition في الـ index.
القراءة❌ Scatter/gather: تسأل كل الـ partitions → tail latency amplification.✅ partition واحد بس للـ term.
ملاحظاتMongoDB, Riak, Cassandra, Elasticsearch, SolrCloud, VoltDB.التحديث غالبًا async (DynamoDB: كسور ثانية عادة) فممكن تقرا index مش محدث.

Rebalancing — إعادة التوزيع

الحمل بيزيد، الداتا بتكبر، ماكينة بتموت… فلازم ننقل partitions بين النودز. المتطلبات: الحمل يفضل عادل، الداتابيز تفضل شغالة أثناء النقل، وماننقلش داتا أكتر من اللازم.

الاستراتيجيةإزايملاحظات
Fixed number of partitionsاعمل partitions أكتر بكتير من النودز (مثلًا 1000 لـ 10 نودز). نود جديدة “تسرق” partitions كاملة من الباقيين. الـ key → partition ثابت، اللي بيتغير بس partition → node.Riak, Elasticsearch, Couchbase, Voldemort. لازم تختار العدد صح من الأول: كبير أوي = overhead، صغير أوي = partitions ضخمة صعب نقلها.
Dynamic partitioningزي B-tree: partition كبر عن حد معين (HBase: 10GB) يتقسم نصين؛ صغر يتدمج مع جاره.HBase, RethinkDB, MongoDB. عدد الـ partitions بيتكيف مع حجم الداتا. مشكلة: داتابيز فاضية = partition واحد → pre-splitting. ينفع للـ range والـ hash.
Proportional to nodesعدد ثابت من الـ partitions لكل نود. نود جديدة تختار partitions عشوائي وتقسمهم وتاخد نصهم.Cassandra (256 لكل نود افتراضيًا)، Ketama. محتاج hash partitioning.

Request Routing — الطلب يروح فين؟

ده جزء من مشكلة أعم اسمها service discovery. تلات طرق:

1. أي نود

الكلاينت يكلم أي نود (round-robin). لو مش عندها الـ partition تحوّل الطلب. (Cassandra/Riak: gossip protocol بين النودز.)

2. Routing tier

طبقة في النص partition-aware load balancer بتوجه الطلب.

3. Client واعي

الكلاينت نفسه عارف الـ partition على أنهي نود.

السؤال الصعب: إزاي اللي بيوجّه يعرف التغييرات؟ ده consensus. أنظمة كتير بتستخدم ZooKeeper: كل نود بتسجل نفسها، وZooKeeper يحتفظ بالـ mapping الموثوق، والـ routing tier يعمل subscribe على التغييرات. (HBase, SolrCloud, Kafka بتستخدمه؛ LinkedIn Espresso بيستخدم Helix فوقه؛ MongoDB عنده config servers و mongos.) وعمومًا الكلاينت بيعرف IPs الـ routing tier عن طريق DNS.

Parallel Query Execution

الـ NoSQL غالبًا بيدعم كويريز بسيطة على key واحد. لكن داتابيزات MPP (Massively Parallel Processing) — المستخدمة في الـ analytics — بتقسم الكويري المعقدة (joins, filters, aggregations) لمراحل بتتنفذ بالتوازي على نودز وpartitions مختلفة.

🧪 Lab

جرّب الـ declarative partitioning في Postgres:

CREATE TABLE events (id bigserial, user_id int, created_at timestamptz)  PARTITION BY HASH (user_id);CREATE TABLE events_p0 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 0);CREATE TABLE events_p1 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 1);CREATE TABLE events_p2 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 2);CREATE TABLE events_p3 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 3);EXPLAIN SELECT * FROM events WHERE user_id = 7;      -- partition واحدEXPLAIN SELECT * FROM events WHERE created_at > now() - interval '1 day';  -- كله

بعدين جرّبها PARTITION BY RANGE (created_at) وقارن.

✅ الخلاصة — Checklist

🧠 اختبر نفسك
١) سينسورات بتكتب بالـ timestamp كمفتاح بـ range partitioning — المشكلة والحل؟
كل الكتابات الحالية في partition واحد (hot spot). حط اسم السينسور قبل الـ timestamp.
٢) ليه global index أسرع في القراءة؟
لأن كل term في partition واحد، مش محتاج scatter/gather.
٣) ليه rebalancing أوتوماتيك بالكامل خطر؟
ممكن يتلخبط مع failure detection ويعمل cascading failure.