14. September
Wir hatten wochenlang auf diesen Tag gewartet. Unser Anbieter für Residential-Proxys war im Juli wegen einer fehlgeschlagenen Zahlung ausgefallen, und zwei Monate lang crawlte ein wachsender Teil unserer 1.375 Quellen nur noch eingeschränkt oder gar nicht – Abrufe liefen ins Timeout, IPs wurden mitten in der Session blockiert, genau die Art von langsamem Aderlass, der sich nicht als Ausfall zeigt, sondern als schrumpfende Zahl, die niemandem auffällt, bis es peinlich wird. Wir erneuerten den Vertrag. Neuer Pool, 20.000 rotierende Residential-Session-Zeilen, jeder Exit wurde geprüft, bevor irgendetwas in die Nähe der Produktion durfte. 100 % der Probe bestanden. AE, DE, JP – alle live, alle saubere.
Also fuhren wir einen überwachten Crawl, von der Art, bei der tatsächlich jemand das Dashboard beobachtet, statt dem Cronjob blind zu vertrauen. Ich erwartete das genaue Gegenteil einer Katastrophe.
Was wir bekamen, war eine Datenbank, die in einem einzigen Durchlauf 84.401 Zeilen verloren hatte, bei 102 Quellen, die gegenüber ihrem vorherigen Snapshot um mehr als 25 % eingebrochen waren. Schlimmer als die zwei Monate Proxy-Mangel, die wir gerade erst behoben hatten.
Die falsche Theorie, kurz gefasst
Mein erster Impuls – und dazu stehe ich offen – war, dass der neue Pool irgendwie schlechter war, als die Probe suggerierte. Vielleicht wurden die Session-IPs mitten im Crawl auf eine Art rate-gelimitet, die ein einmaliger Test gar nicht erfasst. Das ist eine vernünftige Theorie. Sie ist auch falsch, und sie auch nur einen halben Tag lang zu glauben, kostete uns Zeit, die wir nicht hatten, denn der Crawl-Cronjob bleibt die ganze Zeit pausiert, während man eine solche Theorie debuggt – man will nicht, dass ein schlechter Durchlauf einen guten Snapshot überschreibt, während man noch rät.
Der Hinweis, der die Theorie zerschlug, war dubizzle. Eine unserer umsatzstärksten VAE-Quellen hatte in diesem Durchlauf tatsächlich 32.595 Anzeigen abgerufen – eine völlig gesunde Zahl, die Proxys funktionierten offensichtlich – und dann fast alle davon verworfen. Nicht fehlgeschlagen beim Abruf. Abgerufen, und dann weggeworfen.
16. September: den eigentlichen Bug finden
Ab da hörte es auf, eine Proxy-Geschichte zu sein, und wurde zu einer Geschichte über das Ersetzen von Zeilen. Unsere Persistenzschicht erstellt bei jedem Durchlauf für jede Quelle einen Snapshot als vollständiges Überschreiben von <site>_cars_for_sale.csv – nur aktuelle Anzeigen, die ganze Tabelle wird ersetzt. Das ist korrektes Verhalten, wenn ein Crawl fertig wird. Es ist katastrophal, wenn ein Crawl mitten im Lauf ins Timeout läuft und der Scraper schreibt, was er an unvollständigem Batch zusammenbekommen hat, als wäre es der vollständige Snapshot. Eine Quelle, die normalerweise 9.000 aktive Anzeigen hält, wird mit den 1.200 überschrieben, die sie abgegrast hat, bevor die Zeit ablief, und die anderen 7.800 verschwinden – nicht weil die Autos verkauft wurden, sondern weil unser eigener Code entschied, dass ein unvollständiger Abruf ein vollständiger sei.
Die Proxys haben uns nicht belogen. Die Retry-Logik war das Problem, und sie war schon lange leise kaputt, nur maskiert durch einen gesünderen Pool, der Abrufe früher fertigstellte, bevor der Sweep überhaupt relevant wurde.
Hier ist, was dieser Sweep tatsächlich gekostet hat, sobald wir nachgerechnet haben:
| Alter Retry-Sweep | Behobener Sweep | |
|---|---|---|
| Wiederholungen pro Proxy | 3 | 1 |
| Verzögerung pro Versuch | 1s | — |
| Effektive Obergrenze über einen 50-Proxy-Pool | ~56 Minuten pro URL | 120-Sekunden-Gesamtbudget |
| Verhalten bei Timeout | schreibt partiellen Batch als vollständigen Snapshot | Quelle wird übersprungen, vorheriger Snapshot bleibt erhalten |
Sechsundfünfzig Minuten, um eine einzelne URL aufzugeben. Das über einen Crawl mit über tausend Quellen hochgerechnet, ergibt genau die Art von stillem, ungleichmäßig verteiltem Ausfall, den wir sahen – manche Quellen in Ordnung, 102 davon ausgehöhlt, nichts in den Logs, das „Proxy-Ausfall" schrie, denn technisch gesehen gab es keinen.
25. September: der Fix, und der Teil, der für euch zählt
Der Patch, der neun Tage später ausgerollt wurde, tat zwei unspektakuläre Dinge. Er deckelte die Retry-Mathematik, sodass ein hängender Abruf schnell scheitert, statt den Großteil einer Stunde zu fressen, und – das ist der Teil, der den Datenverlust tatsächlich verhindert – ein partieller Abruf ersetzt nicht mehr den vollständigen Zeilensatz. Wenn eine Quelle nur 1.200 ihrer üblichen 9.000 Anzeigen durchbekommt, behalten wir den vorherigen vollständigen Snapshot, statt ihn mit einem Bruchteil zu überschreiben. Am selben Tag kam eine zweite Änderung hinzu, die ein sichtbares CRAWL_PAUSED-Flag schreibt, das unser Monitoring prüft, bevor es jemanden wegen eines Einbruchs alarmiert – sodass eine bewusste Pause als Pause gelesen wird, nicht als Alarm.
Warum sollte das einen Leser interessieren, der einen gebrauchten Hyundai sucht? Weil die Unter-Marktpreis-Markierungen, die wir veröffentlichen – 12.823 davon diese Woche live auf der Seite – nur dann ehrlich sind, wenn die Vergleichsgruppe, gegen die wir eine Anzeige abgleichen, der echte, vollständige regionale Markt ist und nicht ein durch Timeout verstümmeltes Fragment davon. Ein Deal-Badge, das auf einem Snapshot basiert, der drei Viertel seiner Vergleichsmenge verloren hat, ist kein Deal-Badge, es ist Rauschen mit einem grünen Etikett. Wir pausieren lieber neun Tage einen Crawl, als das zu veröffentlichen.
Der Datensatz steht heute bei 1.108.535 Anzeigen über 73 Regionen und 1.375 Quellen – größer als vor diesem ganzen Vorfall, Proxys erneuert und der Zeilenersatz-Bug geschlossen. Aber die Zahl, zu der ich immer wieder zurückkomme, ist nicht die Gesamtsumme. Es sind 84.401 – die Lücke zwischen einem Crawl, der auf Proxy-Ebene gesund aussah, und einem, der es tatsächlich war.



