Scraping

Aufbau eines Residential Proxy Managers: Rotation, Health Checks und Retries

Die Logik, die entscheidet, welcher Exit verwendet wird, wann er auszusondern ist und wie eine Wiederherstellung erfolgt, gehört in eine einzige Komponente. Hier erfahren Sie, wie man sie entwirft und welchen Zustand sie benötigt.

Chris Collins

Chris Collins

26. August 2026 · 9 Min. Lesezeit

Die meisten Scraping-Codebasen entwickeln einen Proxy-Manager, ob nun jemand einen entwirft oder nicht. Es beginnt als Helfer, der eine Proxy-URL erstellt, dann fügt jemand einen Retry hinzu, dann einen Sonderfall für ein Ziel, das Sticky Sessions benötigt, dann einen Zähler, um zu verhindern, dass eine Adresse überlastet wird, die ständig fehlschlägt. Achtzehn Monate später ist diese Logik über vier Module verteilt, und niemand kann sagen, was passiert, wenn eine Anfrage zweimal in derselben Session fehlschlägt.

Es lohnt sich, dies bewusst zu bauen, denn die betroffenen Entscheidungen werden tatsächlich geteilt: Welchen Exit verwendet diese Anfrage, ist dieser Exit gesund, was passiert bei einem Fehler, und wie takten wir das Ganze. Hier ist ein Design für diese Komponente und den Zustand, den sie halten muss.

Wofür der Manager verantwortlich ist

Ziehe die Grenze eng. Ein Proxy-Manager entscheidet, wie eine Anfrage Ihre Infrastruktur verlässt und was zu tun ist, wenn das fehlschlägt. Er parst keine Seiten, er weiß nicht, wie ein Produktlisting aussieht, und er entscheidet nicht, welche URLs besucht werden. Ihn von Ihrer Geschäftslogik fernzuhalten ist genau das, was es jedem Job ermöglicht, ihn zu teilen.

Das lässt vier Verantwortlichkeiten übrig: die Auswahl eines Exits für jede Anfrage gemäß einer Richtlinie, die Verfolgung der Gesundheit dessen, was ausgegeben wird, die Anwendung der Fehlerbehandlung, wenn etwas schiefgeht, und die Durchsetzung der Taktung, damit einzelne Aufrufer ein Ziel nicht überlasten können. Alles andere gehört woanders hin.

Auswahl: Die Richtlinie ist pro Job, nicht global

Das Erste, was der Manager braucht, ist ein Konzept der Art von Exit, die ein Aufrufer möchte, statt eines einzigen globalen Modus. In der Praxis gibt es drei.

Rotating, bei dem jede Anfrage einen frischen Exit erhält. Dies ist die Standardeinstellung für die Massenerfassung und wird ausgedrückt, indem eine Session-Kennung weggelassen wird, was der Art entspricht, wie das Rotationsmodell genutzt werden soll.

Sticky, bei dem ein Aufrufer einen Exit für eine Sequenz hält, und der Manager muss für die Dauer dieser Sequenz dieselbe Session-Kennung zurückgeben und sie dann freigeben. Die Abwägungen dazu stehen in Sticky versus Rotating.

Geo-pinned, bei dem der Exit sich in einem bestimmten Land oder einer bestimmten Stadt befinden muss, was bei Multi-Market-Arbeit orthogonal zu den ersten beiden ist: ein Job kann rotating-in-Germany oder sticky-in-Chicago sein.

Modelliere das als anfragebezogenes Lease statt als globale Einstellung, sodass ein Aufrufer anfordert, was er braucht, und der Manager es erfüllt:

from dataclasses import dataclass
from typing import Optional
import itertools, time

@dataclass
class Lease:
    country: str
    session: Optional[str] = None          # None means rotate per request
    ttl: Optional[int] = None              # only meaningful with a session

    def username(self, customer="USERNAME"):
        parts = [f"customer-{customer}", f"country-{self.country}"]
        if self.session:
            parts.append(f"sid-{self.session}")
            if self.ttl:
                parts.append(f"ttl-{self.ttl}")
        return "-".join(parts)

    def proxies(self, password="PASSWORD", host="p.shifter.io:443"):
        url = f"http://{self.username()}:{password}@{host}"
        return {"http": url, "https": url}

Die Aufgabe des Managers ist es, ein Lease zu erzeugen, es dem Aufrufer zu übergeben und zu beobachten, was damit passiert.

Gesundheit: Sessions verfolgen, nicht Adressen

Hier machen die meisten selbstgebauten Manager Fehler. Bei einem gepoolten Gateway hält man keine Liste von IP-Adressen, die man als gut oder schlecht markiert, weil man sie nie ausgewählt hat und sie nicht behalten kann. Was man verfolgen kann, ist die Gesundheit einer Session und die aggregierte Gesundheit einer Route, also einer Kombination aus Land und Ziel.

