← Zurück zum Blog

Verteilter Parser auf mehreren Servern: Proxy-Pool ohne Konflikte und Sperren aufteilen

Wir analysieren die Architektur eines verteilten Parsers auf mehreren Servern – wie man den Proxy-Pool sinnvoll aufteilt, die Duplizierung von Anfragen von einer IP vermeidet und die Sperren minimiert.

📅21. Oktober 2026

Wenn ein Parser von einem Server auf ein Cluster von mehreren Workern umzieht, entsteht ein neues Problem – mehrere Maschinen beginnen gleichzeitig denselben Proxy-Pool zu verwenden. Ohne Koordination führt dies zu Duplikationen von IPs in Anfragen, Überschreitungen der Limits für eine Adresse und massiven Sperren. In diesem Artikel werden wir besprechen, wie man Proxys richtig zwischen Servern shardiert, welche Verteilungsschemata am besten funktionieren und wie man dies praktisch mit Code in Python und Redis umsetzt.

Warum Proxys in einem verteilten Parser Konflikte verursachen

Stellen wir uns eine typische Situation vor: Sie haben einen Pool von 500 Proxys und drei Server, von denen jeder 50 parallele Parsing-Worker für Wildberries, Ozon oder Avito ausführt. Wenn jeder Server unabhängig Proxys aus der gemeinsamen Liste ohne Koordination entnimmt, können zwei Worker auf verschiedenen Maschinen gleichzeitig eine Anfrage von derselben IP an dieselbe Domain senden. Für Anti-Bot-Systeme sieht dies wie eine anormal hohe Anfragefrequenz von einer Adresse aus – und die IP wird innerhalb von Minuten gesperrt.

Ein weiteres Problem ist die ungleichmäßige Verteilung der Last. Wenn Proxys einfach "wie es kommt" aufgeteilt werden, kann ein Server die schnellsten und frischesten IPs für sich beanspruchen, während ein anderer den Rest mit bereits beschädigtem Ruf erhält. Infolgedessen wird die Parsing-Geschwindigkeit auf den Servern erheblich variieren, ebenso wie die Fehlerquote.

Das dritte Problem ist der Verlust des Status. Wenn ein Proxy auf einem Server gesperrt wird, erfahren die anderen Server nichts davon und verwenden ihn weiterhin, bis sie ebenfalls gesperrt werden. Ohne eine zentralisierte Statusüberwachung "erfindet" jeder Server Amerika neu und verschwendet Anfragen.

Die Lösung aller drei Probleme besteht darin, einen einheitlichen Koordinationsmechanismus einzuführen: entweder über einen zentralisierten Statusspeicher (Redis, PostgreSQL, Consul) oder durch deterministische Aufteilung des Pools im Voraus, ohne Überschneidungen der Bereiche zwischen den Servern.

Architektur des verteilten Parsers

Bevor man Proxys aufteilt, muss man sich auf die allgemeine Architektur des Clusters einigen. Es gibt zwei grundlegende Ansätze.

Statisches Schema (shared-nothing). Der Proxy-Pool wird im Voraus, einmal beim Start, zwischen den Servern aufgeteilt. Jeder Server erhält sein nicht überlappendes Segment und arbeitet autonom damit, ohne auf andere Server zuzugreifen. Vorteil – maximale Einfachheit und Ausfallsicherheit: Der Ausfall eines Servers hat keine Auswirkungen auf die anderen. Nachteil – wenn auf einem Server die Hälfte der Proxys gesperrt wird, kann er sich keine frischen IPs von einem Nachbarn "ausleihen".

Dynamisches Schema (shared-state). Alle Server greifen auf einen gemeinsamen Speicher (meistens Redis) zu, der den Status jedes Proxys speichert und ihn auf Anfrage mit Sperrung ausgibt. Vorteil – gleichmäßige Nutzung des Pools und sofortige Reaktion auf Sperren. Nachteil – der Speicher wird zu einem einzigen Ausfallpunkt, es ist eine separate Logik erforderlich, falls er nicht verfügbar ist.

In der Praxis wird für das Parsing von Marktplätzen und sozialen Netzwerken häufig ein Hybridansatz gewählt: Ein Broker auf Redis gibt Proxys mit einer kurzen Sperrung (TTL) aus, und bei Nichtverfügbarkeit von Redis schalten die Server vorübergehend auf einen lokalen Fallback-Pool um. Dieser Ansatz kombiniert die Zuverlässigkeit des statischen Schemas mit der Flexibilität des dynamischen.

