Hoe we de sub-30ms-pipeline van tracio.ai bouwden
Van signaalverzameling naar bezoekers-ID in minder dan 30ms: onze architectuur met Go, ClickHouse, Redis en gedistribueerde verwerking.
Toen we begonnen met het bouwen van de apparaatidentificatie-engine van tracio.ai, hadden we één niet-onderhandelbare eis: de volledige pipeline — van het ontvangen van versleutelde signalen tot het teruggeven van een bezoekers-ID — moet in minder dan 30 milliseconden voltooid zijn op het 95e percentiel. Dit artikel is een gedetailleerde doorloop van de architectuur die we bouwden om dat doel te halen.
Overzicht van de pipeline
De identificatiepipeline heeft vijf fasen: signaaldecryptie, signaalnormalisatie, hashberekening, identiteitsresolutie en responsserialisatie. Elke fase wordt afzonderlijk geoptimaliseerd, en fasen die parallel kunnen draaien, doen dat ook. Het totale budget is 30ms, ruwweg als volgt verdeeld: decryptie 2ms, normalisatie 3ms, hashing 2ms, identiteitsresolutie 20ms, serialisatie 1ms. De resterende 2ms is buffer.
Signaaldecryptie draait het versleutelde transport aan de clientzijde terug. We gebruiken de cryptopakketten van Go met hardwareversnelling, die de decryptie van een typische payload van 4KB in minder dan 1ms voltooien. Normalisatie parseert de signaal-JSON, valideert types en past platformspecifieke transformaties toe — bijvoorbeeld het normaliseren van user-agent-strings om versiespecifieke ruis te verwijderen.
Gedistribueerde identiteitsresolutie
Identiteitsresolutie — bepalen of dit apparaat eerder is gezien — is de meest latentiegevoelige fase. We slaan apparaatprofielen op in Redis, geshard over een cluster via een gedistribueerde key-routinglaag. De routing verdeelt keys op basis van de hardware-tier-fingerprint, wat garandeert dat lookups voor hetzelfde apparaat altijd bij dezelfde Redis-node terechtkomen.
Onze sharding-implementatie gebruikt virtuele nodes (150 per fysieke node) om een gelijkmatige verdeling te waarborgen. Wanneer een node wordt toegevoegd of verwijderd, hoeft slechts 1/N van de keys te worden geremapt, waarbij N het aantal nodes is. We hebben de routinglaag in Go geïmplementeerd met een lookup-tijd van O(log n) en zonder allocaties.
Redis als identiteitsopslag
We kozen Redis boven alternatieven (Memcached, ScyllaDB, DynamoDB) vanwege de consistente responstijden onder een milliseconde en de ondersteuning voor complexe datastructuren. Elk apparaatprofiel wordt opgeslagen als een Redis-hash met velden voor de hash van elk signaalniveau, de bezoekers-ID, de last-seen-timestamp en confidence-metadata.
De query voor identiteitsresolutie is één enkele HGETALL-aanroep gevolgd door een vergelijking van de binnenkomende signaalhashes met de opgeslagen hashes. Als het hardware-tier overeenkomt, geven we de bestaande bezoekers-ID terug met hoge confidence. Als alleen het software-tier overeenkomt, voeren we een gelijkenisvergelijking uit van de data op signaalniveau om te bepalen of dit hetzelfde apparaat is met een bijgewerkte browser. Als niets overeenkomt, genereren we een nieuwe bezoekers-ID.
ClickHouse voor eventopslag
Elk identificatie-event wordt asynchroon naar ClickHouse geschreven. We gebruiken een gebufferde writer die inserts batcht — hij verzamelt events gedurende 100ms of totdat er 1.000 events zijn opgebouwd, wat het eerst komt. Deze batching is cruciaal omdat ClickHouse het beste presteert met grote inserts (duizenden rijen tegelijk) in plaats van losse rij-inserts.
Ons ClickHouse-schema is geoptimaliseerd voor de twee meest voorkomende querypatronen: het opzoeken van alle events voor een specifieke bezoekers-ID, en het aggregeren van events over tijdsperioden. We gebruiken een MergeTree-engine met een primaire sleutel van (visitor_id, timestamp), die snelle point-lookups en efficiënte range-scans biedt. Materialized views onderhouden voorgeaggregeerde dagelijkse en uurlijkse metrics.
Sub-30ms halen op schaal
Drie architecturale beslissingen waren cruciaal om ons latentiedoel te halen. Ten eerste is de pipeline volledig streaming — we beginnen met het verwerken van signalen voordat de volledige HTTP-request-body is ontvangen. Ten tweede gebruiken Redis-lookups connection pooling met persistente verbindingen, waarmee de overhead van de TCP-handshake wordt geëlimineerd. Ten derde zijn ClickHouse-writes volledig asynchroon en blokkeren ze het responspad nooit.
Onder belastingstests met 50K requests/seconde is onze p50-latentie 12ms, p95 24ms en p99 38ms. De p99 overschrijdt af en toe ons doel van 30ms tijdens het herbalanceren van het Redis-cluster, maar de p95 blijft consistent onder 30ms. Voor klanten met striktere latentie-eisen bieden we dedicated Redis-clusters die multi-tenant-contentie elimineren.