Also pflege zwei Dinge. Pro aktiver Sticky-Session einen kleinen Datensatz mit aufeinanderfolgenden Fehlern und Challenge-Antworten, sodass eine Session, die eindeutig schlecht geworden ist, aussortiert und durch eine frische Kennung ersetzt werden kann. Pro Route eine gleitende Erfolgsrate, die anzeigt, ob eine gesamte Land-Ziel-Kombination sich verschlechtert hat, statt nur eine unglückliche Session.

Eine Gesundheitsprüfung in dieser Welt ist kein periodischer Ping. Ein Ping an einen Exit sagt Ihnen, dass er einen Testendpunkt erreichen kann, was nicht die Frage ist; die Frage ist, ob er Ihr Ziel erreichen und echten Inhalt bekommen kann. Verwenden Sie also passive Gesundheitsprüfung: jede echte Anfrage ist die Gesundheitsprüfung, und ihr validiertes Ergebnis aktualisiert den Datensatz. Aktives Sondieren lohnt sich nur für einen kleinen, günstigen Kanarienvogel gegen jedes Ziel, was nützlich ist, um zu unterscheiden zwischen “dieses Ziel ist für alle down” und “unsere Routen dorthin haben sich verschlechtert”.

Entscheidend ist, die Gesundheit anhand validierter Antworten zu beurteilen und nicht anhand von Statuscodes. Eine Challenge-Seite mit einem 200 ist für Gesundheitszwecke ein Fehler, und ein Manager, der das als Erfolg zählt, wird weiterhin eine Session wiederverwenden, über die das Ziel bereits entschieden hat. Das ist dieselbe Validierungsdisziplin wie beim Erkennen von blockiertem oder gefälschtem Inhalt.

class RouteHealth:
    def __init__(self, window=50):
        self.window, self.results = window, []

    def record(self, ok: bool):
        self.results.append(ok)
        if len(self.results) > self.window:
            self.results.pop(0)

    @property
    def success_rate(self):
        return sum(self.results) / len(self.results) if self.results else 1.0

    @property
    def degraded(self):
        return len(self.results) >= 10 and self.success_rate < 0.7

Fehlerbehandlung: klassifizieren, dann handeln

Der Manager besitzt die Entscheidung darüber, was ein Fehler bedeutet, was verhindert, dass diese Logik pro Job neu erfunden wird. Drei Klassen decken das ab.

Rate-Signale bedeuten verlangsamen: ein 429 oder ein Retry-After. Die korrekte Reaktion ist zu warten, und insbesondere nicht zu einem frischen Exit zu wechseln, damit das gleiche Tempo fortgesetzt werden kann, denn genau das verwandelt eine gedrosselte Route in eine verbrannte.

Identitätssignale bedeuten, dieser Exit ist für dieses Ziel erledigt: eine Blockseite, ein anhaltender 403, oder wiederholte Challenges auf einer Session. Die korrekte Reaktion ist, die Session aussortieren, eine frische nehmen und fortfahren, was dem Failover-Muster entspricht.

Terminale Fehler bedeuten stoppen: ein 404, eine fehlerhafte URL, oder ein 407, was ein Authentifizierungsproblem ist, das bei jedem Retry gleichermaßen falsch sein wird und laut fehlschlagen sollte statt zu loopen, wie in Behebung von 407- und Anmeldedatenfehlern behandelt.

Alles andere ist vorübergehend und erhält exponentiellen Backoff mit Jitter, begrenzt durch eine Versuchsobergrenze, gemäß Retry und Backoff. Der wichtige architektonische Punkt ist, dass der Retry im Manager liegt, sodass jeder Aufrufer dasselbe Verhalten erbt, und der Manager ein Retry-Budget über Aufrufer hinweg durchsetzen kann, statt dass jeder Job unabhängig gegen dasselbe kämpfende Ziel wiederholt.

Fügen Sie dabei auch einen Circuit Breaker pro Route hinzu. Wenn die Gesundheit einer Route unter einen Schwellenwert fällt, stoppen Sie das Senden dorthin für eine Abkühlphase, dann lassen Sie ein Rinnsal zu, um die Erholung zu testen. Das schützt das Ziel vor Ihrem Ansturm und schützt Ihr Konto davor, Fehler gegen eine Seite anzuhäufen, die niemandem antwortet.

Auch die Taktung gehört hierher

Da der Manager jede ausgehende Anfrage sieht, ist er der natürliche Ort, um Rate-Limits pro Ziel und Nebenläufigkeitsgrenzen durchzusetzen, was der Mechanismus ist, der in Rate Limiting und Request Throttling beschrieben wird. Dies hier zu platzieren hat einen konkreten Vorteil: Retries respektieren automatisch den Limiter, weil sie denselben Pfad wie jede andere Anfrage durchlaufen. Ein Retry, der die Taktung umgeht, ist die Art, wie aus einem kämpfenden Ziel ein blockiertes wird.

Alles zusammenfügen