Methoden zur Aufteilung des Proxy-Pools zwischen Servern

Lassen Sie uns spezifische Algorithmen betrachten, die zur Aufteilung des Pools angewendet werden können.

Einfache Aufteilung nach Modulo (Round-Robin Sharding). Proxys werden von 0 bis N-1 nummeriert, der Server mit der Nummer i entnimmt alle Proxys, bei denen index % anzahl_server == i. Einfach, aber schlecht geeignet für das Hinzufügen oder Entfernen von Servern – bei einer Änderung der Anzahl der Maschinen wird fast der gesamte Pool neu verteilt.

Konsistentes Hashing. Jeder Server und jeder Proxy erhält eine Position im Hashring (normalerweise basierend auf dem Hash der IP-Adresse oder der Server-ID). Der Proxy wird dem nächstgelegenen Server im Ring zugeordnet. Bei Hinzufügung oder Entfernung eines Servers wird nur ein kleiner Teil der Proxys neu verteilt, nicht der gesamte Pool – dies ist ein entscheidender Vorteil für Cluster, die dynamisch skalieren (z. B. in Kubernetes mit Auto-Scaling).

Zentralisierter Broker mit Sperrungen. Anstatt Proxys fest einem Server zuzuweisen, fragen alle Server einen "freien" Proxy bei einem gemeinsamen Dienst (Redis, RabbitMQ) an, der ihn mit einer zeitlichen Sperrung ausgibt. Nach der Verwendung wird der Proxy wieder in den Pool zurückgegeben. Dies ist die flexibelste Option, insbesondere wenn die Last auf den Servern ungleichmäßig ist.

Aufteilung nach Domains oder Aufgaben. Wenn der Parser gleichzeitig mit mehreren Quellen arbeitet (z. B. Wildberries, Ozon und Avito parallel), macht es Sinn, nicht nur Proxys zwischen Servern, sondern auch Proxys zwischen Quellen aufzuteilen – damit dieselbe IP nicht gleichzeitig auf mehreren Plattformen sichtbar ist. Dies verringert das Risiko, dass die IP in die gemeinsame Blacklist von Anti-Fraud-Systemen gelangt, wenn solche Plattformen Daten austauschen.

Tabelle: Vergleich der Methoden zur Shardierung von Proxys

Methode Komplexität Skalierbarkeit Wann anwenden
Aufteilung nach Modulo Niedrig Schlecht Festgelegte Anzahl von Servern
Konsistentes Hashing Mittel Gut Auto-Scaling des Clusters
Zentralisierter Broker Mittel-hoch Ausgezeichnet Ungleichmäßige Last, hoher Traffic
Aufteilung nach Domains Niedrig Mittel Parsing mehrerer Plattformen gleichzeitig

Implementierung eines Proxy-Brokers auf Redis

Die praktischste Option für die meisten verteilten Parser ist ein zentralisierter Broker auf Redis. Redis eignet sich, weil es schnell ist, atomare Operationen unterstützt und TTL "out of the box" bietet. Die Idee ist einfach: Alle verfügbaren Proxys liegen in einem Redis Set, und bei der Ausgabe eines Proxys an einen bestimmten Worker wird dieser für eine bestimmte Zeit über einen Schlüssel mit TTL gesperrt.

Im Folgenden finden Sie ein funktionierendes Beispiel für einen Broker in Python mit der Bibliothek redis-py. Er implementiert die Ausgabe eines freien Proxys mit Sperrung, die Freigabe nach der Nutzung und die Markierung des Proxys als gesperrt.

import redis
import time
import random

r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)

PROXY_POOL_KEY = "proxy:pool"       # Menge aller funktionierenden Proxys
LOCK_PREFIX = "proxy:lock:"         # Präfix für die Sperrung eines bestimmten Proxys
BANNED_PREFIX = "proxy:banned:"     # Präfix für die Markierung als gesperrt
LOCK_TTL = 30                       # Sekunden, für die der Proxy gesperrt wird
BAN_TTL = 3600                      # Sekunden, für die der Proxy als gesperrt gilt

