Comment nous avons conçu le pipeline sous les 30 ms de tracio.ai
De la collecte des signaux à l'identifiant visiteur en moins de 30 ms : notre architecture reposant sur Go, ClickHouse, Redis et le traitement distribué.
Lorsque nous avons entrepris de concevoir le moteur d'identification d'appareils de tracio.ai, nous avions une exigence non négociable : l'ensemble du pipeline — de la réception des signaux chiffrés au renvoi d'un identifiant visiteur — devait se terminer en moins de 30 millisecondes au 95e centile. Cet article est une présentation détaillée de l'architecture que nous avons construite pour atteindre cet objectif.
Vue d'ensemble du pipeline
Le pipeline d'identification comporte cinq étapes : déchiffrement des signaux, normalisation des signaux, calcul de hachage, résolution d'identité et sérialisation de la réponse. Chaque étape est optimisée indépendamment, et les étapes qui peuvent s'exécuter en parallèle le font. Le budget total est de 30 ms, réparti approximativement ainsi : déchiffrement 2 ms, normalisation 3 ms, hachage 2 ms, résolution d'identité 20 ms, sérialisation 1 ms. Les 2 ms restants servent de marge.
Le déchiffrement des signaux inverse le transport chiffré côté client. Nous utilisons les paquets de cryptographie de Go avec accélération matérielle, qui déchiffrent un payload typique de 4KB en moins de 1 ms. La normalisation analyse le JSON des signaux, valide les types et applique des transformations spécifiques à chaque plateforme — par exemple, la normalisation des chaînes de user agent pour supprimer le bruit propre à chaque version.
Résolution d'identité distribuée
La résolution d'identité — déterminer si cet appareil a déjà été vu — est l'étape la plus sensible à la latence. Nous stockons les profils d'appareils dans Redis, fragmentés sur un cluster grâce à une couche de routage de clés distribuée. Le routage répartit les clés selon l'empreinte du niveau matériel, ce qui garantit que les recherches pour un même appareil atteignent toujours le même nœud Redis.
Notre implémentation du sharding utilise des nœuds virtuels (150 par nœud physique) pour assurer une répartition uniforme. Lorsqu'un nœud est ajouté ou supprimé, seul 1/N des clés doit être remappé, où N est le nombre de nœuds. Nous avons implémenté la couche de routage en Go avec un temps de recherche en O(log n) et zéro allocation.
Redis comme magasin d'identités
Nous avons choisi Redis plutôt que ses alternatives (Memcached, ScyllaDB, DynamoDB) pour ses temps de réponse constamment inférieurs à la milliseconde et sa prise en charge de structures de données complexes. Chaque profil d'appareil est stocké sous forme de hachage Redis, avec des champs pour le hachage de chaque niveau de signal, l'identifiant visiteur, l'horodatage de la dernière visite et les métadonnées de confiance.
La requête de résolution d'identité est un unique appel HGETALL suivi d'une comparaison des hachages de signaux entrants avec les hachages stockés. Si le niveau matériel correspond, nous renvoyons l'identifiant visiteur existant avec une confiance élevée. Si seul le niveau logiciel correspond, nous effectuons une comparaison de similarité des données au niveau des signaux pour déterminer s'il s'agit du même appareil avec un navigateur mis à jour. Si rien ne correspond, nous générons un nouvel identifiant visiteur.
ClickHouse pour le stockage des événements
Chaque événement d'identification est écrit dans ClickHouse de façon asynchrone. Nous utilisons un écrivain à tampon qui regroupe les insertions par lots — en collectant les événements pendant 100 ms ou jusqu'à ce que 1,000 événements s'accumulent, selon ce qui survient en premier. Ce regroupement est essentiel, car ClickHouse est plus performant avec de grandes insertions (des milliers de lignes à la fois) qu'avec des insertions de lignes individuelles.
Notre schéma ClickHouse est optimisé pour les deux schémas de requête les plus courants : rechercher tous les événements d'un identifiant visiteur donné et agréger les événements sur des périodes de temps. Nous utilisons un moteur MergeTree avec une clé primaire (visitor_id, timestamp), qui offre des recherches ponctuelles rapides et des balayages de plage efficaces. Des vues matérialisées maintiennent des métriques quotidiennes et horaires préagrégées.
Atteindre moins de 30 ms à grande échelle
Trois décisions d'architecture ont été déterminantes pour atteindre notre objectif de latence. Premièrement, le pipeline est entièrement en streaming — nous commençons à traiter les signaux avant même d'avoir reçu tout le corps de la requête HTTP. Deuxièmement, les recherches Redis utilisent un pooling de connexions avec des connexions persistantes, éliminant la surcharge du handshake TCP. Troisièmement, les écritures dans ClickHouse sont entièrement asynchrones et ne bloquent jamais le chemin de réponse.
En tests de charge à 50K requêtes/seconde, notre latence p50 est de 12 ms, la p95 de 24 ms et la p99 de 38 ms. La p99 dépasse occasionnellement notre objectif de 30 ms pendant le rééquilibrage du cluster Redis, mais la p95 reste systématiquement sous les 30 ms. Pour les clients aux exigences de latence plus strictes, nous proposons des clusters Redis dédiés qui éliminent la contention multi-tenant.