Quando un parser si sposta da un server a un cluster di più worker, sorge un nuovo problema: più macchine iniziano a utilizzare lo stesso pool di proxy contemporaneamente. Senza coordinazione, questo porta alla duplicazione degli IP nelle richieste, al superamento dei limiti per un singolo indirizzo e a ban di massa. In questo articolo analizzeremo come shardare correttamente i proxy tra i server, quali schemi di distribuzione funzionano meglio e come implementarlo nella pratica con codice in Python e Redis.
Perché i proxy confliggono in un parser distribuito
Immaginiamo una situazione tipica: hai un pool di 500 proxy e tre server, ognuno dei quali esegue 50 worker di parsing paralleli per Wildberries, Ozon o Avito. Se ogni server prende i proxy dalla lista comune senza coordinazione, due worker su macchine diverse possono inviare contemporaneamente una richiesta dallo stesso IP allo stesso dominio. Per un sistema anti-bot, questo appare come una frequenza anormalmente alta di richieste da un singolo indirizzo — e l'IP viene bannato in pochi minuti.
Il secondo problema è la distribuzione irregolare del carico. Se i proxy vengono semplicemente divisi "come capita", un server può prendere i proxy più veloci e freschi, mentre un altro si ritrova con quelli già compromessi. Di conseguenza, la velocità di parsing sui server varierà notevolmente, così come il tasso di errore.
Il terzo problema è la perdita di stato. Se un proxy viene bannato su un server, gli altri server non ne sono a conoscenza e continueranno a utilizzarlo fino a quando non verrà bannato anche per loro. Senza un registro centralizzato dello stato dei proxy, ogni server "riapre l'America" da capo, sprecando richieste.
La soluzione a tutti e tre i problemi è introdurre un meccanismo di coordinazione unico: tramite uno storage centralizzato dello stato (Redis, PostgreSQL, Consul) o attraverso una divisione deterministica del pool in anticipo, senza sovrapposizioni di intervallo tra i server.
Architettura del parser distribuito
Prima di dividere i proxy, è necessario decidere l'architettura generale del cluster. Ci sono due approcci di base.
Schema statico (shared-nothing). Il pool di proxy viene diviso tra i server in anticipo, una sola volta all'avvio. Ogni server riceve il proprio segmento non sovrapposto e lavora autonomamente, senza fare riferimento ad altri server. Vantaggio: massima semplicità e resilienza: il guasto di un server non influisce sugli altri. Svantaggio: se su un server vengono bannati metà dei proxy, non può "prendere in prestito" proxy freschi dal vicino.
Schema dinamico (shared-state). Tutti i server accedono a uno storage comune (di solito Redis), che memorizza lo stato di ogni proxy e lo fornisce su richiesta con blocco. Vantaggio: utilizzo uniforme del pool e reazione immediata ai ban. Svantaggio: lo storage diventa un unico punto di guasto, è necessaria una logica separata nel caso in cui non sia disponibile.
Nella pratica, per il parsing di marketplace e social media si sceglie spesso un ibrido: un broker su Redis fornisce proxy con blocco per un breve periodo (TTL), e in caso di indisponibilità di Redis, i server si spostano temporaneamente su un pool di fallback locale. Questo approccio combina l'affidabilità dello schema statico con la flessibilità dello schema dinamico.
Metodi di divisione del pool di proxy tra i server
Esaminiamo algoritmi specifici che possono essere applicati per la divisione del pool.
Divisione semplice per modulo (round-robin sharding). Numeriamo i proxy da 0 a N-1, il server con numero i prende tutti i proxy per cui index % numero_server == i. Semplice, ma non gestisce bene l'aggiunta o la rimozione di server: quando cambia il numero di macchine, quasi tutto il pool viene ridistribuito.
Hashing consistente. A ogni server e a ogni proxy viene assegnata una posizione su un anello di hash (di solito in base all'hash dell'indirizzo IP o all'ID del server). Il proxy è assegnato al server più vicino sull'anello. Quando si aggiunge o si rimuove un server, viene ridistribuita solo una piccola parte dei proxy, non l'intero pool — questo è il vantaggio chiave per i cluster che si scalano dinamicamente (ad esempio, in Kubernetes con autoscaling).
Broker centralizzato con blocchi. Invece di assegnare rigidamente i proxy a un server, tutti i server richiedono un proxy "libero" a un servizio comune (Redis, RabbitMQ), che lo fornisce con un blocco temporaneo. Dopo l'uso, il proxy torna nel pool. Questa è l'opzione più flessibile, soprattutto se il carico sui server è irregolare.
Divisione per domini o task. Se il parser lavora contemporaneamente con più fonti (ad esempio, Wildberries, Ozon e Avito), ha senso dividere non solo i proxy tra i server, ma anche i proxy tra le fonti — affinché lo stesso IP non venga utilizzato su più piattaforme. Questo riduce il rischio che l'IP finisca in una blacklist comune dei sistemi anti-frode, se tali piattaforme scambiano dati.
Tabella: confronto dei metodi di sharding dei proxy
| Metodo | Complessità | Resilienza allo scaling | Quando applicare |
|---|---|---|---|
| Divisione per modulo | Bassa | Scarsa | Numero fisso di server |
| Hashing consistente | Media | Buona | Autoscaling del cluster |
| Broker centralizzato | Media-alta | Eccellente | Carico irregolare, alto traffico |
| Divisione per domini | Bassa | Media | Parsing di più piattaforme contemporaneamente |
Implementazione di un broker di proxy su Redis
L'opzione più pratica per la maggior parte dei parser distribuiti è un broker centralizzato su Redis. Redis è adatto perché è veloce, supporta operazioni atomiche e TTL "out of the box". L'idea è semplice: tutti i proxy disponibili sono memorizzati in un Redis Set e quando un proxy viene fornito a un worker specifico, viene bloccato per un certo periodo tramite una chiave con TTL.
Di seguito è riportato un esempio funzionante di broker in Python con la libreria redis-py. Implementa la fornitura di un proxy libero con blocco, il rilascio dopo l'uso e la marcatura del proxy come bannato.
import redis
import time
import random
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
PROXY_POOL_KEY = "proxy:pool" # insieme di tutti i proxy funzionanti
LOCK_PREFIX = "proxy:lock:" # prefisso di blocco per un proxy specifico
BANNED_PREFIX = "proxy:banned:" # prefisso di marcatura per il ban
LOCK_TTL = 30 # secondi per cui blocchiamo il proxy
BAN_TTL = 3600 # secondi per cui consideriamo il proxy bannato
def load_pool(proxy_list):
"""Carichiamo la lista di proxy in Redis una sola volta all'avvio del cluster."""
if proxy_list:
r.sadd(PROXY_POOL_KEY, *proxy_list)
def get_free_proxy(server_id, max_attempts=20):
"""Ogni server chiama questa funzione per ottenere un proxy libero."""
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
# impostazione atomica del blocco, se il proxy è libero
acquired = r.set(lock_key, server_id, nx=True, ex=LOCK_TTL)
if acquired:
return proxy
return None # nessun proxy libero trovato, è necessario un fallback
def release_proxy(proxy):
"""Rilasciamo il proxy dopo una richiesta riuscita."""
r.delete(LOCK_PREFIX + proxy)
def mark_banned(proxy):
"""Segniamo il proxy come bannato per BAN_TTL secondi, rimuoviamo il blocco."""
r.set(BANNED_PREFIX + proxy, 1, ex=BAN_TTL)
r.delete(LOCK_PREFIX + proxy)
r.srem(PROXY_POOL_KEY, proxy) # rimuoviamo temporaneamente dal pool
def return_to_pool_if_recovered():
"""Compito periodico: restituiamo i proxy di cui è scaduto il ban."""
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)
Il punto chiave qui è l'uso di SET ... NX EX in Redis. Questo comando è atomico: se due server tentano di bloccare lo stesso proxy contemporaneamente, solo uno di essi avrà successo, l'altro riceverà un rifiuto e passerà al prossimo candidato nella lista. Questo è ciò che esclude i conflitti tra server durante l'accesso parallelo al pool comune.
Ecco un esempio di utilizzo del broker in un worker del parser:
import requests
def fetch_with_proxy(url, server_id):
proxy = get_free_proxy(server_id)
if not proxy:
raise RuntimeError("Nessun proxy libero nel 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
Questo schema è facilmente scalabile: puoi aggiungere 10 o 50 server, tutti si collegheranno allo stesso istanza di Redis e non ci saranno conflitti di IP, perché il blocco è atomico a livello di storage, non a livello di singolo server.
Sessioni sticky e associazione dei proxy ai task
Per alcune attività, la semplice fornitura di "qualsiasi proxy libero" non è sufficiente. Se il parser emula il comportamento dell'utente — ad esempio, visualizzando consecutivamente le schede dei prodotti, aggiungendo al carrello, navigando tra le pagine del catalogo — è importante che tutte le richieste di una sessione provengano dallo stesso IP. Altrimenti, il sito vedrà un'improvvisa variazione di geolocalizzazione o dispositivo durante la sessione e la bloccherà come sospetta.
Per questo motivo si introduce il concetto di sessione sticky: il proxy non è associato a una singola richiesta, ma a un task specifico o a una sessione utente per tutta la sua durata. In Redis, questo viene implementato tramite una chiave separata del tipo session:{task_id} -> proxy con un TTL pari alla durata prevista della sessione.
SESSION_PREFIX = "proxy:session:"
SESSION_TTL = 600 # 10 minuti per la sessione
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
Per attività con sessioni lunghe — ad esempio, la navigazione nel catalogo di Wildberries emulando il comportamento di un acquirente reale — è fondamentale utilizzare proxy che si comportano il più possibile come un utente normale. Gli proxy residenziali sono particolarmente adatti, poiché sono associati a veri indirizzi IP domestici e suscitano meno sospetti nei sistemi anti-bot dei marketplace rispetto agli indirizzi dei data center.
Gestione dei ban e rotazione nel cluster
In un sistema distribuito, è importante che le informazioni sui ban dei proxy diventino immediatamente disponibili a tutti i server, non solo a quello che ha ricevuto il ban. Nell'esempio sopra, la funzione mark_banned si occupa di questo: non appena un server registra un 403 o 429 dal sito target, il proxy viene rimosso dal pool comune in Redis e gli altri server smettono automaticamente di riceverlo.
Per determinare un ban, è utile analizzare non solo il codice di risposta HTTP, ma anche il contenuto della pagina: alcuni siti restituiscono 200 OK, ma con un captcha o un placeholder invece del contenuto reale. La logica di rilevamento del ban di solito appare così:
- Codici HTTP 403, 429, 503 — chiaro segno di blocco o rate limit.
- Redirect a una pagina di captcha o di autorizzazione — verifica dell'URL dopo la richiesta.
- Improvviso ridimensionamento della risposta o assenza di selettori CSS attesi — segno di un placeholder.
- Timeout di connessione — spesso significa che il proxy ha smesso di rispondere del tutto.
Dopo aver registrato un ban, ha senso non solo rimuovere il proxy dal pool per sempre, ma metterlo in "quarantena" con TTL (come nell'esempio — BAN_TTL), dopo il quale il proxy torna automaticamente nel pool. Molti ban nei marketplace e nei social media sono temporanei e vengono rimossi dopo 30-60 minuti.
Se i ban si verificano troppo frequentemente anche con una distribuzione uniforme del carico, di solito è un segnale che il pool di proxy stesso ha una bassa qualità o una dimensione insufficiente per il volume richiesto di richieste. In tali casi, espandere il pool con proxy dei data center per attività meno sensibili (ad esempio, controllare la disponibilità delle pagine) e utilizzare IP residenziali più costosi solo per sessioni critiche aiuta a ridurre il tasso complessivo di ban senza aumentare le spese per l'intero traffico.
Monitoraggio della salute del pool di proxy
In un cluster di più server, è facile perdere la visione d'insieme: quanti proxy sono attivi, quanti sono bannati, quale è la velocità media di risposta di ciascuno. Senza monitoraggio, è difficile capire perché il parsing si è rallentato: se è crollato un server o se sono finiti i proxy attivi.
Il set minimo di metriche che vale la pena raccogliere in Redis o in un database separato è:
- Numero di proxy attivi nel pool al momento attuale.
- Numero di proxy bannati e tempo medio fino al loro recupero.
- Tempo medio di risposta per ciascun proxy nelle ultime N richieste.
- Percentuale di richieste riuscite (success rate) per server e per il pool nel suo insieme.
- Distribuzione del carico — quante richieste ha elaborato ciascun server nel periodo.
Per la visualizzazione, è comodo raccogliere queste metriche in Prometheus tramite un semplice esportatore, che legge i dati da Redis ogni minuto e li restituisce in un formato comprensibile a Grafana. Questo consente di vedere rapidamente un calo del success rate per una specifica geolocalizzazione o tipo di proxy e di aggiungere tempestivamente nuovi IP al pool, prima che il parser si fermi completamente.
È importante monitorare separatamente i picchi di ban quando si avviano nuovi server nel cluster: se, durante il scaling da 3 a 6 server, la percentuale di ban aumenta drasticamente, è probabile che il problema risieda nello sharding: i nuovi server hanno iniziato a sovrapporsi agli intervalli di proxy con i vecchi. Questo è il primo segnale che è necessario passare dalla divisione per modulo all'hashing consistente.
Checklist per l'avvio del parser distribuito
Prima di distribuire il parser su più server, controlla i seguenti punti:
- È stato scelto un meccanismo di coordinazione del pool: divisione statica, hashing consistente o broker centralizzato su Redis.
- È stata implementata una blocco atomico dei proxy, che esclude l'uso di un IP da due server contemporaneamente.
- È stato impostato un TTL per il blocco e per il ban, affinché i proxy non "rimangano bloccati" per sempre in caso di guasto del worker.
- Per i task che richiedono l'emulazione della sessione utente, è stata impostata l'associazione sticky dei proxy per tutta la durata del task.
- È stata impostata la rilevazione dei ban non solo in base al codice HTTP, ma anche al contenuto della risposta.
- È stato attivato il monitoraggio del success rate, dei proxy attivi e bannati in tutto il cluster.
- Il pool di proxy è stato diviso per tipi di task: IP dei data center — per richieste meno sensibili, proxy residenziali o proxy mobili — per sessioni in cui è importante massimizzare la somiglianza con un utente reale.
- È stato verificato il carico sullo storage dello stato (Redis) — con un gran numero di server e un'alta frequenza di richieste, potrebbe essere necessario un cluster Redis invece di un'unica istanza.
Conclusione
La principale causa di conflitti quando si passa a più server è l'assenza di una fonte unica di verità sullo stato del pool di proxy. Non appena appare un broker centralizzato con blocchi atomici (su Redis o uno storage simile), il problema della duplicazione degli IP e della desincronizzazione dei ban scompare da solo. Rimane solo da scegliere la strategia di sharding in base alla scala del tuo cluster: per un numero fisso di server, è sufficiente una semplice divisione, mentre per un'infrastruttura in crescita dinamica è meglio pianificare subito l'hashing consistente.
Se il tuo parser distribuito lavora con marketplace o social media, dove è importante la verosimiglianza del traffico a livello di singola sessione, è opportuno dividere il pool per tipo di task: per la scansione massiva dei cataloghi, sono adatti proxy dei data center a basso costo, mentre per sessioni che emulano il comportamento dell'utente — proxy residenziali con veri IP domestici, che riducono significativamente il tasso di ban nei sistemi sensibili alle frodi.