def load_pool(proxy_list):
    """Lädt die Liste der Proxys einmal beim Start des Clusters in Redis."""
    if proxy_list:
        r.sadd(PROXY_POOL_KEY, *proxy_list)

def get_free_proxy(server_id, max_attempts=20):
    """Jeder Server ruft diese Funktion auf, um einen freien Proxy zu erhalten."""
    candidates = list(r.smembers(PROXY_POOL_KEY))
    random.shuffle(candidates)

    for proxy in candidates[:max_attempts]:
        if r.exists(BANNED_PREFIX + proxy):
            continue
        lock_key = LOCK_PREFIX + proxy
        # Atomare Sperrung, wenn der Proxy frei ist
        acquired = r.set(lock_key, server_id, nx=True, ex=LOCK_TTL)
        if acquired:
            return proxy
    return None  # Keine freien Proxys gefunden, Fallback erforderlich

def release_proxy(proxy):
    """Gibt den Proxy nach erfolgreicher Anfrage frei."""
    r.delete(LOCK_PREFIX + proxy)

def mark_banned(proxy):
    """Markiert den Proxy als gesperrt für BAN_TTL Sekunden, entfernt die Sperrung."""
    r.set(BANNED_PREFIX + proxy, 1, ex=BAN_TTL)
    r.delete(LOCK_PREFIX + proxy)
    r.srem(PROXY_POOL_KEY, proxy)  # Vorübergehend aus dem Pool entfernen

def return_to_pool_if_recovered():
    """Periodische Aufgabe: Gibt Proxys zurück, deren Sperre abgelaufen ist."""
    for proxy in r.scan_iter(match=BANNED_PREFIX + "*"):
        clean_proxy = proxy.replace(BANNED_PREFIX, "")
        if not r.exists(BANNED_PREFIX + clean_proxy):
            r.sadd(PROXY_POOL_KEY, clean_proxy)

Der Schlüsselpunkt hier ist die Verwendung von SET ... NX EX in Redis. Dieser Befehl ist atomar: Wenn zwei Server gleichzeitig versuchen, denselben Proxy zu sperren, wird nur einer von ihnen erfolgreich sein, der andere erhält eine Ablehnung und geht zum nächsten Kandidaten in der Liste. Genau das schließt Konflikte zwischen Servern beim parallelen Zugriff auf den gemeinsamen Pool aus.

Beispiel für die Verwendung des Brokers im Parser-Worker:

import requests

def fetch_with_proxy(url, server_id):
    proxy = get_free_proxy(server_id)
    if not proxy:
        raise RuntimeError("Keine freien Proxys im Pool")

    proxies = {"http": f"http://{proxy}", "https": f"http://{proxy}"}
    try:
        response = requests.get(url, proxies=proxies, timeout=10)
        if response.status_code in (403, 429):
            mark_banned(proxy)
        else:
            release_proxy(proxy)
        return response
    except requests.exceptions.RequestException:
        mark_banned(proxy)
        raise

Dieses Schema lässt sich leicht skalieren – man kann 10 oder 50 Server hinzufügen, sie werden alle auf dasselbe Redis-Instanz zugreifen, und es werden keine IP-Konflikte auftreten, da die Sperrung auf der Ebene des Speichers atomar ist und nicht auf der Ebene eines einzelnen Servers.

Sticky-Sessions und Bindung von Proxys an Aufgaben

Für einige Aufgaben reicht die einfache Ausgabe "irgendeines freien Proxys" nicht aus. Wenn der Parser das Verhalten eines Benutzers emuliert – zum Beispiel nacheinander Produktkarten ansieht, in den Warenkorb legt und durch die Katalogseiten navigiert – ist es wichtig, dass alle Anfragen einer Sitzung von derselben IP kommen. Andernfalls wird die Website einen plötzlichen Wechsel der Geo- oder Geräteinformationen mitten in der Sitzung bemerken und diese als verdächtig blockieren.

Dafür wird das Konzept der Sticky-Session eingeführt: Der Proxy wird nicht an eine Anfrage, sondern an eine bestimmte Aufgabe oder Benutzersitzung für die Dauer ihrer Existenz gebunden. In Redis wird dies über einen separaten Schlüssel des Typs session:{task_id} -> proxy mit einer TTL, die der erwarteten Dauer der Sitzung entspricht, realisiert.

