Projectoverzicht

Een groot deel van het verkeer dat op een commerciële website binnenkomt is niet menselijk. Een deel ervan is onschuldig — zoekcrawlers, uptimemonitors, voorbeeldophalers. De rest niet: geautomatiseerde inlogpogingen, scrapers, kassabots, en geautomatiseerde kliks die stilletjes advertentiebudgetten opmaken en elk analysecijfer vergiftigen waarop een bedrijf besluiten neemt. Het probleem is dat het allemaal door dezelfde voordeur binnenkomt als echte klanten, en tegen de tijd dat het in een weekrapport opduikt is het geld al uitgegeven.

Onze klant wilde dat gat dichten met een platform dat één vraag beantwoordt — is deze bezoeker een mens of een machine? — terwijl het bezoek nog gaande is. Geen nachtelijke batchtaak, geen rapport: een oordeel dat terugkomt op hetzelfde moment dat de pagina laadt, zodat de systemen van de klant er meteen naar kunnen handelen.

We besteedden acht maanden aan het bouwen van dat product van begin tot eind, van de signaalverzamelaar in de browser via de scoringsservices tot het dataplatform eronder. De opdracht liep door 2022 en werd in 2023 afgerond.

Onder NDA

Dit project valt onder een geheimhoudingsovereenkomst. De klant, de productnaam en de interface voor eindklanten zijn weggelaten, en de architectuur hieronder is op het niveau van vorm beschreven en niet van implementatiedetails.

De uitdaging

Drie randvoorwaarden bepaalden het technische werk, en elk daarvan trok aan de andere.

Het moest realtime zijn. Een oordeel dat binnenkomt nadat de bezoeker al is geconverteerd — of al een budget heeft leeggetrokken — is een auditspoor en geen verdediging. Het scoren moest binnen een verzoek afgerond zijn, met een latencybudget dat klein genoeg was dat een klant het vrijwillig in het kritieke pad van zijn eigen paginalading zou zetten.

Het moest zware belasting aankunnen. Het platform draaide op werkelijk hoge volumes en verwerkte over het klantenbestand tienduizenden verzoeken per minuut, met verkeer dat zonder waarschuwing piekte — een campagnestart aan de kant van de klant en een aanval van de andere kant zien er voor een capaciteitsplanner identiek uit. De piek was nooit het gemiddelde, en het systeem moest op de piek gedimensioneerd zijn terwijl het bij het gemiddelde betaalbaar bleef.

Het moest bewijsbaar zijn. Elk oordeel moest achteraf uit te leggen en opnieuw te onderzoeken zijn. Klanten handelen niet naar een onverklaarde score, en noch de detectielogica noch de modellen erachter kunnen beter worden zonder dat de ruwe signalen die de besluiten van gisteren opleverden nog voorhanden zijn. Dat betekende alles bewaren — een bewaar- en analyseprobleem dat pal achter een latencyprobleem zit.

Realtime, hoog volume en volledige bewaring zijn drie eisen waar geen enkele database tegelijk aan voldoet. Het grootste deel van de architectuur hieronder volgt uit de weigering om op ook maar één ervan in te leveren.

Signaalverzameling in kale JavaScript

De clientkant van het platform is een verzamelaar die binnen de pagina van de klant draait en het bewijs verzamelt waarop een oordeel wordt gebaseerd: kenmerken van de browser en de omgeving, render- en timinggedrag, interactiepatronen, en de vele kleine inconsistenties die een echte browser bestuurd door een mens onderscheiden van een geautomatiseerde die de user agent van een echte browser draagt.

Hij is geschreven in kale JavaScript — geen framework, geen runtime uit een build, geen afhankelijkheden. Dat was een bewuste beperking en geen voorkeur:

  • Hij draait in andermans pagina. De verzamelaar zit ingebed in duizenden sites die wij niet beheren, naast wat die sites verder ook laden. Hij mag geen modulesysteem, geen polyfills en geen bepaalde browserondergrens aannemen, en hij mag niet botsen met wat er al op de pagina staat.
  • Hij moet klein en snel zijn. Alles wat naar elke bezoeker van elke klant wordt gestuurd, wordt afgemeten aan het prestatiebudget van die klant zelf. Een frameworkruntime was groter geweest dan de hele verzamelaar.
  • Hij werkt in een vijandige omgeving. De onderwerpen van de meting proberen hem actief te verslaan. Signaalverzameling moet zich verdedigen tegen een pagina die eromheen geïnstrumenteerd, aangepast of geëmuleerd kan zijn — en dat pleit voor code zonder lagen tussen haar en de browser-API's die ze uitleest.

De taak van de verzamelaar houdt op bij het verzamelen en versturen van signalen. Alle beoordeling gebeurt aan de serverkant, waar de logica niet zichtbaar is voor wat er beoordeeld wordt.

Go op het hete pad

Elke service op het verzoekpad is in Go geschreven. De werklast ligt dicht bij de ideale vorm voor die taal: enorme aantallen kleine, onafhankelijke, grotendeels I/O-gebonden verzoeken die gelijktijdig en met voorspelbare latency afgehandeld moeten worden.

Twee eigenschappen telden in de praktijk het zwaarst. Goroutines maakten gelijktijdigheid per verzoek goedkoop genoeg dat ingest, verrijking en scoring elk intern konden uitwaaieren zonder een threadpool om af te stemmen. En de runtime van Go gaf ons het gedrag in de staart van de latencyverdeling waarop het product werd verkocht — het getal dat telt in een realtimesysteem is niet het gemiddelde antwoord maar de traagste paar procent, en juist daar geeft een zwaardere runtime zijn budget uit.