Die gesamte Oberfläche ist klein, und das ist der Sinn der Sache:

class ProxyManager:
    def __init__(self, limiter_factory, health_factory):
        self.limiters = {}          # host -> TargetLimiter
        self.health = {}            # (country, host) -> RouteHealth
        self.limiter_factory, self.health_factory = limiter_factory, health_factory

    def get(self, url, host, country, session=None, attempts=4):
        route = self.health.setdefault((country, host), self.health_factory())
        limiter = self.limiters.setdefault(host, self.limiter_factory(host))
        sid = session
        for attempt in range(attempts):
            if route.degraded:
                raise RouteUnavailable(country, host)      # circuit open
            limiter.acquire()                              # pacing, retries included
            lease = Lease(country=country, session=sid, ttl=600 if sid else None)
            outcome = self._send(url, lease)               # returns (klass, response)
            route.record(outcome.klass == "ok")
            if outcome.klass == "ok":
                return outcome.response
            if outcome.klass == "terminal":
                raise TerminalError(url, outcome.response)
            if outcome.klass == "identity" and sid:
                sid = new_session_id()                     # retire, do not reuse
            if outcome.klass == "rate":
                time.sleep(outcome.retry_after or backoff(attempt))
            else:
                time.sleep(backoff(attempt))               # transient
        raise Exhausted(url)

Zwei Designhinweise. Der Manager gibt Antworten zurück und wirft typisierte Fehler statt None zurückzugeben, sodass Aufrufer einen Fehler nicht stillschweigend als leere Daten behandeln können. Und er verschluckt niemals einen terminalen Fehler, denn ein Authentifizierungs- oder Konfigurationsproblem sollte einen Job stoppen, statt unsichtbar wiederholt zu werden.

Betriebliche Belange

Machen Sie es beobachtbar. Der Manager sieht jede Anfrage, daher ist es der Ort, an dem Erfolgsrate pro Route, Retry-Verhältnis und Bytes pro Anfrage ausgegeben werden sollten, was genau die Daten sind, aus denen die Metriken in Proxy-KPIs gebildet werden und was Pipeline-Monitoring konsumiert.

Teilen Sie Zustand über Prozesse hinweg. Ein Manager pro Prozess multipliziert Ihre effektive Rate mit der Anzahl der Worker und fragmentiert die Gesundheitsverfolgung. In einer verteilten Bereitstellung gehören Limiter, Breaker und Routen-Gesundheit in gemeinsamen Speicher, was dieselbe Überlegung ist wie bei Skalierung mit Kubernetes.

Halten Sie Anmeldedaten heraus. Der Manager setzt Benutzernamen zusammen; er sollte das Passwort aus einem Secret-Store lesen, statt es in der Konfiguration zu halten, und Rotation sollte ein Deployment-Schritt sein statt einer Codeänderung.

Übertreiben Sie es nicht mit Abstraktion. Widerstehen Sie einer Plugin-Architektur für hypothetische Anbieter. Ein sauber ausgedrücktes Gateway ist leichter zu durchdenken als eine Abstraktionsschicht über Anbieter, die Sie nicht nutzen.

Das Fazit

Ein Proxy-Manager ist die Komponente, die entscheidet, wie eine Anfrage Ihr System verlässt und was passiert, wenn sie fehlschlägt, und diese Entscheidungen zu zentralisieren ist das, was verhindert, dass dieselbe Logik inkonsistent in jedem Job neu implementiert wird. Geben Sie Aufrufern ein anfragebezogenes Lease, sodass Rotation, Stickiness und Geografie pro Job statt global sind. Verfolgen Sie Gesundheit auf Session- und Routenebene, statt so zu tun, als würden Sie einzelne Adressen verwalten, und beurteilen Sie sie anhand validierter Antworten, sodass eine Challenge-Seite als der Fehler zählt, der sie ist. Besitzen Sie die Fehlerklassifizierung an einem Ort: warten bei Rate-Signalen, Session aussortieren bei Identitätssignalen, laut fehlschlagen bei terminalen Fehlern, und mit Jitter zurückweichen bei allem anderen, mit einem Circuit Breaker pro Route. Platzieren Sie die Taktung im selben Pfad, sodass Retries sie nicht umgehen können. Geben Sie dann Metriken davon aus, denn er ist die einzige Komponente, die alles sieht.

Darunter liegt das Residential-Proxy-Netzwerk: ein Gateway, in dem Land, Stadt, Session und TTL alle im Benutzernamen ausgedrückt werden, was einen Manager wie diesen zu einer kleinen Menge Code macht statt zu einem Integrationsprojekt, mit Preisgestaltung pro GB, sodass die Effizienz, die er Ihnen bringt, sich direkt auf der Rechnung zeigt.

Bereit, loszulegen?

Testen Sie Shifters Residential-Proxys, 205M+ IPs, 195+ Länder, ab $0.75/GB.

Jetzt starten