Come abbiamo costruito la pipeline sotto i 30ms di tracio.ai
Dalla raccolta dei segnali all'ID visitatore in meno di 30ms: la nostra architettura basata su Go, ClickHouse, Redis e l'elaborazione distribuita.
Quando abbiamo deciso di costruire il motore di identificazione dei dispositivi di tracio.ai, avevamo un requisito non negoziabile: l'intera pipeline — dalla ricezione dei segnali cifrati alla restituzione di un ID visitatore — deve completarsi in meno di 30 millisecondi al 95° percentile. Questo articolo è una panoramica dettagliata dell'architettura che abbiamo costruito per raggiungere quell'obiettivo.
Panoramica della pipeline
La pipeline di identificazione ha cinque fasi: decifratura dei segnali, normalizzazione dei segnali, calcolo degli hash, risoluzione dell'identità e serializzazione della risposta. Ogni fase è ottimizzata in modo indipendente e le fasi che possono essere eseguite in parallelo lo sono. Il budget totale è di 30ms, ripartito grossomodo così: decifratura 2ms, normalizzazione 3ms, hashing 2ms, risoluzione dell'identità 20ms, serializzazione 1ms. I restanti 2ms sono di margine.
La decifratura dei segnali inverte il trasporto cifrato lato client. Usiamo i pacchetti crypto di Go con accelerazione hardware, che completano la decifratura di un payload tipico da 4KB in meno di 1ms. La normalizzazione analizza il JSON del segnale, valida i tipi e applica trasformazioni specifiche per piattaforma — per esempio, normalizza le stringhe user agent per rimuovere il rumore legato alle versioni.
Risoluzione distribuita dell'identità
La risoluzione dell'identità — determinare se questo dispositivo è già stato visto in precedenza — è la fase più sensibile alla latenza. Memorizziamo i profili dei dispositivi in Redis, distribuiti su un cluster tramite un livello di routing distribuito delle chiavi. Il routing distribuisce le chiavi in base al fingerprint del livello hardware, il che garantisce che le ricerche per lo stesso dispositivo colpiscano sempre lo stesso nodo Redis.
La nostra implementazione dello sharding utilizza nodi virtuali (150 per nodo fisico) per garantire una distribuzione uniforme. Quando un nodo viene aggiunto o rimosso, è necessario rimappare solo 1/N delle chiavi, dove N è il numero di nodi. Abbiamo implementato il livello di routing in Go con tempo di ricerca O(log n) e zero allocazioni.
Redis come archivio dell'identità
Abbiamo scelto Redis rispetto alle alternative (Memcached, ScyllaDB, DynamoDB) per i suoi tempi di risposta costantemente inferiori al millisecondo e il supporto a strutture dati complesse. Ogni profilo di dispositivo è memorizzato come un hash Redis con campi per l'hash di ciascun livello di segnale, l'ID visitatore, il timestamp dell'ultima visualizzazione e i metadati di confidenza.
La query di risoluzione dell'identità è una singola chiamata HGETALL seguita da un confronto tra gli hash dei segnali in arrivo e gli hash memorizzati. Se il livello hardware corrisponde, restituiamo l'ID visitatore esistente con alta confidenza. Se corrisponde solo il livello software, eseguiamo un confronto di somiglianza dei dati a livello di segnale per determinare se si tratta dello stesso dispositivo con un browser aggiornato. Se nulla corrisponde, generiamo un nuovo ID visitatore.
ClickHouse per l'archiviazione degli eventi
Ogni evento di identificazione viene scritto su ClickHouse in modo asincrono. Usiamo uno scrittore bufferizzato che raggruppa gli inserimenti in batch — raccogliendo eventi per 100ms o finché non si accumulano 1.000 eventi, a seconda di quale condizione si verifica prima. Questo batching è fondamentale perché ClickHouse rende al meglio con inserimenti di grandi dimensioni (migliaia di righe alla volta) anziché con inserimenti di righe singole.
Il nostro schema ClickHouse è ottimizzato per i due schemi di query più comuni: cercare tutti gli eventi per uno specifico ID visitatore e aggregare gli eventi su periodi di tempo. Usiamo un motore MergeTree con una chiave primaria di (visitor_id, timestamp), che offre ricerche puntuali rapide e scansioni per intervalli efficienti. Le viste materializzate mantengono metriche giornaliere e orarie pre-aggregate.
Raggiungere i sotto-30ms su larga scala
Tre decisioni architetturali sono state fondamentali per raggiungere il nostro obiettivo di latenza. Primo, la pipeline è completamente in streaming — iniziamo a elaborare i segnali prima che l'intero corpo della richiesta HTTP sia stato ricevuto. Secondo, le ricerche su Redis utilizzano il connection pooling con connessioni persistenti, eliminando l'overhead dell'handshake TCP. Terzo, le scritture su ClickHouse sono completamente asincrone e non bloccano mai il percorso di risposta.
Nei test di carico con 50K richieste al secondo, la nostra latenza p50 è di 12ms, p95 di 24ms e p99 di 38ms. Il p99 occasionalmente supera il nostro obiettivo di 30ms durante il ribilanciamento del cluster Redis, ma il p95 rimane costantemente sotto i 30ms. Per i clienti con requisiti di latenza più stringenti, offriamo cluster Redis dedicati che eliminano la contesa multi-tenant.