كيف نصمم نسخة مبسطة من WhatsApp لـ 100 مليون مستخدم باستخدام Node.js؟
كيف نصمم نسخة من تطبيق WhatsApp ل 100 مليون مستخدم باستخدام Node.js
كجزء من رحلتي في تعمق مفاهيم الـ Backend وقواعد البيانات، قمت بتحليل معمارية نظام WhatsApp وكيف يمكننا بناء نظام شبيه برمجياً هندسياً ليتعامل مع حجم بيانات ضخم وملايين المستخدمين المتزامنين (Concurrent Users). إليك تفكيك النظام برمجياً:
1. تصميم قاعدة البيانات (Database Schema using Mongoose)
بما أننا نتعامل مع بيانات ضخمة، سنعتمد على MongoDB لتخزين المستندات المرنة، ونشكل الموديلات عبر Mongoose كالتالي:
- User Schema: لتخزين بيانات المستخدمين وحالة اتصالهم من خلال حقل الحالات الحية (
isOnline: Boolean) ووقت آخر ظهور (lastSeen). - Chat Schema: يربط المستخدمين ببعضهم ويحتوي على مصفوفة للمشاركين باستخدام الـ References لتحديد أعضاء المحادثة (سواء فردية أو جماعية):
javascript
participants: [{ type: Schema.Types.ObjectId, ref: 'User' }]
Use code with caution.
- Message Schema: يحتوي على نص الرسالة، معرف المرسل
senderId، معرف المحادثةchatId، وحقل لمتابعة الحالةstatusوتأخذ القيم (sent,delivered,read).
2. كيف نحفظ ونعرض الرسائل بسرعة؟ (Message Storage & Pagination)
الرسائل تحفظ كـ Documents داخل قاعدة البيانات. لجلبها بسرعة دون إرهاق السيرفر مع 100 مليون مستخدم، لا نقوم بجلب المحادثة كاملة دفعة واحدة، بل نطبق مفهوم الـ Pagination القائم على الوقت (Cursor-based Pagination) لضمان عدم تكرار الرسائل، ونجلب آخر 20 رسالة فقط عند فتح المحادثة باستخدام:
javascript
limit(20).sort({ createdAt: -1 })
Use code with caution.
3. تتبع حالة الرسالة الذكي (Sent / Delivered / Read)
نعتمد هنا على اتصالات فوريّة عبر بروتوكول الـ WebSockets (باستخدام Socket.io) بجانب الـ REST APIs:
- Sent: عند إرسال طلب POST، تُحفظ الرسالة في قاعدة البيانات وتأخذ الحالة الافتراضية
sent(صح واحد). - Delivered (الهاتف متصل والتطبيق مغلق): النظام لا ينتظر فتح التطبيق. إذا كان المستلم غير متصل بالـ Socket (Offline)، يقوم السيرفر بإرسال إشعار دفع صامت (Silent Push Notification) عبر FCM أو APNs. يستقبله نظام تشغيل الهاتف في الخلفية، ويفتح اتصالاً سريعاً مع السيرفر لسحب الرسالة، ثم يرسل الهاتف إشارة تأكيد تلقائية تسمى Acknowledgment (ACK). فوراً يقوم السيرفر بتحديث الحالة في قاعدة البيانات إلى
delivered(صحين رمادي). - Read: بمجرد فتح المستلم لشاشة المحادثة، يرسل التطبيق حدثاً فوريّاً (Socket Event) أو طلب PATCH لتحديث حقل الـ
statusإلىread(صحين زرق).
4. التعامل مع المستخدم غير المتصل (Offline Queue)
إذا كان هاتف المستلم مغلقاً تماماً وفشلت إشعارات الدفع الصامتة في الوصول إليه:
تظل الرسالة بحالة sent في قاعدة البيانات. بمجرد أن يفتح المستخدم الهاتف ويصبح متصلاً (Online) ويُنشئ اتصال الـ WebSocket الجديد، يقوم السيرفر بعمل استعلام سريع عن جميع الرسائل الخاصة به التي تحمل حالة sent ويرسلها له دفعة واحدة (Bulk Delivery)، ثم يُحدّث حالتها في قاعدة البيانات إلى delivered.
5. الدفاع ضد الهجمات المليونية (Handling Spam & DDoS)
لحماية السيرفر وقاعدة البيانات من محاولات إرسال 1000 رسالة دفعة واحدة، نضع خطين دفاعيين صارمين:
- Rate Limiter Middleware: نضع Middleware (مثل
express-rate-limitمدعوماً بـ Redis) يحدد أقصى حد للطلبات (مثلاً 5 رسائل في الثانية لكل IP أو User)، وفي حال التجاوز يتم إرجاع خطأ429 Too Many Requests. - Express Validation: استخدام مكتبات مثل
joiأوexpress-validatorللتأكد من أن البيانات المدخلة مطابقة للشروط ومحمية من ثغرات الحقن (Injection) قبل معالجتها.

🛠️ الإضافات الهندسية الضرورية لضمان استقرار هذا النظام المليوني
للانتقال بهذا النظام من "تطبيق تجريبي" إلى "نظام عالمي مستقر لـ 100 مليون مستخدم"، يجب إدخال المعماريات التالية:
أ. إدخال الـ Caching عبر Redis (خط الدفاع الأول)
قاعدة البيانات لن تتحمل ملايين استعلامات القراءة والكتابة في الثانية. يجب استخدام Redis كطبقة ذاكرة مؤقتة لتخزين حالات الاتصال للمستخدمين (isOnline) وآخر 20 رسالة للمحادثات النشطة. القراءة تتم من الـ Cache، وتُكتب الرسائل لاحقاً في قاعدة البيانات بشكل غير متزامن.
ب. تقسيم قاعدة البيانات (Database Sharding & Indexing)
تخزين 100 مليون مستخدم ورسائلهم في جدول واحد بـ MongoDB سيؤدي لبطء شديد. الحل هو:
- عمل Sharding (تقسيم أفقي لقاعدة البيانات) بناءً على الـ
chatIdأوuserIdلتوزيع البيانات على عِدّة سيرفرات. - عمل Compound Indexing في الـ Mongoose على حقلي
chatIdوcreatedAtلضمان جلب الرسائل في أجزاء من الملي ثانية.
ج. استخدام موجه الرسائل (Message Broker مثل RabbitMQ / Kafka)
عندما يرسل مستخدم رسالة، يجب ألا ينتظر السيرفر حتى تُكتب الرسالة في قاعدة البيانات وتُرسل لـ FCM. السيرفر يستقبل الرسالة ويرميها فوراً في طابور (Queue) مثل RabbitMQ، ويتولى سيرفر آخر (Worker) معالجتها وإرسالها في الخلفية. هذا يضمن استجابة السيرفر الرئيسي في ميكرو-ثانية دون أي اختناق.
د. توزيع الأحمال (Load Balancing & Horizontal Scaling)
تطبيق Node.js يعمل على نواة واحدة (Single Thread). مع هذا الحجم، نحتاج لـ PM2 Cluster Mode لاستغلال كامل أنوية السيرفر، ووضع Nginx أو AWS ALB كـ Load Balancer لتوزيع اتصالات الـ WebSockets والطلبات على عشرات السيرفرات المتاحة في الخلفية.
الرجاء تسجيل الدخول لتتمكن من التعليق