Het pad zelf is bewust kort: binnenhalen en valideren, context oplossen, de detectieregels en modellen tegen de binnenkomende signalen afwegen, het oordeel teruggeven. Alles wat niet vóór het antwoord hoeft te gebeuren — opslaan, aggregeren, verrijken, modelterugkoppeling — wordt van het hete pad geduwd naar de asynchrone pijplijn erachter.

Van één database naar een dataplatform

Het platform ging live op PostgreSQL, en dat was in het begin de juiste keuze. Eén database hield de gebeurtenissen, de accounts, de configuratie en de aggregaties vast; één querytaal beantwoordde elke vraag; het hele product was op één plek te doorgronden terwijl het nog vorm zocht.

Het overleefde het contact met productievolumes niet, en de reden is het waard precies te benoemen: het probleem was niet "PostgreSQL is te traag", maar dat aan één opslag gevraagd werd tegelijk vier verschillende dingen te zijn. Een gebeurtenisopslag die met hoge snelheid alleen toevoegt, een opzoekopslag met lage latency op het verzoekpad, een analytisch magazijn voor aggregaties over miljarden rijen, en een zoekindex om afzonderlijke sessies te onderzoeken zijn vier werklasten met vier tegenstrijdige eisenpakketten. Afstemmen op de een maakte de andere slechter.

Dus haalden we ze uit elkaar en verhuisden we het platform naar Google Cloud:

Redis — de staat van het hete pad. Redis bewaart alles wat een scoringsbesluit binnen enkele milliseconden moet raadplegen: recente staat per bezoeker en per sessie, tellers voor snelheid en frequentie, opgeloste configuratie, en reputatie-opzoekingen. Het is wat een oordeel binnen het verzoek überhaupt mogelijk maakt, en het vangt de leesbelasting op die anders op de hoofddatabase was geland.

— de naad tussen synchroon en asynchroon. Elk beoordeeld verzoek wordt als gebeurtenis gepubliceerd en het antwoord komt meteen terug; alles verderop leest daaruit. Deze ontkoppeling houdt het verzoekpad kort en laat de rest van het platform schalen, opnieuw uitrollen of tijdelijk vertragen zonder dat het scoringsendpoint het merkt. Het betekent ook dat een verkeerspiek in een wachtrij landt in plaats van in een database.

Objectopslag — ruwe gebeurtenissen, onbeperkt bewaard. Elke signaallading wordt in de oorspronkelijke vorm gearchiveerd naar S3-compatibele . Het is de goedkoopste duurzame plek voor data waarvan het toekomstige gebruik nog niet vaststaat, en het is de reden dat detectielogica achteraf over echt historisch verkeer opnieuw gedraaid kan worden in plaats van over een samenvatting daarvan.

— de analytische laag. Aggregaties over de volledige gebeurtenishistorie, rapportage over verkeerskwaliteit, cohort- en campagneanalyse, en het verkennende werk achter nieuwe detectieheuristieken draaien hier, op een magazijn dat voor scans van die omvang is gebouwd, en volledig buiten het operationele pad.

Elasticsearch — onderzoek. Betwist een klant een oordeel of gaat een analist achter een nieuw patroon aan, dan luidt de vraag "laat me deze specifieke sessies zien, zesvoudig gefilterd" — een interactief zoekprobleem in plaats van een aggregatieprobleem. bedient dat, zodat verkennende query's nooit de systemen raken die verkeer bedienen.

PostgreSQL bleef, met een veel smallere opdracht: accounts, klantconfiguratie, en de relationele data die werkelijk om transacties en constraints vraagt. De werklasten weghalen waar het nooit geschikt voor was, is wat het weer een goede keuze maakte.

Kubernetes op Google Cloud

Het platform draait als services in containers op een Kubernetes-cluster in Google Cloud. Gezien het verkeersprofiel stond het alternatief nooit serieus ter discussie: belasting die binnen minuten een orde van grootte schommelt vraagt om infrastructuur die zelf capaciteit toevoegt, en om services die stuk voor stuk goedkoop te vermenigvuldigen zijn.

De opsplitsing in kleine Go-services betaalt zich hier terug. Ingest, scoring en de asynchrone afnemers hebben heel verschillende schaalcurves, en als afzonderlijke deployments schaalt elk op zijn eigen signaal — de ingestlaag volgt het aantal verzoeken, de afnemers volgen de diepte van de wachtrij. Rollende uitrollen betekenden dat het scoringsendpoint tijdens kantooruren bijgewerkt kon worden zonder onderhoudsvenster, en voor een dienst die in het paginaladingspad van een klant zit was dat een harde eis en geen gemak.

Resultaat

De klant sloot de opdracht af met een productieplatform dat live verkeer in realtime beoordeelt, tienduizenden verzoeken per minuut volhoudt met ruimte voor pieken van een veelvoud daarvan, en elk ruw signaal bewaart dat het ooit heeft gezien — zodat een detectieverbetering van vandaag getoetst kan worden aan jaren echt verkeer in plaats van aan een kunstmatige benchmark.

Minstens zo belangrijk: de architectuur scheidt de zorgen langs de lijnen die er voor het bedrijf werkelijk toe doen. Het hete pad blijft klein, snel en saai, terwijl analyse, onderzoek en modelwerk achter een wachtrij gebeuren waar ze onbeperkt kunnen groeien zonder ooit de responstijd te bedreigen die een klant ziet.

Dit is het soort systeem dat we van begin tot eind oppakken: Go-services met hoge doorvoer, een gebeurtenisgedreven dataplatform, en de Kubernetes-infrastructuur om beide te draaien. Heb je een werklast met deze vorm, vertel het ons.