رصد لقاءات السفن خلال مللي ثوانٍ باستخدام خلايا جيوهاش وNeo4j
مشكلة فحص كل زوج من السفن
يبدو رصد التقاء السفن مسألة هندسية بسيطة إلى أن تنظر إلى الحجم الفعلي. محرك رصد الحالات الشاذة البحرية الذي بنيناه لتتبع هذا يستقبل تقارير موقع AIS من آلاف السفن، تصل باستمرار عبر Kafka. فحص كل زوج من السفن مقابل كل زوج آخر عملية تربيعية، وعند هذا الحجم لا تعمل قريبا من نطاق المللي ثانية. لا الاستعلام المباشر ولا المخطط المباشر يتوسعان مع الحجم.
خلايا جيوهاش (geohash) بدلا من الإحداثيات الخام
بدلا من مقارنة خطي الطول والعرض الخام، نضع كل تقرير موقع في خلية جيوهاش بدقة المستوى 6، ما يعطي خلايا يبلغ عرضها نحو 1.2 كيلومتر. لا يمكن لسفينتين أن تقتربا بما يكفي ليُحسب لقاء بينهما إلا إذا شاركتا الخلية نفسها أو وقعتا في خلايا متجاورة. تنكمش مساحة البحث من مقارنة كل سفينة بكل سفينة إلى مقارنة كل سفينة بحفنة من السفن في الخلايا القريبة. هذا قرار تصميم يتعلق بدقة التفصيل التي تحتاجها مهمة الرصد هذه، لا ادعاء بشأن دقة نظام تحديد المواقع، وهو التغيير الوحيد الذي جعل المشكلة قابلة للحل عند حجم الاستيعاب لدينا.
لماذا رسم بياني، لا استعلام مكاني في SQL فقط
نخزّن السفن وأحداث تقاربها كرسم بياني في Neo4j بدلا من صفوف في جدول علائقي. اللقاء ليس اقتراب سفينتين مرة واحدة، بل علاقة تستمر أو تتكرر أو تتصاعد مع الوقت، وهذا النوع من العلاقات يعيش بطبيعته في رسم بياني. عندما تشترك سفينتان في خلية جيوهاش مدة كافية لتُحسب لقاء، نكتب حافة ENCOUNTERED محسوبة مسبقا بين عقدتيهما :Vessel، مفهرسة بمعرّفي السفينتين والطابع الزمني معا. الأسئلة اللاحقة، مثل هل التقى هذان الزوجان من قبل وكم مرة وقرب أي ميناء، تُنفَّذ كاجتياز Cypher على عقد مفهرسة بدلا من ضم مقابل جدول سجل تاريخي متنام.
الحفاظ على دقة الدفق
- الضغط العكسي: يعالج مستهلك Kafka رسائل AIS بالمعدل الذي يستطيع كاتبا جيوهاش والرسم البياني مواكبته فعليا، بدلا من تخزين رسائل غير محدودة في الذاكرة عندما تتباطأ كتابة تالية.
- الإسقاط المتكرر الآمن: تعيد تغذيات AIS إرسال الرسائل وتكررها، لذا تُكتب كل تحديثة موقع بمفتاح معرّف السفينة وطابع وقت الرسالة، وتُحدّث النسخة المكررة الصف نفسه بدلا من إنشاء صف جديد.
- صندوق الصادر: عند رصد لقاء، يُكتب الحدث الذي يشترك فيه المستهلكون التاليون في جدول صندوق صادر ضمن المعاملة نفسها التي تكتب حافة الرسم البياني، ثم يُنشر بشكل غير متزامن، فلا يرى أي مستهلك حدثا لحافة أزالها تراجع لاحق.
ما يمنحنا إياه Spring WebFlux هنا
يعمل مسار الاستيعاب على Spring WebFlux فوق Java 21، لأن الحمل يهيمن عليه الانتظار على الإدخال والإخراج، مستهلك Kafka وكتابات Neo4j، لا الحساب المرتكز على المعالج. يتيح خط المعالجة التفاعلي لعدد صغير من مسارات التنفيذ التعامل مع عدد كبير من تدفقات المواقع المتزامنة دون أن تحجب كتابة بطيئة الرسالة التالية في الطابور. هذا أهم من اختيار الإطار بمعزل عن غيره، لأنه ما يجعل معالجة الضغط العكسي أعلاه تصمد فعليا تحت الحمل لا على مخطط فقط.
ما كنا سنقوله لفريق يبني شيئا مشابها
اختر دقة التفصيل المكاني كقرار تصميم واعٍ قبل لمس المخطط، ولا تلجأ افتراضيا إلى الإحداثيات الخام لأنها تبدو أدق. نمذج العلاقات التي تستمر أو تتكرر كرسم بياني، لا كصفوف في جدول تظل تضمه إلى نفسه. وعامل تكرار الرسائل والضغط العكسي كأمرين مؤكدين في أي خط معالجة للدفق، لا كحالات استثنائية، لأنهما يظهران عند أحجام الاستيعاب الفعلية في الأسبوع الأول لا في السنة الأولى.