SESSION_PREFIX = "proxy:session:"
SESSION_TTL = 600  # 10 Minuten für die Sitzung

def get_sticky_proxy(task_id, server_id):
    existing = r.get(SESSION_PREFIX + task_id)
    if existing:
        return existing
    proxy = get_free_proxy(server_id)
    if proxy:
        r.set(SESSION_PREFIX + task_id, proxy, ex=SESSION_TTL)
    return proxy

Für Aufgaben mit langen Sitzungen – zum Beispiel das Durchsuchen des Wildberries-Katalogs mit der Emulation des Verhaltens eines echten Käufers – ist es entscheidend, Proxys zu verwenden, die sich so verhalten wie ein normaler Benutzer. Hier sind residential Proxys gut geeignet, da sie an echte Heim-IP-Adressen gebunden sind und seltener Verdacht bei den Anti-Bot-Systemen der Marktplätze erregen als Rechenzentrumsadressen.

Umgang mit Sperren und Rotation im Cluster

In einem verteilten System ist es wichtig, dass Informationen über die Sperrung eines Proxys sofort allen Servern zur Verfügung stehen und nicht nur dem, der diese Sperrung erhalten hat. Im obigen Beispiel ist die Funktion mark_banned dafür verantwortlich – sobald ein Server 403 oder 429 von der Zielwebsite erhält, wird der Proxy aus dem gemeinsamen Pool in Redis entfernt, und die anderen Server hören automatisch auf, ihn zu verwenden.

Um eine Sperrung festzustellen, sollte man nicht nur den HTTP-Statuscode analysieren, sondern auch den Inhalt der Seite – einige Websites geben 200 OK zurück, aber mit einem Captcha oder einer Platzhalterseite anstelle des tatsächlichen Inhalts. Die Logik zur Erkennung von Sperren sieht normalerweise so aus:

  • HTTP-Codes 403, 429, 503 – eindeutiges Zeichen für eine Blockierung oder Rate-Limit.
  • Weiterleitung auf eine Captcha- oder Anmeldeseite – Überprüfung der URL nach der Anfrage.
  • Plötzlicher Rückgang der Antwortgröße oder Fehlen erwarteter CSS-Selektoren – Zeichen für eine Platzhalterseite.
  • Timeout der Verbindung – bedeutet oft, dass der Proxy überhaupt nicht mehr antwortet.

Nach der Feststellung einer Sperre ist es sinnvoll, den Proxy nicht einfach dauerhaft aus dem Pool zu entfernen, sondern ihn in "Quarantäne" mit einer TTL (wie im Beispiel – BAN_TTL) zu setzen, nach der der Proxy automatisch wieder in den Pool zurückkehrt. Viele Sperren auf Marktplätzen und in sozialen Netzwerken sind vorübergehend und werden nach 30-60 Minuten aufgehoben.

Wenn Sperren zu häufig auftreten, selbst bei gleichmäßiger Lastverteilung, ist das normalerweise ein Signal, dass der Proxy-Pool selbst von schlechter Qualität oder zu klein für das erforderliche Anfragevolumen ist. In solchen Fällen hilft es, den Pool durch Rechenzentrums-Proxys für weniger empfindliche Aufgaben (z. B. Überprüfung der Verfügbarkeit von Seiten) zu erweitern und teurere residential IPs nur für kritische Sitzungen zu verwenden, um die Gesamtzahl der Sperren zu reduzieren, ohne die Kosten für den gesamten Traffic zu erhöhen.

Überwachung der Gesundheit des Proxy-Pools

In einem Cluster aus mehreren Servern kann man leicht den Überblick verlieren: Wie viele Proxys sind derzeit aktiv, wie viele sind gesperrt, wie hoch ist die durchschnittliche Antwortgeschwindigkeit jedes einzelnen. Ohne Überwachung ist es schwierig zu verstehen, warum das Parsing langsamer geworden ist – ob ein Server ausgefallen ist oder ob die lebenden Proxys ausgegangen sind.

