مشروع: تصميم طبقة البيانات
مشروع: تصميم طبقة البيانات
كل ما تناولناه في هذا البرنامج التعليمي — SQL مقابل NoSQL، وضمانات ACID، واستراتيجيات الفهرسة، وطوبولوجيا النسخ المتماثلة، ومناهج التقسيم، ومقايضات إلغاء التطبيع، وتخزين الكتل الثنائية، وأطر اختيار قواعد البيانات — كلها تلتقي في مهارة عملية واحدة: تصميم طبقة بيانات لنظام حقيقي من الصفر. يشرح هذا الدرس الختامي هذه العملية بالكامل لنظام ملموس غير مبسّط، مع توضيح المنطق وراء كل قرار.
النظام: منصة استدعاء السيارات
سنصمم طبقة البيانات لخدمة مشابهة لـUber أو Lyft. تتكون من خمسة أنظمة فرعية رئيسية، لكل منها خصائص بيانات مميزة:
- حسابات الركاب والسائقين — التسجيل والملفات الشخصية والمصادقة والتحقق من المستندات
- دورة حياة الرحلة — الحجز والمطابقة والتوجيه وحساب الأجرة والإتمام والسجل
- الموقع الآني — إحداثيات GPS للسائقين تُحدَّث كل 3 إلى 5 ثوانٍ لملايين السائقين النشطين
- المدفوعات والفواتير — الرسوم والاسترداد وعائدات السائقين وسجل الفواتير
- التحليلات والتقارير — لوحات أعمال، وكشف احتيال، وخرائط حرارية للعرض والطلب
أهداف التوسع: 10 ملايين راكب نشط يومياً، 2 مليون سائق نشط، 5 ملايين رحلة يومياً، ذروة 50,000 رحلة نشطة متزامنة.
الخطوة 1 — تحديد أنماط الوصول قبل اختيار التخزين
الخطوة الأهم على الإطلاق هي حصر أنماط القراءة والكتابة السائدة لكل نظام فرعي قبل النطق باسم قاعدة بيانات واحدة. تقنية التخزين يجب أن تتبع أنماط الوصول، لا العكس.
- الحسابات: قراءة بمعرّف المستخدم مع كل طلب مصادَق؛ كتابة عند التسجيل أو التحديث. اتساق قوي. كتابات قليلة وقراءات كثيرة.
- الرحلات: تُنشأ عند الحجز؛ تُحدَّث مرات عديدة أثناء الرحلة (مطابقة، بدء، إنهاء، تقييم)؛ تُستعلَم بمعرّف الراكب/السائق للسجل. آلة حالة متعددة الخطوات — الذرية أمر حيوي.
- المواقع: كثيفة الكتابة — مليونا سائق × تحديث كل 4 ثوانٍ = ~500,000 كتابة في الثانية في الذروة. الاستعلامات مكانية: "أجد جميع السائقين في نطاق 2 كم من (lat, lng)". البيانات مؤقتة — نهتم فقط بالموقع الحالي لا بالتاريخ.
- المدفوعات: دفتر حسابات بقيد مزدوج. كل رسوم يجب أن يقابلها مدين؛ الأخطاء الجزئية كارثية. تتطلب أقوى ضمانات الاتساق.
- التحليلات: تجميعات ضخمة على مجموعات بيانات تاريخية كبيرة. تحمّل التأخير مرتفع (ثوانٍ إلى دقائق). أنماط الاستعلام استكشافية وغير متوقعة.
الخطوة 2 — تخصيص كل نظام فرعي لتقنية التخزين المناسبة
بعد وضوح أنماط الوصول، نطابق كل نظام فرعي مع التقنية التي يحتاجها فعلاً:
- الحسابات → PostgreSQL (أساسي + نسخ للقراءة). بيانات منظمة، مخطط مستقر، استعلامات معقدة (JOIN users → documents → preferences)، اتساق قوي لتدفقات المصادقة. يتحمل أساسي واحد كتابات الحسابات بهذا الحجم. أضف 2-3 نسخ للقراءة لتوزيع عمليات البحث عن الملفات الشخصية.
- الرحلات → PostgreSQL (نفس المجموعة أو مجموعة مخصصة للعزل). الرحلات علائقية (ركاب وسائقون وطرق وأجور وتقييمات كلها مترابطة)، وتتطلب تحديثات ذرية متعددة الصفوف (انتقالات آلة الحالة)، وتستفيد من استعلامات SQL الغنية لحل النزاعات والدعم. قسّم بـ
city_idأوrider_idإن تجاوز حجم الكتابة الخادم الأساسي — عند 5 ملايين رحلة يومياً هذا ~58 كتابة في الثانية، وهو ضمن سعة أساسي PostgreSQL واحد بيسر. - المواقع الآنية → Redis (وضع المجموعة، أوامر Geo). توفر
GEOADD/GEORADIUSفي Redis استعلامات جغرافية بزمن استجابة دون الميلي ثانية. في وضع المجموعة عبر 6 عقد (3 أساسية، 3 نسخ)، تتعامل Redis مع أكثر من مليون كتابة في الثانية. تُخزَّن المواقع بـTTL مدته ~30 ثانية — إن توقف السائق عن إرسال التحديثات، يُحذف سجله تلقائياً. هذه ذاكرة تخزين مؤقتة عابرة بالكامل؛ لا حاجة للديمومة. - المدفوعات → PostgreSQL بعزل مباشر (Serializable). دفتر حسابات بقيد مزدوج. كل صف في جدول
ledgerيمثل طرفاً واحداً من المعاملة. كل دفعة تُدرج صفين ذرياً (مدين الراكب، دائن أمانة السائق). عزل Serializable يمنع القراءات الوهمية. فكر في مجموعة PostgreSQL مخصصة معزولة عن قاعدة بيانات الرحلات حتى لا يُجوّع استعلام تحليلي ثقيل كتابات الدفع. - التحليلات → Amazon S3 + Apache Parquet + محرك استعلام (Athena / BigQuery / Redshift). تُبثّ سجلات الرحلات والمدفوعات إلى S3 بصيغة Parquet عبر مسار التقاط تغييرات البيانات (CDC). يستعلم المحللون بـAthena أو BigQuery — دفع حسب الاستعلام، لا مستودع لإدارته. لإضافة لوحات الوقت الفعلي (إشارات الاحتيال، خرائط حرارية للعرض) أضف Apache Kafka → Apache Flink → مخزن سلاسل زمنية (InfluxDB أو TimescaleDB).
- مستندات السائق وصور الملفات الشخصية → تخزين الكائنات (S3 / GCS). كتل ثنائية غير قابلة للتغيير؛ تُصَل إليها نادراً بعد الرفع؛ تُوزَّع بـCDN للتنزيل العالمي السريع. خزّن فقط مفتاح S3 في PostgreSQL، لا تضع الملف الثنائي في قاعدة البيانات أبداً.
الخطوة 3 — نمذجة المخططات الأساسية
بعد تحديد تقنيات التخزين، نصمم أكثر مخططين علائقيين حيوية: الرحلات والمدفوعات.
مخطط الرحلات (PostgreSQL):
مخطط المدفوعات (PostgreSQL، مجموعة منفصلة):
SELECT * FROM trips WHERE status = 'active' يعمل باستمرار (محرك المطابقة، حسابات وقت الوصول). في أي لحظة لا يزيد عدد الرحلات النشطة عن ~50,000 من أصل 5 ملايين رحلة يومية. فهرس جزئي — WHERE status NOT IN ('completed', 'cancelled') — يغطي هذه الصفوف فقط. يبقى الفهرس صغيراً (يُخزَّن كاملاً في ذاكرة L2 لخادم قاعدة البيانات)، مما يجعل استعلامات تعيين السائقين شبه فورية بغض النظر عن الحجم الإجمالي للرحلات.
الخطوة 4 — تصميم استراتيجية النسخ المتماثل والتقسيم
عند 5 ملايين رحلة يومياً (~58 كتابة في الثانية)، يتحمل أساسي PostgreSQL واحد الرحلات بيسر. لكننا نخطط مسبقاً:
- النسخ أولاً: انشر نسختين متدفقتين متزامنتين لمجموعتي الرحلات والحسابات. تستوعب النسخ جميع حركة القراءة (استعلامات السجل، بحث الدعم، تصدير التحليلات). يضمن التجاوز التلقائي مع Patroni استبدال الأساسي في أقل من 30 ثانية في حال فشله.
- متى التقسيم: إن تجاوز معدل الكتابة على جدول الرحلات 5,000 كتابة/ثانية بشكل منتظم، قسّم بـ
city_id. كل مجموعة مدينة أساسي مستقل + نسختان. رحلات لاغوس لا تلمس أبداً عقد قاعدة بيانات رحلات لندن. الاستعلامات عبر المدن (لوحات عالمية) تستخدم مسار التحليلات، لا مجموعات OLTP. - مجموعة Redis الجغرافية: مقسّمة أفقياً بالفعل تلقائياً. استخدم 6 عقد (3 شرائح أساسية، 3 نسخ). يُوزَّع مفاتيح المواقع بـhash معرّف السائق عبر الشرائح الثلاثة الأساسية تلقائياً بواسطة المجموعة.
- تخزين الكائنات (S3): موزَّع بطبيعته؛ لا تقسيم لازم. فعّل الإصدارات لسجل مراجعة المستندات؛ قواعد دورة الحياة تنقل الملفات الأقدم من 90 يوماً إلى S3 Glacier Instant Retrieval لتخفيض تكاليف التخزين بنسبة ~70%.
الخطوة 5 — تطبيق إلغاء التطبيع باستراتيجية
مخططاتنا موحَّدة بالكامل — جيد للصحة، لكن بعض مسارات القراءة تستفيد من إلغاء تطبيع انتقائي:
- تخزين fare_cents في جدول الرحلات (موجود أعلاه). حساب الأجرة من بيانات المسار عند كل قراءة لسجل الرحلات سيتطلب JOIN بجدول fare_rules وإعادة الحساب. تخزينه مرة واحدة عند إتمام الرحلة يجعل شاشة سجل الراكب جلب صف واحد بسيطاً.
- عداد رحلات الراكب في جدول المستخدمين: عمود
total_trips INTيُزاد عند إتمام الرحلة، يتجنب تجميعCOUNT(*)على جدول الرحلات عند كل تحميل للملف الشخصي. اقبل الاتساق النهائي — إن فشل الزيادة، تصحيح ليلي يصلحه. - لا تُلغِ تطبيع اسم السائق في جدول الرحلات. الملفات الشخصية للسائقين تتغير (تصحيحات أسماء قانونية). الدمج عند القراءة يبقي سجل الرحلات متسقاً مع الملف الشخصي الحالي. الدمج رخيص: صف واحد بالمفتاح الأساسي.
الخطوة 6 — فصل مسار التحليلات
مبدأ حيوي: لا تُشغِّل تحليلات ثقيلة على قاعدة بيانات OLTP الخاصة بك أبداً. استعلام لوحة تحكم مدته 10 دقائق يمسح 100 مليون صف رحلات سيُجوّع محرك المطابقة من إدخال/إخراج القرص، مسبباً انتهاء مهل تعيين الرحلات للمستخدمين الفعليين.
البنية الصحيحة تفصل المستويين تماماً:
- التقاط تغييرات البيانات (CDC): يقرأ Debezium سجل WAL في PostgreSQL وينشر كل تغيير على صف إلى مواضيع Kafka (
trips.changes،payments.changes) خلال ثوانٍ، دون أي حمل على الأساسي. - المسار الدُّفعي: مستهلك Kafka (Spark Structured Streaming أو Kafka Connect S3 Sink البسيط) يكتب ملفات Parquet إلى S3 مُقسَّمة بـ
city/date/hour. Amazon Athena أو BigQuery تستطيع الاستعلام على بيتابايتات من هذه البيانات بـ5 دولارات لكل تيرابايت مُفحوص. - المسار الآني: يستهلك Apache Flink تيار Kafka ويُبقي على تجميعات متحركة (رحلات في الدقيقة، إيراد في الساعة، عدد السائقين النشطين بالمدينة) في TimescaleDB. تستعلم لوحات التحكم على TimescaleDB بزمن استجابة دون الثانية.
الخطوة 7 — التحقق من التصميم مقابل المتطلبات
أغلق الدائرة دائماً بالتحقق من التصميم مقابل أهداف التوسع الأصلية:
- 50,000 رحلة نشطة متزامنة: الفهرس الجزئي على
trips(status)يغطي ~50k صف فقط. يكتمل استعلام محرك المطابقة الماسح للرحلات النشطة في ميكروثوانٍ. - 500,000 كتابة موقع في الثانية: تتعامل Redis Cluster مع 3 شرائح أساسية مع هذا بيسر (~167k كتابة/شريحة). أظهرت القياسات أن مجموعة 6 عقد تُدير 800k عملية/ثانية في بيئة الإنتاج في Uber.
- صحة المدفوعات: معاملات PostgreSQL القابلة للتسلسل (Serializable) تمنع الرسوم المضاعفة. نموذج الدفتر يجعل أي اختلال قابلاً للكشف بـاستعلام مطابقة
SUM(amount_cents) GROUP BY account_id. - عزل التحليلات: CDC → Kafka → S3 يعني أن لا استعلام تحليلي يلمس الخوادم الأساسية لـOLTP أبداً.
- مرونة الأعطال: كل طبقة لها نسخة. PostgreSQL يتجاوز الأعطال في أقل من 30 ثانية. Redis Cluster يُرقّي نسخة تلقائياً. S3 لديه متانة 11 تسعة. لا نقطة فشل فردية.
الخلاصة: عملية التصميم
- حصر أنماط الوصول لكل نظام فرعي قبل تسمية أي تقنية.
- مطابقة التخزين مع النمط: علائقي → ACID/دمج، قيمة مفتاحية/جغرافي → سرعة، كائنات ثنائية → ملفات غير متغيرة، أعمدة/سلاسل زمنية → تحليلات.
- نمذجة المخططات مع الصحة أولاً؛ أضف إلغاء التطبيع فقط حيث يتطلبه مسار قراءة محدد مع وجود عقد تحديث واضحة.
- خطط للنسخ المتماثل (نسخ متزامنة للتوافر العالي) قبل التقسيم؛ قسّم فقط حين يتجاوز معدل الكتابة الحد المُقاس لأساسي واحد.
- عزل التحليلات عن OLTP عبر CDC/Kafka. لا تُشغّل تجميعات ثقيلة على أساسيك الإنتاجي أبداً.
- تحقق من كل قرار تصميمي مقابل أهداف التوسع الأصلية. تتبّع القرار حتى المتطلب هو ما يجعل التصميم قابلاً للدفاع عنه في مقابلة أو مراجعة معمارية.