الإقليم ٤ — مشكلة الحالة: النسخ المتماثل (Primary-Replica)
النبذة
في الإقليم ٣ وزّعنا المستخدمين على خادمين بلا عناء — لأن طبقتَي web و app عديمتا الحالة (stateless): أي طلبٍ يصلح على أي خادم، فلا فرق. لكن لكل خادمٍ قاعدة بيانات، والبيانات حالة (state). وهنا تنكسر السهولة. هذا الإقليم يجيب عن سؤال Task 1: «كيف يعمل عنقود Primary-Replica، وما الفرق بين العقدة الأساسية والنسخة من منظور التطبيق؟»
موزّعك (round robin) أرسل مستخدماً ليغيّر بريده إلى خادم A، فكتب التغيير في قاعدة بيانات A. بعد ثانيةٍ أعاد التحميل، فأرسله الموزّع إلى خادم B، فقرأ من قاعدة بيانات B… البريد القديم. الموقع «نسي» ما كتبه للتوّ.
لماذا نجح توزيع الـ web/app وفشل توزيع البيانات؟ وكيف تجعل الخادمين يريان نفس البيانات دون أن تخلق تعارضاً؟ صمّم حلّك على ورقة. ستكتشف أنك أمام مقايضةٍ قاسية: نسخةٌ واحدةٌ من الحقيقة (SPOF) مقابل نسختين قد تتعارضان.
الدرس
ليش الحالة تختلف عن الكود — جذر المشكلة
الكود يُقرأ فقط وقت التشغيل: نسختان متطابقتان منه تعطيان نفس النتيجة دائماً، فاستنساخه مجاني. البيانات تُكتب وتتغيّر: اللحظة التي يكتب فيها خادمٌ بياناً، تصبح نسخته مختلفةً عن البقية. نسختان من قاعدة بيانات تقبلان الكتابة بحرّية = حقيقتان متناقضتان (write conflict). لذلك لا يمكن «مجرّد نسخ» قاعدة البيانات كما نسخنا الـ app. نحتاج بروتوكولاً يقرّر من يملك الحقيقة، وكيف تنتشر النسخ منها.
ليش Primary-Replica — تصميمٌ يحلّ التعارض بقرارٍ واحد
أبسط حلٍّ للتعارض: اسمح بالكتابة في مكانٍ واحدٍ فقط. هذا هو جوهر Primary-Replica (الاسم القديم Master-Slave):
- Primary (الأساسية): العقدة الوحيدة التي تقبل الكتابة (writes). كل تغييرٍ يمرّ عبرها — فلا تعارض، لأن هناك مصدرَ حقيقةٍ واحد.
- Replica (النسخة): نسخةٌ للقراءة فقط (read-only) من الـ primary. تستقبل تدفّق التغييرات من الأساسية وتطبّقها على نفسها لتبقى محدّثة.
كيف تبقى الـ replica محدَّثة؟ الأساسية تسجّل كل تغييرٍ في سجلّ (في MySQL اسمه binary log / binlog)، والنسخة تقرأ هذا السجلّ وتعيد تنفيذ التغييرات على نفسها. غالباً غير متزامن (asynchronous): الأساسية لا تنتظر النسخة، فقد تتأخّر النسخة لحظاتٍ خلف الأساسية (replication lag) — وهذا بالضبط ما سبّب لغز «البريد القديم». لكنه ثمنٌ مقبولٌ مقابل عدم التعارض.
ليش هذا يفيد التطبيق — التقسيم Read/Write
الآن صار للتطبيق قاعدتان بدورين مختلفين، وهذا ما يسأل عنه Task 1 («الفرق من منظور التطبيق»):
- التطبيق يرسل كل عمليات الكتابة (INSERT/UPDATE/DELETE) إلى الـ primary.
- ويرسل عمليات القراءة (SELECT) إلى الـ replica(s) — فيوزّع حِمل القراءة (وأغلب مواقع الويب قراءةٌ في معظمها).
إذاً الفرق من منظور التطبيق: الأساسية = حيث أكتب (وأقرأ ما يجب أن يكون فورياً)؛ النسخة = حيث أقرأ لأخفّف الحِمل. النسخة لا تقبل كتابةً من التطبيق إطلاقاً.
فائدة جانبية للتوفّر: إن ماتت الأساسية، يمكن «ترقية» نسخةٍ لتصير الأساسية الجديدة (failover) — وهذا تطبيقٌ عمليٌّ لـ Active-Passive من الإقليم ٣ على طبقة البيانات.
الوجه الآخر للعملة: لماذا «كاتبٌ واحد» مشكلة (سؤال Task 2)
Task 2 يسألك عن عيب هذه البنية: «لماذا وجود خادم MySQL واحدٍ قادرٍ على قبول الكتابة مشكلة؟». طبّق العدسة:
- SPOF للكتابة: الأساسية واحدة. لو ماتت، يستطيع الموقع أن يقرأ (من النسخ) لكن لا يستطيع أن يكتب — أي تعطّلٌ جزئيٌّ لكن حقيقي حتى يُرقّى بديل.
- اختناق للكتابة (write bottleneck): كل الكتابات تتكدّس على عقدةٍ واحدة؛ لا يمكن توزيع حِمل الكتابة كما وزّعنا القراءة. هذا سقفٌ للتوسّع.
(الحلول الأعمق — multi-primary، sharding — خارج نطاق هذا المشروع عمداً؛ المشروع يقول صراحةً «لا تدخل تفاصيل لم تُطلب». اعرف أن المشكلة موجودة وسببها، لا أكثر.)
بذرة Task 2/3: لماذا «كل الخوادم بنفس المكوّنات» مشكلة
لاحظ بنية Task 1: كل خادمٍ يحوي web + app + db معاً. Task 2 يسألك لماذا قد يكون هذا مشكلة. الجواب يولد من فصل الاهتمامات (إقليم ١):
- المكوّنات تتنافس على نفس موارد الصندوق (CPU/RAM/قرص): قاعدة البيانات الجائعة تخنق الـ web server في نفس الجهاز.
- لا يمكن توسيع كلٍّ على حدة: إن كان الاختناق في الـ app فقط، تُجبر على شراء صندوقٍ كامل (بقاعدة بيانات زائدة) لتوسّع جزءاً واحداً.
- أيُّ replica هنا ليست قاعدة بياناتٍ مستقلّة نظيفة، بل مدفونةٌ مع web/app.
هذا بالضبط ما يدفع Task 3 إلى فصل المكوّنات على خوادمها الخاصّة (إقليم ٧). سجّله.
تحليل الأخطاء
- «النسخة تقبل الكتابة أيضاً.» لا في هذا النموذج — لو قبلت، عاد التعارض الذي وُجد النموذج كله لمنعه. النسخة read-only من منظور التطبيق.
- «النسخ يجعل البيانات فوريةً على الجميع.» غالباً غير متزامن: هناك تأخّر (lag). إن طلب منطقك قراءةً فوريةً لما كُتب للتوّ، اقرأه من الأساسية.
- «Primary-Replica يحلّ اختناق الكتابة.» يحلّ اختناق القراءة فقط. الكتابة تبقى على عقدةٍ واحدة — وهذا عيبه المقصود في سؤال Task 2.
أضِف إلى مخطّط Task 1 أسهم النسخ المتماثل: سهمٌ من الأساسية إلى كل نسخة (تدفّق binlog)، وسمِّ أيُّ قاعدةٍ primary وأيُّها replica. ثم على ورقة:
- اشرح في ٣ أسطر كيف يعمل Primary-Replica (writes→primary، binlog→replica، reads→replica).
- اكتب الفرق من منظور التطبيق (أين أكتب، أين أقرأ).
- اكتب عيبَي «الكاتب الواحد» (SPOF كتابة + اختناق كتابة)، وعيبَ «كل المكوّنات على كل خادم».
الخلاصة — وصلٌ في الشجرة
- فهمتَ جذر مشكلة الحالة: الكود يُستنسخ مجاناً، البيانات تتعارض إن استُنسخت بحرّية.
- اشتققتَ Primary-Replica كحلٍّ بقرارٍ واحد (كاتبٌ واحد ⟵ لا تعارض)، وآليته (binlog، async، lag)، وتقسيم read/write من منظور التطبيق.
- ربطتَه بالإقليم ٣ (failover = Active-Passive على البيانات) وحضّرت إجابتَي Task 2 (عيب الكاتب الواحد) وبذرة Task 3 (فصل المكوّنات).
البذرة المتروكة: حتى الآن كل همّنا التوفّر والتوزيع. لكن الموقع الآن مكشوفٌ للإنترنت بلا حماية، وحركته تمرّ عاريةً يقرؤها أي متنصّت. قبل أن نوسّع أكثر، يجب أن نمنع الدخيل ونختم الكلام. هذا هو الإقليم ٥.
A Primary-Replica (Master-Slave) MySQL cluster has one Primary that accepts writes, and one or more Replicas that are read-only copies. The Primary records every change in its binlog; each Replica reads that log and replays it to stay in sync (usually asynchronously, so replicas can lag slightly).
From the application's view: it sends all writes to the Primary and spreads reads across Replicas. The Replica never takes writes from the app.
Why one writable MySQL is a problem: the Primary is a write SPOF and a write bottleneck — you can scale reads but not writes.
Why identical servers (db+web+app each) is a problem: components contend for the same resources and can't be scaled independently.
وقفة الانضباط: لا تذكر sharding/Galera/group-replication ما لم يُسأل. المشروع يطلب primary-replica فقط.