Der minimale Satz von Metriken, die man in Redis oder einer separaten Datenbank sammeln sollte:

  • Anzahl der aktiven Proxys im Pool zu diesem Zeitpunkt.
  • Anzahl der gesperrten Proxys und durchschnittliche Zeit bis zu ihrer Wiederherstellung.
  • Durchschnittliche Antwortzeit für jeden Proxy in den letzten N Anfragen.
  • Prozentsatz erfolgreicher Anfragen (Success Rate) pro Server und für den Pool insgesamt.
  • Lastverteilung – wie viele Anfragen jeder Server über einen Zeitraum bearbeitet hat.

Zur Visualisierung ist es praktisch, diese Metriken in Prometheus zu speichern, über einen einfachen Exporter, der einmal pro Minute Daten aus Redis liest und sie im für Grafana verständlichen Format bereitstellt. So kann man schnell einen Rückgang der Success Rate für ein bestimmtes Geo oder einen bestimmten Proxy-Typ erkennen und rechtzeitig frische IPs in den Pool hinzufügen, bevor der Parser vollständig stoppt.

Separat sollten die Peaks bei Sperren beim Start neuer Server im Cluster überwacht werden – wenn der Prozentsatz der Sperren beim Skalieren von 3 auf 6 Server plötzlich ansteigt, liegt das wahrscheinlich am Sharding: Die neuen Server haben begonnen, sich in den Proxy-Bereichen mit den alten zu überschneiden. Dies ist ein erstes Zeichen dafür, dass man von der Modulo-Aufteilung zu konsistentem Hashing wechseln sollte.

Checkliste zum Start eines verteilten Parsers

Bevor Sie den Parser auf mehreren Servern bereitstellen, gehen Sie die folgenden Punkte durch:

  • Ein Mechanismus zur Koordination des Pools wurde ausgewählt: statische Aufteilung, konsistentes Hashing oder zentralisierter Broker auf Redis.
  • Eine atomare Sperrung von Proxys wurde implementiert, die die gleichzeitige Nutzung einer IP durch zwei Server ausschließt.
  • TTL für Sperrung und Sperre wurde eingerichtet, damit Proxys nicht "für immer" im gesperrten Zustand verbleiben, wenn ein Worker ausfällt.
  • Für Aufgaben, die eine Emulation der Benutzersitzung erfordern, wurde die Bindung von Sticky-Proxys für die gesamte Dauer der Aufgabe eingerichtet.
  • Die Erkennung von Sperren wurde nicht nur nach HTTP-Code, sondern auch nach dem Inhalt der Antwort eingerichtet.
  • Die Überwachung der Success Rate, aktiven und gesperrten Proxys im gesamten Cluster wurde aktiviert.
  • Der Proxy-Pool wurde nach Aufgabentypen aufgeteilt: Rechenzentrums-IP für weniger empfindliche Anfragen, residential oder mobile Proxys für Sitzungen, bei denen die maximale Ähnlichkeit mit einem echten Benutzer wichtig ist.
  • Die Last auf dem Statusspeicher (Redis) wurde überprüft – bei einer großen Anzahl von Servern und einer hohen Anfragefrequenz kann ein Redis-Cluster anstelle einer einzelnen Instanz erforderlich sein.

Fazit

Der Hauptgrund für Konflikte beim Übergang des Parsers auf mehrere Server ist das Fehlen einer einheitlichen Quelle der Wahrheit über den Status des Proxy-Pools. Sobald ein zentralisierter Broker mit atomaren Sperrungen (auf Redis oder einem ähnlichen Speicher) vorhanden ist, verschwindet das Problem der Duplizierung von IPs und der Desynchronisation von Sperren von selbst. Danach bleibt nur noch, eine Sharding-Strategie für die Größe Ihres Clusters auszuwählen: Für eine feste Anzahl von Servern reicht eine einfache Aufteilung aus, während für eine dynamisch wachsende Infrastruktur besser von Anfang an konsistentes Hashing eingeplant werden sollte.

Wenn Ihr verteilter Parser mit Marktplätzen oder sozialen Netzwerken arbeitet, bei denen die Glaubwürdigkeit des Traffics auf der Ebene einzelner Sitzungen wichtig ist, sollten Sie den Pool nach Aufgabentypen aufteilen: Für massives Scannen von Katalogen eignen sich kostengünstige Rechenzentrums-Proxys, während für Sitzungen mit der Emulation des Benutzerverhaltens residential Proxys mit echten Heim-IP-Adressen, die die Sperrquote in sensiblen Anti-Fraud-Systemen deutlich senken, besser geeignet sind.