تصميم نظام مشاركة الركوب
تصميم نظام مشاركة الركوب
تعالج منصات مثل Uber وLyft وDiDi مئات الآلاف من طلبات الرحلات في الدقيقة، وتطابق الراكبين مع السائقين في أقل من ثانيتين، وتتابع ملايين المركبات المتحركة في الوقت الفعلي. يرتكز كل نظام مشاركة ركوب على مشكلتين بالغتي الصعوبة: الفهرسة الجغرافية المكانية (أين جميع السائقين الآن؟) والمطابقة الفورية (أي سائق يُعيَّن لهذا الراكب؟). يشرح هذا الدرس كلتيهما.
المتطلبات والحجم
تقدير مبدئي قبل البدء بالتصميم:
- الراكبون النشطون يومياً: 10 ملايين
- السائقون النشطون يومياً: مليون — يرسلون كلهم نبضات GPS كل 4 ثوانٍ
- طلبات الرحلات في ذروة الاستخدام: ~100,000 في الدقيقة (~1700 طلب/ثانية)
- عمليات كتابة مواقع السائقين: 1,000,000 ÷ 4 = 250,000 كتابة/ثانية (الرقم الأصعب)
- اتفاقية مستوى خدمة المطابقة: أقل من ثانيتين من الطلب إلى التعيين
- دقة الموقع: دقة 10–50 متراً كافية للمطابقة؛ الملاحة المفصلة تُعالج بخدمة خرائط منفصلة
الفهرسة الجغرافية المكانية — المشكلة الجوهرية
عندما يطلب راكب رحلة من الإحداثيات (lat, lng)، يجب أن يجيب النظام على: ابحث عن جميع السائقين في نطاق 5 كم مرتبين حسب وقت الوصول. فحص كل سجل سائق في قاعدة البيانات هو عملية O(n) على مليون صف — بطيء جداً. تحتاج إلى فهرس مكاني.
النهج الأول — Geohash
يُرمِّز Geohash زوجاً (lat, lng) إلى سلسلة أحرف وأرقام قصيرة. يعتمد الترميز على منحنى Z-order (Morton) الذي يتشابك بتات خط العرض وخط الطول. الخاصية الجوهرية: كلما طال البادئة المشتركة، كلما كانت المواقع أقرب جغرافياً.
- طول Geohash 6 ← خلية ~1.2 كم × 0.6 كم
- طول Geohash 7 ← خلية ~153 م × 153 م
- طول Geohash 8 ← خلية ~38 م × 19 م
سائق في وسط مدينة كبرى يُخزَّن geohash موقعه في Redis كمفتاح في hashmap: HSET drivers:geo <driver_id> "9q8yy4n". يجلب استعلام القرب الخلايا التسع المجاورة (الخلية ذاتها بالإضافة إلى الثماني المحيطة بها) ويفحص تلك المجموعة فقط — عادةً بضع مئات من الإدخالات لا مليون.
حالة حافة: خلايا Geohash مستطيلة الشكل وقد تكون جاراتها غير متواصلة بالقرب من خط التاريخ الدولي والأقطاب. الحل الصحيح هو دائماً الاستعلام عن الخلية بالإضافة إلى جاراتها الثماني.
النهج الثاني — Quadtree
يقسِّم Quadtree الخريطة تكرارياً إلى أربعة أرباع. تحتفظ كل عقدة طرفية بقائمة السائقين في تلك الخلية. الخلايا ذات الكثافة العالية تنقسم أكثر؛ الخلايا الخفيفة تبقى كبيرة. هذا يمنح فهرساً تكيفياً — كثافة العقد تعكس كثافة السائقين.
النهج الثالث — Redis GEO
توفر Redis واجهة GEO برية (أوامر GEOADD وGEOSEARCH) مدعومة بـ sorted set تكون فيه النتيجة عدداً صحيحاً من 52 بت يمثل geohash. هذا هو الخيار العملي الحديث:
GEOADD drivers 37.7749 -122.4194 "driver:42"— تحديث الموقع في O(log n)GEOSEARCH drivers FROMMEMBER "rider:9" BYRADIUS 5 km ASC COUNT 10— أقرب 10 سائقين في نطاق 5 كم- يوزع Redis Cluster الـ sorted set أفقياً عند الحاجة
البنية المعمارية الشاملة
ينقسم نظام مشاركة الركوب إلى خدمات مستقلة:
- Location Service — يستوعب 250,000 نبضة GPS/ثانية من تطبيقات السائقين، يكتب إلى Redis GEO (أساسي) ومخزن السلاسل الزمنية (للتحليلات).
- Matching Service — عمال عديمو الحالة يستعلمون Redis عن السائقين القريبين عند طلب رحلة، يرتبونهم حسب وقت الوصول، ويعيِّنون الأفضل.
- Trip Service — يمتلك آلة حالة دورة الرحلة (REQUESTED → ACCEPTED → EN_ROUTE → ARRIVED → IN_TRIP → COMPLETED) ويستمر في قاعدة بيانات علائقية (Postgres/MySQL مجزأة حسب المدينة).
- ETA Service — يُغلِّف محرك التوجيه لحساب وقت القيادة بين نقطتين.
- Notification Service — يدفع التحديثات الفورية إلى تطبيقات الراكب والسائق عبر WebSocket / APNS / FCM.
- Payment Service — يتولى حساب الأجرة والخصم عند اكتمال الرحلة.
خوارزمية المطابقة بين الراكب والسائق
المطابقة ليست مجرد "أقرب سائق". دالة الترتيب الحقيقية توازن عدة إشارات:
- وقت الوصول إلى نقطة الالتقاء (ETA) — الإشارة الأساسية. سائق على بُعد 1.2 كم لكنه عالق في الازدحام أسوأ من آخر على بُعد 2 كم في طريق سيار.
- تقييم السائق — خصم طفيف للسائقين ذوي التقييم المنخفض لحماية تجربة الراكب.
- محاذاة الاتجاه — سائق متجه نحو المنطقة العامة للراكب يتكبد تكلفة تنقل خامل أقل.
- عدالة الأسعار الزائدة — توزيع رحلات الضريبة العالية على السائقين الأطول انتظاراً لتفادي المجاعة.
تسير حلقة المطابقة كالتالي:
- يطلب الراكب رحلة من الإحداثيات
(lat, lng). - يستعلم Matching Service من Redis:
GEOSEARCH drivers BYRADIUS 5 km ASC COUNT 30→ يُعيد 30 مرشحاً. - استدعاء ETA Service دفعياً لجميع الـ 30 مرشحاً بالتوازي (fan-out).
- تسجيل نقاط كل مرشح. تحديد المرشح الأفضل.
- إرسال العرض للسائق عبر Notification Service. لدى السائق 10 ثوانٍ للقبول.
- إذا رفض أو انتهت المهلة، يُختار المرشح التالي وتُكرَّر العملية.
التعامل مع إنتاجية كتابة الموقع
250,000 عملية GEOADD في الثانية تتجاوز طاقة عقدة Redis واحدة (تبلغ ذروتها ~100,000–200,000 عملية/ثانية في ظروف حقيقية). الحلول:
- Redis Cluster مع التجزئة الجغرافية — تقسيم keyspace حسب المدينة أو المنطقة. كل شارد يتولى سائقي مدينة واحدة، مما يخفض الحمل على كل عقدة بشكل كبير.
- التجميع من جهة العميل — يرسل تطبيق السائق نبضة كل 4 ثوانٍ؛ يجمع Location Service نبضات من سائقين متعددين ويصدر أوامر
GEOADDفي pipelines بحجم 100–500 أمر، مما يُقلل تكلفة الرحلات الشبكية. - مخزن مؤقت للكتابة — وضع مخزن صغير في الذاكرة (موضوع Kafka أو ring buffer) أمام Redis لاستيعاب التدفقات المفاجئة دون فقدان التحديثات.
الاتساق ومعالجة الأعطال
بيانات الموقع قديمة بطبيعتها — بحلول وقت اتخاذ قرار المطابقة ربما تحرك السائق. يقبل النظام الاتساق اللحظي في الموقع. ما يجب أن يكون متسقاً تماماً هو تعيين الرحلة (لا يمكن تعيين نفس السائق لراكبين). يستخدم Trip Service قفلاً متفائلاً (تحديث CAS على عمود حالة السائق) أو قفلاً موزعاً (Redis SETNX على driver:<id>:lock) لمنع التعيين المزدوج.
إذا انهار Location Service، تنتهي صلاحية إدخالات السائقين في Redis عبر TTL (10 ثوانٍ). الإدخالات المنتهية تُستبعد تلقائياً من نتائج GEOSEARCH، لذا لن يُطابَق راكب أبداً مع سائق انقطع تطبيقه.
ملخص: قرارات التصميم الرئيسية
- استخدم Redis GEO (أو geohash + Redis) لجميع قراءات وكتابات مواقع السائقين — إنه الخيار العملي الوحيد عند 250,000 كتابة/ثانية.
- خدمة المطابقة عديمة الحالة — تُوسَّع أفقياً بإضافة عمال خلف موزع الأحمال.
- استخدم استدعاءات ETA الدفعية المتوازية في حلقة المطابقة للوفاء بـ SLA الثانيتين.
- جزِّئ Redis حسب المدينة/المنطقة للإبقاء على معدل الكتابة لكل عقدة في نطاق مُدار.
- استخدم TTL على مفاتيح موقع السائقين لاستبعاد السائقين المنقطعين تلقائياً من مجموعة المتاحين.
- احمِ تعيين السائق بـ قفل موزع أو تحكم التزامن المتفائل — الموقع يمكن أن يكون متسقاً في نهاية المطاف، أما التعيين فلا.