Så byggde vi tracio.ais pipeline på under 30 ms
Från signalinsamling till besökar-ID på under 30 ms: vår arkitektur med Go, ClickHouse, Redis och distribuerad bearbetning.
När vi bestämde oss för att bygga tracio.ais motor för enhetsidentifiering hade vi ett krav som inte gick att förhandla om: hela pipelinen — från att ta emot krypterade signaler till att returnera ett besökar-ID — måste slutföras på under 30 millisekunder vid 95:e percentilen. Den här artikeln är en detaljerad genomgång av arkitekturen vi byggde för att nå det målet.
Översikt över pipelinen
Identifieringspipelinen har fem steg: signaldekryptering, signalnormalisering, hash-beräkning, identitetsupplösning och svarsserialisering. Varje steg optimeras oberoende, och steg som kan köras parallellt gör det. Den totala budgeten är 30 ms, ungefär fördelad som: dekryptering 2 ms, normalisering 3 ms, hashning 2 ms, identitetsupplösning 20 ms, serialisering 1 ms. Återstående 2 ms är buffert.
Signaldekryptering vänder på den klientkrypterade transporten. Vi använder Gos kryptopaket med hårdvaruacceleration, som slutför dekrypteringen av en typisk 4KB-payload på under 1 ms. Normaliseringen tolkar signal-JSON, validerar typer och tillämpar plattformsspecifika transformationer — till exempel normalisering av user agent-strängar för att ta bort versionsspecifikt brus.
Distribuerad identitetsupplösning
Identitetsupplösning — att avgöra om den här enheten har setts tidigare — är det mest latenskänsliga steget. Vi lagrar enhetsprofiler i Redis, shardade över ett kluster med hjälp av ett lager för distribuerad nyckeldirigering. Dirigeringen fördelar nycklar baserat på hårdvarunivåns fingeravtryck, vilket säkerställer att uppslagningar för samma enhet alltid träffar samma Redis-nod.
Vår shard-implementering använder virtuella noder (150 per fysisk nod) för att säkerställa jämn fördelning. När en nod läggs till eller tas bort behöver bara 1/N av nycklarna omfördelas, där N är antalet noder. Vi implementerade dirigeringslagret i Go med uppslagningstid O(log n) och noll allokeringar.
Redis som identitetslager
Vi valde Redis framför alternativen (Memcached, ScyllaDB, DynamoDB) på grund av dess konsekventa svarstider under en millisekund och stöd för komplexa datastrukturer. Varje enhetsprofil lagras som en Redis-hash med fält för varje signalnivås hash, besökar-ID:t, tidsstämpeln för senast sedd och konfidensmetadata.
Frågan för identitetsupplösning är ett enda HGETALL-anrop följt av en jämförelse av de inkommande signalhasharna mot de lagrade hasharna. Om hårdvarunivån stämmer returnerar vi det befintliga besökar-ID:t med hög konfidens. Om bara mjukvarunivån stämmer utför vi en likhetsjämförelse av data på signalnivå för att avgöra om det är samma enhet med en uppdaterad webbläsare. Om ingenting stämmer genererar vi ett nytt besökar-ID.
ClickHouse för händelselagring
Varje identifieringshändelse skrivs asynkront till ClickHouse. Vi använder en buffrad skrivare som batchar inserts — samlar händelser i 100 ms eller tills 1 000 händelser ackumulerats, det som inträffar först. Denna batchning är avgörande eftersom ClickHouse presterar bäst med stora inserts (tusentals rader åt gången) snarare än inserts av enskilda rader.
Vårt ClickHouse-schema är optimerat för de två vanligaste frågemönstren: att slå upp alla händelser för ett specifikt besökar-ID och att aggregera händelser över tidsperioder. Vi använder en MergeTree-motor med en primärnyckel bestående av (visitor_id, timestamp), vilket ger snabba punktuppslagningar och effektiva intervallskanningar. Materialiserade vyer underhåller föraggregerade dagliga och timvisa mätvärden.
Att nå under 30 ms i skala
Tre arkitekturbeslut var avgörande för att nå vårt latensmål. För det första är pipelinen helt strömmande — vi börjar bearbeta signaler innan hela HTTP-förfrågans kropp har tagits emot. För det andra använder Redis-uppslagningar connection pooling med beständiga anslutningar, vilket eliminerar TCP-handskakningens overhead. För det tredje är skrivningar till ClickHouse helt asynkrona och blockerar aldrig svarsvägen.
Under lasttester med 50K förfrågningar/sekund är vår p50-latens 12 ms, p95 är 24 ms och p99 är 38 ms. p99 överskrider ibland vårt mål på 30 ms under ombalansering av Redis-klustret, men p95 förblir konsekvent under 30 ms. För kunder med striktare latenskrav erbjuder vi dedikerade Redis-kluster som eliminerar konkurrens mellan flera tenanter.