Détecter les rendez-vous de navires en millisecondes avec des cellules geohash et Neo4j
Le problème de comparer chaque paire de navires
La détection de rendez-vous entre navires ressemble à un simple problème de géométrie jusqu'à ce qu'on en regarde l'échelle. Un moteur de détection d'anomalies maritimes que nous avons construit pour ce suivi ingère des rapports de position AIS de milliers de navires, arrivant en continu via Kafka. Comparer chaque paire de navires à toutes les autres paires est une opération quadratique, et à ce volume, elle ne tourne nulle part près de la milliseconde. Ni la requête naïve ni le schéma naïf ne passent à l'échelle.
Des cellules geohash plutôt que des coordonnées brutes
Plutôt que de comparer latitude et longitude brutes, nous regroupons chaque rapport de position dans une cellule geohash de précision 6, ce qui donne des cellules d'environ 1,2 kilomètre de large. Deux navires ne peuvent être assez proches pour compter comme un rendez-vous que s'ils partagent une cellule ou occupent des cellules adjacentes. L'espace de recherche passe ainsi de chaque navire contre tous les autres à chaque navire contre la poignée de navires des cellules voisines. C'est une décision de conception sur la granularité dont cette tâche de détection a besoin, pas une affirmation sur la précision du GPS, et c'est le seul changement qui a rendu le problème traitable à notre volume d'ingestion.
Pourquoi un graphe, pas seulement du SQL spatial
Nous stockons les navires et leurs événements de proximité comme un graphe dans Neo4j plutôt que comme des lignes dans une table relationnelle. Un rendez-vous n'est pas deux navires proches l'un de l'autre à un instant donné. C'est une relation qui persiste, se répète ou s'intensifie dans le temps, et ce type de relation vit naturellement dans un graphe. Quand deux navires partagent une cellule geohash assez longtemps pour compter comme un rendez-vous, nous écrivons une arête ENCOUNTERED précalculée entre leurs nœuds :Vessel, indexée sur les deux identifiants de navire et l'horodatage. Les questions posées plus tard, ces deux navires se sont-ils déjà croisés, à quelle fréquence, près de quel port, s'exécutent comme un parcours Cypher sur des nœuds indexés plutôt que comme une jointure sur une table d'historique qui ne cesse de croître.
Garder le flux fiable
- Backpressure : le consommateur Kafka traite les messages AIS au rythme que les écritures geohash et graphe peuvent réellement suivre, au lieu de mettre en mémoire tampon un nombre illimité de messages quand une écriture en aval ralentit.
- Projection idempotente : les flux AIS retransmettent et dupliquent des messages, donc chaque mise à jour de position est écrite avec pour clé l'identifiant du navire et l'horodatage du message, et un doublon met à jour la même ligne au lieu d'en créer une nouvelle.
- Outbox : quand un rendez-vous est détecté, l'événement auquel s'abonnent les consommateurs en aval est écrit dans une table outbox au sein de la même transaction que l'arête du graphe, puis publié de façon asynchrone, si bien qu'un consommateur ne voit jamais un événement pour une arête qu'un rollback a ensuite supprimée.
Ce que Spring WebFlux nous apporte ici
Le chemin d'ingestion tourne sur Spring WebFlux avec Java 21, car la charge est dominée par l'attente d'entrées-sorties, le consommateur Kafka, les écritures Neo4j, pas par du calcul limité par le CPU. Un pipeline réactif permet à un petit nombre de threads de gérer un grand nombre de flux de position simultanés sans qu'une écriture lente ne bloque le message suivant dans la file. Cela compte plus que le choix du framework pris isolément, car c'est ce qui fait que la gestion du backpressure décrite plus haut tient réellement sous charge, pas seulement sur un schéma.
Ce que nous dirions à une équipe qui construit quelque chose de similaire
Choisissez votre granularité spatiale comme une décision de conception délibérée avant de toucher au schéma, ne partez pas par défaut sur des coordonnées brutes parce qu'elles semblent plus précises. Modélisez les relations qui persistent ou se répètent comme un graphe, pas comme des lignes d'une table que vous joignez sans cesse à elle-même. Et considérez la duplication des messages et le backpressure comme des certitudes dans tout pipeline de streaming, pas comme des cas limites, car aux volumes d'ingestion réels, ils apparaissent dès la première semaine, pas la première année.