Cum am construit pipeline-ul sub-30ms al tracio.ai
De la colectarea semnalelor la ID-ul vizitatorului în sub 30ms: arhitectura noastră cu Go, ClickHouse, Redis și procesare distribuită.
Când ne-am propus să construim motorul de identificare a dispozitivelor al tracio.ai, aveam o cerință care nu era negociabilă: întregul pipeline — de la primirea semnalelor criptate până la returnarea unui ID de vizitator — trebuie să se încheie în sub 30 de milisecunde la percentila 95. Acest articol este o prezentare detaliată a arhitecturii pe care am construit-o pentru a atinge această țintă.
Prezentare generală a pipeline-ului
Pipeline-ul de identificare are cinci etape: decriptarea semnalelor, normalizarea semnalelor, calculul hash-urilor, rezolvarea identității și serializarea răspunsului. Fiecare etapă este optimizată independent, iar etapele care pot rula în paralel o fac. Bugetul total este de 30ms, alocat aproximativ astfel: decriptare 2ms, normalizare 3ms, hashing 2ms, rezolvarea identității 20ms, serializare 1ms. Cele 2ms rămase sunt rezervă.
Decriptarea semnalelor inversează transportul criptat pe partea de client. Folosim pachetele crypto ale Go cu accelerare hardware, care finalizează decriptarea unui payload tipic de 4KB în sub 1ms. Normalizarea parsează JSON-ul semnalului, validează tipurile și aplică transformări specifice platformei — de exemplu, normalizarea șirurilor user agent pentru a elimina zgomotul specific versiunilor.
Rezolvarea distribuită a identității
Rezolvarea identității — determinarea faptului că acest dispozitiv a mai fost văzut înainte — este etapa cea mai sensibilă la latență. Stocăm profilurile dispozitivelor în Redis, distribuite pe un cluster printr-un strat de rutare distribuită a cheilor. Rutarea distribuie cheile pe baza fingerprint-ului de nivel hardware, ceea ce asigură că interogările pentru același dispozitiv ajung întotdeauna la același nod Redis.
Implementarea noastră de sharding folosește noduri virtuale (150 per nod fizic) pentru a asigura o distribuție uniformă. Când un nod este adăugat sau eliminat, doar 1/N din chei trebuie remapate, unde N este numărul de noduri. Am implementat stratul de rutare în Go, cu timp de căutare O(log n) și zero alocări.
Redis ca depozit de identitate
Am ales Redis în locul alternativelor (Memcached, ScyllaDB, DynamoDB) datorită timpilor săi de răspuns constant sub o milisecundă și suportului pentru structuri de date complexe. Fiecare profil de dispozitiv este stocat ca un hash Redis cu câmpuri pentru hash-ul fiecărui nivel de semnal, ID-ul vizitatorului, marcajul temporal al ultimei vizualizări și metadatele de încredere.
Interogarea de rezolvare a identității este un singur apel HGETALL urmat de o comparație a hash-urilor semnalelor primite cu hash-urile stocate. Dacă nivelul hardware corespunde, returnăm ID-ul de vizitator existent cu încredere ridicată. Dacă doar nivelul software corespunde, efectuăm o comparație de similaritate a datelor la nivel de semnal pentru a determina dacă este vorba de același dispozitiv cu un browser actualizat. Dacă nimic nu corespunde, generăm un nou ID de vizitator.
ClickHouse pentru stocarea evenimentelor
Fiecare eveniment de identificare este scris în ClickHouse asincron. Folosim un writer cu buffer care grupează inserările — colectând evenimente timp de 100ms sau până când se acumulează 1.000 de evenimente, oricare survine mai întâi. Această grupare este esențială, deoarece ClickHouse are cele mai bune performanțe cu inserări mari (mii de rânduri deodată), nu cu inserări de rânduri individuale.
Schema noastră ClickHouse este optimizată pentru cele două tipare de interogare cele mai frecvente: căutarea tuturor evenimentelor pentru un anumit ID de vizitator și agregarea evenimentelor pe intervale de timp. Folosim un motor MergeTree cu o cheie primară de (visitor_id, timestamp), care oferă căutări punctuale rapide și scanări de interval eficiente. Vizualizările materializate mențin metrici zilnice și orare pre-agregate.
Atingerea sub-30ms la scară
Trei decizii arhitecturale au fost esențiale pentru atingerea țintei noastre de latență. În primul rând, pipeline-ul este complet streaming — începem să procesăm semnalele înainte ca întregul corp al cererii HTTP să fi fost primit. În al doilea rând, căutările în Redis folosesc connection pooling cu conexiuni persistente, eliminând overhead-ul handshake-ului TCP. În al treilea rând, scrierile în ClickHouse sunt complet asincrone și nu blochează niciodată calea de răspuns.
În teste de încărcare cu 50K de cereri/secundă, latența noastră p50 este de 12ms, p95 este de 24ms, iar p99 este de 38ms. p99 depășește ocazional ținta noastră de 30ms în timpul reechilibrării clusterului Redis, dar p95 rămâne în mod constant sub 30ms. Pentru clienții cu cerințe de latență mai stricte, oferim clustere Redis dedicate care elimină contenția multi-tenant.