14 september
We hadden weken naar deze dag toegeleefd. Onze residentiële proxyprovider was in juli door een factureringsfout uitgevallen, en twee maanden lang draaide een steeds groter deel van onze 1.375 bronnen gedegradeerd of helemaal niet — fetches die timeden out, IP's die midden in een sessie geblokkeerd werden, het soort trage bloeding dat zich niet manifesteert als een storing maar als een krimpend aantal dat niemand opmerkt totdat het gênant wordt. We vernieuwden de pool. Nieuwe pool, 20.000 roterende residentiële sessieregels, elke exit getest voordat er iets bij productie in de buurt kwam. 100% van de test slaagde. AE, DE, JP — allemaal live, allemaal schoon.
Dus draaiden we een gesuperviseerde crawl, het soort waarbij iemand echt naar het dashboard kijkt in plaats van te vertrouwen op de cron. Ik verwachtte het tegenovergestelde van een ramp.
Wat we kregen was een database die in één enkele pass 84.401 rijen had verloren, met 102 bronnen die meer dan 25% lager stonden dan hun vorige snapshot. Erger dan de twee maanden proxy-uithongering die we net hadden opgelost.
De verkeerde theorie, kort samengevat
Mijn eerste instinct — en dat neem ik ronduit op de kin — was dat de nieuwe pool op een of andere manier slechter was dan de test suggereerde. Misschien werden de sessie-IP's halverwege de crawl rate-limited op een manier die een eenmalige test niet zou opvangen. Het is een redelijke theorie. Het is ook onjuist, en er zelfs maar een halve dag in geloven kostte ons tijd die we niet hadden, want de crawl-cron blijft de hele tijd gepauzeerd terwijl je zo'n theorie aan het uittesten bent — je wilt niet dat een slechte pass een goede snapshot overschrijft terwijl je nog aan het gokken bent.
Het signaal dat de theorie onderuithaalde was dubizzle. Een van onze bronnen met het hoogste volume in de VAE had tijdens die run daadwerkelijk 32.595 advertenties opgehaald — een volkomen gezond aantal, de proxy's werkten duidelijk — en vervolgens bijna alles weer weggegooid. Niet mislukt bij het ophalen. Opgehaald, en daarna weggegooid.
16 september: de echte bug gevonden
Vanaf dat moment werd het geen proxyverhaal meer, maar een verhaal over het vervangen van rijen. Onze persistentielaag maakt bij elke pass een snapshot van elke bron als een volledige overschrijving van <site>_cars_for_sale.csv — alleen actuele advertenties, hele tabel vervangen. Dat is correct gedrag wanneer een crawl volledig afrondt. Het is catastrofaal wanneer een crawl halverwege timet en de scraper wat voor partiële batch hij ook heeft kunnen verzamelen wegschrijft alsof het de complete snapshot was. Een bron die normaal 9.000 live advertenties bevat, wordt overschreven met de 1.200 die hij kon scrapen voordat de tijd om was, en de overige 7.800 verdwijnen — niet omdat de auto's verkocht zijn, maar omdat onze eigen code besloot dat een incomplete fetch een complete was.
De proxy's logen niet tegen ons. De retry-logica was het probleem, en die was al lange tijd stilletjes slecht, alleen gemaskeerd door een gezondere pool die fetches vroeger afrondde voordat de sweep er iets toe deed.
Dit is wat die sweep ons daadwerkelijk kostte, nadat we de rekensom hadden gemaakt:
| Oude retry-sweep | Gefixte sweep | |
|---|---|---|
| Retries per proxy | 3 | 1 |
| Vertraging per poging | 1s | — |
| Effectief plafond over een pool van 50 proxy's | ~56 minuten per URL | totaalbudget van 120 seconden |
| Gedrag bij timeout | schrijft partiële batch als volledige snapshot | bron overgeslagen, vorige snapshot behouden |
Zesenvijftig minuten om het op te geven voor één enkele URL. Vermenigvuldig dat over een crawl die meer dan duizend bronnen aanraakt en je krijgt precies het soort stille, ongelijk verdeelde falen dat we zagen — sommige bronnen in orde, 102 ervan uitgehold, niets in de logs dat "proxy-storing" schreeuwde, want technisch gezien was er geen.
25 september: de fix, en het deel dat voor jou telt
De patch die negen dagen later uitkwam, deed twee onopvallende dingen. Hij beperkte de retry-wiskunde zodat een vastgelopen fetch snel faalt in plaats van bijna een uur te verslinden, en — dit is degene die het dataverlies daadwerkelijk voorkomt — een partiële fetch vervangt de volledige rijset niet meer. Als een bron maar door 1.200 van zijn gebruikelijke 9.000 advertenties komt, houden we de vorige complete snapshot aan in plaats van die te overschrijven met een fractie. Op dezelfde dag landde een tweede wijziging die een zichtbare CRAWL_PAUSED-vlag schrijft die onze monitoring controleert voordat iemand wordt gewaarschuwd over een daling — zodat een bewuste pauze leest als een pauze, en niet als een alarm.
Waarom zou een lezer die een gebruikte Hyundai zoekt zich hier iets bij aan moeten trekken? Omdat de onder-de-marktprijs-markeringen die we publiceren — 12.823 ervan live op de site deze week — alleen eerlijk zijn als de cohort waarmee we een advertentie vergelijken de werkelijke, complete regionale markt is, en niet een door een timeout afgekapt fragment ervan. Een deal-badge gebouwd op een snapshot die driekwart van zijn vergelijkingsset verloor, is geen deal-badge, het is ruis met een groen label. We pauzeren liever negen dagen een crawl dan dat we dat publiceren.
De dataset staat vandaag op 1.108.535 advertenties verspreid over 73 regio's en 1.375 bronnen — groter dan vóór dit alles begon, met vernieuwde proxy's en de rijvervangingsbug gedicht. Maar het getal waar ik steeds op terugkom is niet het totaal. Het is 84.401 — de kloof tussen een crawl die op het proxyniveau gezond leek, en een die het daadwerkelijk was.



