السلسلة: هذا الدرس جزء من سلسلة MongoDB. اقرأ المقال الرئيسي.
MongoDB في الإنتاج لا يدور أبداً standalone. Replica set وحدة النشر المعيارية: 3 عقد تُكرّر نفس البيانات، تنتخب primary آلياً، تُبدّل في 10-30 ثانية إن سقط primary، وتُفعّل transactions multi-documents (مرفوضة على standalone).
المتطلبات
- 3 آلات (أو 3 منافذ على نفس الآلة للتطوير) مع MongoDB 8.0 LTS
- وصول root
- توصيل شبكي ثنائي بين العقد على المنفذ 27017
- ساعة مزامَنة عبر NTP
- 120 دقيقة
الخطوة 1 — لماذا replica set لا standalone
Standalone: آلة واحدة، عملية واحدة. سقطت = الخدمة تنقطع. Replica set: مجموعة عمليات MongoDB تتكرّر باستمرار عبر journal يُسمّى oplog. عقدة واحدة primary تقبل الكتابات؛ الأخريات secondaries تُطبّق oplog. إن اختفت primary، يكتشف secondaries غياب heartbeat ويُطلقون انتخاباً.
ثلاث فوائد إضافية: transactions multi-documents، قراءة موزَّعة عبر readPreference، نسخ احتياطية من secondary بلا حمل primary.
الخطوة 2 — ضبط 3 عمليات mongod
# /etc/mongod-rs0-a.conf
storage:
dbPath: /var/lib/mongodb-rs0-a
net:
port: 27017
bindIp: 127.0.0.1,192.168.1.10
replication:
replSetName: rs0
security:
keyFile: /etc/mongodb-keyfile
# الـ keyFile مشترك يحاكي العقد فيما بينها
openssl rand -base64 756 > /etc/mongodb-keyfile && chmod 400 /etc/mongodb-keyfile
# إطلاق كل خدمة
sudo systemctl start mongod-rs0-a mongod-rs0-b mongod-rs0-c
الخطوة 3 — تهيئة replica set
mongosh "mongodb://192.168.1.10:27017"
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "192.168.1.10:27017", priority: 2 },
{ _id: 1, host: "192.168.1.11:27017", priority: 1 },
{ _id: 2, host: "192.168.1.12:27017", priority: 1 }
]
});
rs.status();
Priority 2 على العقدة الأولى ليست أمراً، إنها تفضيل: مع تساوي البقية، هذه العقدة ستُنتخَب primary أولاً.
الخطوة 4 — آلية الانتخاب
بروتوكول انتخاب MongoDB مُستوحى من Raft. كل عضو يتبادل heartbeat كل ثانيتين. حين لا يستلم secondary heartbeat من primary لـ 10 ثوان (electionTimeoutMillis)، يُطلق انتخاباً.
3 قواعد تحسم: (1) المرشّح يجب أن يملك oplog الأحدث؛ (2) أغلبية صارمة من الأصوات؛ (3) priority للتعادل.
// فرض انتخاب يدوي (مفيد للاختبارات)
rs.stepDown(60); // primary الحالي يتنحى 60 ثانية
// رؤية primary الحالي
db.hello().primary;
// إعدادات replica set
rs.conf();
الخطوة 5 — عدد المُصوّتين والإعدادات الصالحة
| عدد العقد | Quorum مطلوب | أعطال محتملة | الاستخدام |
|---|---|---|---|
| 3 | 2 | 1 | الحد الأدنى للإنتاج |
| 5 | 3 | 2 | تحمّل أكبر، قراءة موزَّعة |
| 7 | 4 | 3 | geo-réplication متعددة المناطق |
| 3 (2 data + 1 arbitre) | 2 | 1 | اقتصاد قرص |
القاعدة الذهبية: عدد فردي من المُصوّتين (3، 5، 7). مع 4، تقسيم شبكي 2-vs-2 يشلّ الانتخاب. التوصية 2026: فضّل 3 data nodes على 2 data + arbitre.
الخطوة 6 — قراءات موزَّعة بـ readPreference
// Driver Node.js — URI مع readPreference
const uri = "mongodb://192.168.1.10:27017,192.168.1.11:27017,192.168.1.12:27017/" +
"?replicaSet=rs0&readPreference=secondaryPreferred";
// أو لكل عملية
db.collection("commandes").find(
{ statut: "payee" },
{ readPreference: "secondary", maxStalenessSeconds: 90 }
);
5 أوضاع: primary، primaryPreferred، secondary، secondaryPreferred، nearest. maxStalenessSeconds يستبعد secondaries المتأخرة جداً.
الخطوة 7 — Transactions متعددة الوثائق
const session = client.startSession();
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
try {
await db.collection("comptes").updateOne(
{ _id: idDebit }, { $inc: { solde: -10000 } }, { session }
);
await db.collection("comptes").updateOne(
{ _id: idCredit }, { $inc: { solde: +10000 } }, { session }
);
await db.collection("operations").insertOne(
{ type: "virement", debit: idDebit, credit: idCredit, montant: 10000 },
{ session }
);
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
await session.endSession();
}
3 قواعد: مدّة قصوى 60 ثانية، transactions مكلفة (تجنّبها إن وثيقة واحدة تكفي)، w: majority ضروري للضمان.
الخطوة 8 — اختبار failover في ظروف حقيقية
# على primary، محاكاة عطل وحشي
sudo systemctl stop mongod-rs0-a
# لاحظ في سجلات التطبيق:
# - 5-15 ثانية أخطاء "no primary"
# - استئناف آلي للكتابات على primary الجديد
# تحقّق من الطوبولوجيا الجديدة
mongosh "mongodb://192.168.1.11:27017"
rs.status(); // عقدة أخرى يجب أن تكون PRIMARY
# أعد تشغيل primary القديم، يعود SECONDARY
sudo systemctl start mongod-rs0-a
نافذة الخطأ النمطية 10-30 ثانية. drivers MongoDB 6.x+ تُعيد العمليات idempotent آلياً (retryable writes، مفعَّل افتراضياً).
الخطوة 9 — النسخ الاحتياطي من secondary
mongodump \
--uri="mongodb://user:pass@192.168.1.12:27017/?replicaSet=rs0&readPreference=secondary" \
--gzip \
--archive=/backup/dump_$(date +%Y%m%d).gz
# الاستعادة على primary (دائماً)
mongorestore \
--uri="mongodb://user:pass@192.168.1.10:27017/?replicaSet=rs0" \
--gzip \
--archive=/backup/dump_20260515.gz
القاعدة: كتابات الاستعادة على primary، قراءات النسخ الاحتياطي على secondary.
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
| لا primary منتخَب | لا أغلبية متصلة | أصلح التوصيل أو أضف عضواً مُصوّتاً |
| الانتخاب في حلقة | الساعات غير متزامنة | NTP، timedatectl set-ntp true |
| Secondary في RECOVERING دائم | تأخّر يفوق نافذة oplog | resync كامل: أوقف، فرّغ dbPath، أعد التشغيل |
| «Transaction numbers are only allowed on a replica set» | محاولة transaction على standalone | حوّل إلى replica set حتى مونو-عقدة |
| Driver يخسر 30 ثانية عند failover | serverSelectionTimeoutMS عالٍ جداً |
قلّصه إلى 5000 ms |
كتابات بطيئة مع w:majority |
secondary بعيد يجرّ الكمون | delayed أو priority أخفض |
أسئلة شائعة
هل يمكن إطلاق replica set مونو-عقدة؟ نعم، إلزامي لتفعيل transactions على بيئة تطوير. replSetName: rs0 + rs.initiate() بعضو واحد، العقدة فوراً primary.
كم تدوم الانتخابات؟ 10-30 ثانية نمطياً. electionTimeoutMillis الافتراضي 10 000.
هل Atlas يُغني عن فهم replica sets؟ لا. Atlas يدير النشر والصيانة، لكن دلالة transactions، writeConcern، readPreference تبقى مسؤوليتك جانب driver.
replica set ممتدّ على عدة datacenters؟ ممكن وموصى به للـ DR. وضع 3 عقد على 3 مناطق مختلفة يضمن البقاء حتى عند خسارة منطقة كاملة.
هل نُفضّل replica set أم sharding؟ replica set أولاً. يصل 30-50K كتابة/ثانية. sharding يُدخَل حين يُشبَع replica set واحد فقط. sharding بلا replica set داخلي غير موجود — كل shard نفسه replica set.