14 septembrie
Așteptam de săptămâni ziua asta. Furnizorul nostru de proxy-uri rezidențiale rămăsese fără acoperire în iulie, din cauza unei facturi neplătite, și de două luni o parte tot mai mare din cele 1.375 de surse ale noastre funcționa degradat sau deloc — fetch-uri care expirau, IP-uri blocate în mijlocul sesiunii, genul de hemoragie lentă care nu se manifestă ca o pană de sistem, ci mai degrabă ca o cifră care se tot micșorează, până când nimeni n-o observă decât când e deja stânjenitor. Am reînnoit contractul. Pool nou, 20.000 de linii de sesiune rezidențiale rotative, fiecare exit verificat înainte să se apropie de producție. 100% din teste au trecut. AE, DE, JP — toate active, toate curate.
Așa că am lansat un crawl supravegheat, genul în care cineva se uită efectiv pe dashboard, nu doar se bazează pe cron. Mă așteptam la exact opusul unui dezastru.
Ce am obținut a fost o bază de date care pierduse 84.401 de rânduri într-o singură trecere, cu 102 surse scăzute cu mai mult de 25% față de instantaneul anterior. Mai rău decât cele două luni de proxy-uri insuficiente pe care tocmai le reparasem.
Teoria greșită, pe scurt
Primul meu instinct — și o să recunosc deschis — a fost că noul pool era cumva mai slab decât arătau testele. Poate că IP-urile de sesiune erau limitate de rată în timpul crawl-ului, într-un mod pe care un test punctual nu l-ar fi prins. E o teorie rezonabilă. E și greșită, iar faptul că am crezut-o chiar și pentru jumătate de zi ne-a costat timp pe care nu-l aveam, pentru că cron-ul de crawl rămâne pus pe pauză cât timp depanezi o astfel de teorie — nu vrei ca o trecere proastă să suprascrie un instantaneu bun cât tu încă ghicești.
Indiciul care a spulberat teoria a fost dubizzle. Una dintre sursele noastre cu cel mai mare volum din Emiratele Arabe Unite preluase efectiv 32.595 de anunțuri în timpul acelei rulări — o cifră perfect sănătoasă, proxy-urile funcționau clar — și apoi aruncase aproape tot. Nu nereușit să le preia. Le-a preluat, și apoi le-a aruncat.
16 septembrie: găsirea bugului real
Atunci povestea a încetat să mai fie despre proxy-uri și a devenit despre înlocuirea rândurilor. Stratul nostru de persistență face instantaneu fiecare sursă ca o suprascriere completă a fișierului <site>_cars_for_sale.csv la fiecare trecere — doar anunțurile curente, se înlocuiește tot tabelul. E un comportament corect când un crawl se termină. E catastrofal când un crawl expiră la mijloc, iar scraperul scrie orice lot parțial a reușit să adune ca și cum ar fi instantaneul complet. O sursă care de obicei ține 9.000 de anunțuri active e suprascrisă cu cele 1.200 preluate înainte să sune ceasul, iar celelalte 7.800 dispar — nu pentru că mașinile s-au vândut, ci pentru că propriul nostru cod a decis că un fetch incomplet e unul complet.
Proxy-urile nu ne mințeau. Problema era logica de reîncercare, care fusese discret defectuoasă de multă vreme, mascată doar de un pool mai sănătos, care de obicei termina fetch-urile înainte ca bătaia de reîncercare să conteze.
Iată ce a costat efectiv acea bătaie de reîncercare, după ce am făcut calculele:
| Bătaia veche de reîncercare | Bătaia corectată | |
|---|---|---|
| Reîncercări per proxy | 3 | 1 |
| Întârziere per tentativă | 1s | — |
| Plafon efectiv pe un pool de 50 de proxy-uri | ~56 de minute per URL | buget total de 120 de secunde |
| Comportament la expirare | scrie lotul parțial ca instantaneu complet | sursa e omisă, se păstrează instantaneul anterior |
Cincizeci și șase de minute pentru a abandona un singur URL. Multiplică asta pe un crawl care atinge peste o mie de surse și obții exact genul de defecțiune silențioasă și distribuită neuniform pe care am văzut-o — unele surse perfect în regulă, 102 dintre ele devastate, nimic în loguri care să strige „pană de proxy", pentru că, tehnic, nu exista una.
25 septembrie: reparația, și partea care te interesează pe tine
Patch-ul lansat nouă zile mai târziu a făcut două lucruri deloc spectaculoase. A plafonat matematica reîncercărilor, astfel încât un fetch blocat eșuează rapid, în loc să consume aproape o oră, și — asta e partea care previne efectiv pierderea de date — un fetch parțial nu mai înlocuiește setul complet de rânduri. Dacă o sursă reușește să preia doar 1.200 din cele 9.000 de anunțuri obișnuite, păstrăm instantaneul complet anterior, în loc să-l suprascriem cu o fracțiune. În aceeași zi a apărut o a doua modificare, care scrie un semnalizator vizibil CRAWL_PAUSED, pe care monitorizarea noastră îl verifică înainte să alerteze pe cineva despre o scădere — astfel încât o pauză deliberată să fie citită ca o pauză, nu ca o alertă.
De ce ar trebui să-i pese unui cititor care caută un Hyundai la mâna a doua de toate astea? Pentru că etichetele de „sub prețul pieței" pe care le publicăm — 12.823 active pe site în această săptămână — sunt corecte doar dacă cohorta cu care comparăm un anunț este piața regională reală, completă, și nu un fragment trunchiat de un timeout. O etichetă de „ofertă bună" construită pe baza unui instantaneu care a pierdut trei pătrimi din setul de comparație nu e o etichetă de ofertă, e zgomot care poartă o etichetă verde. Preferăm să punem pe pauză un crawl timp de nouă zile decât să publicăm asta.
Setul de date se află astăzi la 1.108.535 de anunțuri, în 73 de regiuni și 1.375 de surse — mai mare decât era înainte să înceapă toată povestea asta, cu proxy-urile reînnoite și bugul de înlocuire a rândurilor închis. Dar cifra la care mă tot gândesc nu e totalul. E 84.401 — diferența dintre un crawl care părea sănătos la nivelul proxy-urilor și unul care era de fapt sănătos.



