Rust in productie: waarom we onze signaalverwerker herschreven
We herschreven onze signaalverwerkingsengine van Go naar Rust. Hier lees je waarom, wat we leerden en de 4x hogere doorvoer die we bereikten.
Zes maanden geleden namen we het besluit om onze signaalverwerkingsengine — het component dat ruwe browsersignalen omzet in genormaliseerde, hashbare feature vectors — te herschrijven van Go naar Rust. Dit was geen besluit dat we lichtvaardig namen. Onze Go-implementatie werkte. Ze was getest. Ze was uitgerold. Maar ze was het knelpunt in onze pipeline geworden, en we hadden een sprongsgewijze verbetering in doorvoer nodig. Dit is wat er gebeurde.
Waarom we Go ontgroeiden
Onze signaalverwerker doet rekenintensief werk: het parsen van JSON-payloads, het toepassen van normalisatiefuncties op meer dan 130 signalen, het berekenen van propriëtaire hashes en het opbouwen van identificatie-hashvectoren. In Go was dit werk CPU-gebonden, en de garbage collector van Go werd een probleem op schaal. Elke signaalverwerkingscyclus alloceerde tussenliggende objecten — geparste JSON-nodes, genormaliseerde stringwaarden, hashbuffers — die GC-druk veroorzaakten.
Bij 30K events/seconde vertoonde onze Go-signaalverwerker GC-pauzes van 2-5ms om de paar seconden. Deze pauzes waren acceptabel. Bij 50K events/seconde groeiden de GC-pauzes naar 8-15ms en traden ze vaker op. Bij 80K events/seconde — onze verwachte belasting voor Q3 — zouden de GC-pauzes de p99-latency onze SLA hebben doen overschrijden. We hadden ofwel meer servers nodig (duur) ofwel een efficiëntere implementatie.
Waarom Rust
We evalueerden drie opties: het optimaliseren van de Go-implementatie (sync.Pool, arena-allocatie, GOGC-tuning), herschrijven in C++, en herschrijven in Rust. Go-optimalisatie leverde een verbetering van 30% op, maar loste het GC-probleem niet fundamenteel op. C++ werd afgewezen vanwege zorgen over geheugenveiligheid in een beveiligingskritisch systeem. Rust bood zero-cost abstractions, geen garbage collector en geheugenveiligheidsgaranties die bij compileren worden afgedwongen.
Het Rust-ecosysteem had ook volwassen libraries voor alles wat we nodig hadden: serde voor het parsen van JSON, hoogperformante hashing-crates en tokio voor asynchrone I/O. De leercurve was reëel — ons team had diepgaande Go-ervaring maar beperkte Rust-ervaring — maar de prestatiekenmerken waren precies wat we nodig hadden.
Het herschrijfproces
We herschreven de signaalverwerker als een standalone service die via gRPC met de rest van onze pipeline communiceert. Hierdoor konden we deze naast de Go-implementatie uitrollen en het verkeer geleidelijk verschuiven. Het herschrijven kostte drie engineers vier weken — twee weken voor de kernimplementatie en twee weken voor testen, benchmarking en het afhandelen van randgevallen.
Het meest uitdagende aspect was niet de taal zelf, maar het waarborgen van gedragspariteit met de Go-implementatie. We bouwden een vergelijkingsharnas dat beide implementaties op dezelfde invoer draaide en verifieerde dat ze identieke uitvoer produceerden. We ontdekten tijdens dit proces 14 subtiele verschillen — voornamelijk gerelateerd aan floating-point-verwerking, Unicode-normalisatie en randgevallen bij het parsen van JSON.
Prestatieresultaten
De Rust-implementatie verwerkt signalen in gemiddeld 0,8ms tegenover 3,2ms voor Go — een 4x verbetering. Het geheugengebruik daalde van 2,1GB naar 340MB voor dezelfde werklast. Er zijn geen GC-pauzes omdat er geen garbage collector is. Het CPU-gebruik daalde met 60% bij dezelfde doorvoer, wat betekent dat elke server 4x meer verkeer aankan.
Bij 80K events/seconde handhaaft de Rust-implementatie een p99-verwerkingstijd van 1,4ms zonder pauzes. Deze speling betekent dat we de prestaties van signaalverwerking voor de afzienbare toekomst niet opnieuw hoeven te bekijken. Het verlaagde CPU- en geheugengebruik vertaalt zich ook direct in lagere infrastructuurkosten — we faseerden 8 van de 12 signaalverwerkingsservers uit.
Geleerde lessen
Herschrijven in Rust was de moeite waard voor ons specifieke geval — een CPU-gebonden, allocatie-intensieve, latency-gevoelige werklast. We zouden onze HTTP-ingestielaag of onze ClickHouse-queryservice niet in Rust herschrijven, omdat die componenten I/O-gebonden zijn en Go ze efficiënt afhandelt. De les is niet "herschrijf alles in Rust" maar "gebruik Rust waar zijn zero-cost abstractions en deterministische prestaties het meest van belang zijn".
De grootste verrassing was hoeveel de Rust-compiler tijdens het herschrijven ving. Verscheidene latente bugs in onze Go-implementatie — race conditions op gedeelde buffers, integer overflow in hashberekening en out-of-bounds-toegang op misvormde invoer — werden als compileerfouten in Rust gevangen. De compiler is veeleisend, maar betaalt zichzelf terug in correctheid.
Eerlijk gezegd was de eerste week pijnlijk. Sarah hield een telling van "gevechten met de borrow checker" bij op het whiteboard — we bereikten 47 voordat het team stopte met tellen. Maar tegen week drie werkte de code die compileerde gewoon. Geen mysterieuze productie-panics, geen dataraces onder belasting. Die afweging is de moeite waard voor alles op het hot path.