مقدمة الجزء التاني: ليه نوزع أصلًا؟
| المعمارية | الوصف | المشكلة |
|---|---|---|
| Shared-memory (scale up) | CPUs ورامات وهاردات كتير تحت OS واحد. | التكلفة بتزيد أسرع من الأداء، وفي مكان جغرافي واحد. |
| Shared-disk | ماكينات مستقلة بتشارك array هاردات بشبكة سريعة (data warehouses). | الـ locking والـ contention بيحدوا التوسع. |
| Shared-nothing ⭐ | كل نود (ماكينة) مستقلة بالكامل، والتنسيق على الشبكة في السوفتوير. | تعقيد في الأبلكيشن — وده موضوع الجزء ده كله. |
طريقتين لتوزيع الداتا (وغالبًا بيتعملوا مع بعض): Replication (نسخ نفس الداتا) و Partitioning (تقطيعها، الفصل 6).
ليه Replication؟
عشان: الداتا تبقى قريبة من المستخدم (latency)، السيستم يفضل شغال لو جزء وقع (availability)، وتوزيع القراءات على ماكينات أكتر (read throughput). الصعوبة كلها في التعامل مع التغييرات — لو الداتا ثابتة خلاص انسخها مرة وخلّصت.
تلات استراتيجيات أساسية: Single-leader، Multi-leader، Leaderless.
Single-Leader (Leader–Follower)
القراءة ممكن من أي حد، الكتابة من الـ leader بس. مستخدم في PostgreSQL، MySQL، Oracle Data Guard، SQL Server AlwaysOn، MongoDB، RethinkDB، Espresso، وكمان Kafka و RabbitMQ.
Synchronous vs Asynchronous
الـ leader يستنى الـ follower يأكد قبل ما يقول للعميل “تم”. ✅ الـ follower نسخة مضمونة. ❌ لو الـ follower وقف، كل الكتابات تقف. عمليًا: follower واحد sync والباقي async = semi-synchronous.
الـ leader مايستناش. ✅ مش بيقف أبدًا. ❌ لو الـ leader مات قبل ما يوصّل، كتابات اتأكدت للعميل هتضيع. ومع ذلك منتشر جدًا، خصوصًا مع followers كتير أو موزعين جغرافيًا.
إضافة follower جديد من غير downtime
لما نود تقع
- Follower وقع: سهلة — catch-up recovery: يشوف آخر transaction عنده في الـ log ويطلب الباقي.
- Leader وقع → Failover: (1) نكتشف إنه مات (غالبًا timeout)، (2) نختار leader جديد (انتخاب أو controller — الأفضل اللي عنده أحدث داتا = مشكلة consensus)، (3) نعيد ضبط السيستم (العملاء يكتبوا للجديد، والقديم لما يرجع يعرف إنه بقى follower).
إزاي الـ Replication Log بيتنفذ؟
| الطريقة | الفكرة | المشاكل |
|---|---|---|
| Statement-based | ابعت جمل SQL نفسها (INSERT/UPDATE). | دوال non-deterministic زي NOW() و RAND()، auto-increment لازم نفس الترتيب بالظبط، triggers و stored procedures. (MySQL القديمة استخدمته.) |
| WAL shipping | ابعت الـ WAL نفسه (بايتات على مستوى الديسك). PostgreSQL و Oracle. | مربوط جدًا بالـ storage engine — غالبًا لازم نفس الـ version على الليدر والفولورز، فالـ upgrade من غير downtime صعب. |
| Logical (row-based) | log منفصل بيوصف التغيير على مستوى الصف (insert بكل القيم، delete بالمفتاح، update بالقيم الجديدة). MySQL binlog. | ✅ منفصل عن الـ engine، ونسخ مختلفة تشتغل مع بعض، وأنظمة تانية تقدر تقراه = Change Data Capture. |
| Trigger-based | في الأبلكيشن: triggers تكتب التغييرات في جدول ويقراه process خارجي (Databus, Bucardo). | Overhead أكبر وباجات أكتر — بس مرونة كاملة. |
مشاكل الـ Replication Lag
مع async، الـ follower ممكن يكون متأخر — عادة كسور ثانية، بس تحت الضغط ممكن دقايق. ده بيدّي eventual consistency: “لو وقفت كتابة، في الآخر هيتساووا” — والـ”في الآخر” ده مش محدد! وده بيعمل 3 مشاكل مشهورة:
| المشكلة | اللي بيحصل | الضمان المطلوب والحل |
|---|---|---|
| 😱 اختفاء كتابتك | كتبت كومنت وعملت refresh → القراءة راحت لـ follower متأخر → الكومنت مش موجود! | Read-your-writes: اقرا الحاجات اللي ممكن اليوزر يعدلها (بروفايله) من الـ leader؛ أو لمدة دقيقة بعد آخر تعديل اقرا من الـ leader؛ أو الكلاينت يفتكر timestamp آخر كتابة. وخد بالك من cross-device (كتب من الموبايل وفتح من اللابتوب). |
| ⏪ الزمن بيرجع لورا | refresh مرتين: أول مرة من follower محدّث (شاف الكومنت)، تاني مرة من follower متأخر (اختفى!). | Monotonic reads: كل يوزر يقرا دايمًا من نفس الـ replica (مثلًا hash على الـ user ID). |
| 🔮 النتيجة قبل السبب | شايف الرد قبل السؤال، لأن البارتيشنز مختلفة بتتأخر بشكل مختلف. | Consistent prefix reads: الكتابات المرتبطة سببيًا تتكتب في نفس البارتيشن، أو تتبع الـ causal dependencies. |
Multi-Leader Replication
أكتر من نود بيقبل كتابات، وكل leader هو follower للباقيين. نادرًا ماتستاهل جوه داتاسنتر واحد، بس مفيدة في:
leader في كل داتاسنتر. ✅ latency أحسن، يستحمل وقوع داتاسنتر، والشبكة بين الداتاسنترات لو وقعت مش بتوقف الكتابة.
كل جهاز (تقويم الموبايل) هو leader فيه داتابيز محلية وبيعمل sync لما النت يرجع. (CouchDB مصمم لكده.)
Google Docs: كل يوزر بيكتب محلي وبيتعمل sync. ممكن تـ lock (زي single-leader) أو تخلي الوحدة صغيرة جدًا (ضغطة زرار).
التعامل مع الـ conflicts
- Avoidance (الأفضل): كل ريكورد ليه leader “بيته” — مثلًا كل يوزر يكتب دايمًا لداتاسنتر واحد. بيبوظ بس لو غيّرت الـ home.
- Convergence: لازم كل الـ replicas توصل لنفس القيمة في الآخر:
- LWW (Last Write Wins): كل كتابة ليها ID أو timestamp والأكبر يكسب — بسيط بس بيضيّع داتا.
- أولوية لـ replica معينة (برضه بيضيّع).
- دمج القيم (مثلًا “B/C” بترتيب أبجدي).
- تسجّل التعارض وتسيبه للأبلكيشن أو اليوزر.
- Custom logic: on write (handler يتنفذ وقت اكتشاف التعارض — Bucardo) أو on read (كل النسخ ترجع والأبلكيشن/اليوزر يختار — CouchDB).
- أفكار أوتوماتيك: CRDTs (data structures بتتدمج لوحدها — Riak)، mergeable persistent data structures (زي Git — three-way merge)، وOperational Transformation (Etherpad, Google Docs).
Topologies
الدايري والنجمة: لو نود واحدة وقعت السلسلة اتقطعت (ولازم كل نود تسجل ID عشان الرسالة ماتلفش للأبد). الـ all-to-all أكثر fault tolerance، بس ممكن الرسايل توصل بترتيب غلط (update قبل الـ insert) → الحل: version vectors. وخد بالك إن conflict detection مش متطبق صح في سيستمات كتير.
Leaderless Replication (Dynamo-style)
مفيش leader! الكلاينت (أو coordinator) بيبعت الكتابة لكذا replica مع بعض، والقراءة كمان من كذا replica. فكرة Amazon Dynamo، ومستخدمة في Riak، Cassandra، Voldemort.
Quorums
طب الـ replica اللي كانت واقعة ورجعت إزاي تلحق؟
وقت القراءة لو لقيت replica بقيمة قديمة (بالـ version number) تكتب لها الجديدة. كويس للداتا اللي بتتقرا كتير.
process في الخلفية بيقارن الـ replicas ويصلح الفروقات (مش بترتيب، وممكن يتأخر).
Sloppy Quorums و Hinted Handoff
لو الشبكة قطعت والكلاينت مش واصل للـ n “الأصليين”، نقبل الكتابة على أي w نودز متاحة (حتى لو مش من “بيت” المفتاح)، ولما الشبكة ترجع تتنقل لبيتها الأصلي = hinted handoff. ده بيزود الـ write availability بس يكسر ضمان الـ quorum — ده مجرد “ضمان durability” إن الداتا متخزنة في w مكان. وفي multi-datacenter: Cassandra/Voldemort بيعملوا n موزعة على الداتاسنترات و quorum محلي، و Riak بيخلي الـ replication بين الداتاسنترات async.
اكتشاف الكتابات الـ Concurrent
الأحداث ممكن توصل بترتيب مختلف لكل نود — لو كل نود خلّت آخر قيمة شافتها، النودز مش هتتساوى أبدًا. الحلول:
- LWW: نجبر ترتيب (timestamp) ونرمي الباقي. Cassandra بتعتمده. بيوصل لـ convergence بس بيضيّع كتابات اتقال للكلاينت إنها نجحت. الاستخدام الآمن الوحيد: كل key يتكتب مرة واحدة بس (immutable، زي UUID كمفتاح).
- Happens-before: عملية B “بتعتمد” على A لو B عارفة بـ A أو مبنية عليها. لو مفيش واحدة عارفة التانية → concurrent. “Concurrent” هنا مش معناها في نفس اللحظة بالظبط — معناها محدش فيهم كان عارف بالتاني.
ومع كذا replica بدل version number واحد: Version vector — version لكل replica لكل key، والكلاينت بيبعته مع الكتابة. Riak بيستخدم dotted version vector وبيبعته كـ”causal context”. (version vector ≠ vector clock بالظبط بس قريبين.)
🧪 Lab
شغّل Postgres primary + replica بـ Docker (فيه images جاهزة زي bitnami/postgresql بتدعم replication بـ env vars — راجع الـ docs).
- اكتب في الـ primary واقرا من الـ replica فورًا.
- وقّف الـ replica، اكتب كذا صف، ورجّعها — شوف الـ catch-up.
- شوف الـ lag بنفسك:
-- على الـ primarySELECT client_addr, state, replay_lag FROM pg_stat_replication;