Lorsque le parseur passe d'un serveur à un cluster de plusieurs workers, un nouveau problème se pose : plusieurs machines commencent à utiliser le même pool de proxies simultanément. Sans coordination, cela entraîne une duplication des IP dans les requêtes, un dépassement des limites par adresse et des bans massifs. Dans cet article, nous allons examiner comment shard correctement les proxies entre les serveurs, quelles schémas de distribution fonctionnent le mieux et comment mettre cela en pratique avec du code en Python et Redis.
Pourquoi les proxies entrent en conflit dans un parseur distribué
Imaginons une situation typique : vous avez un pool de 500 proxies et trois serveurs, chacun exécutant 50 workers de parsing parallèles pour Wildberries, Ozon ou Avito. Si chaque serveur prend indépendamment des proxies dans la liste commune sans coordination, deux workers sur des machines différentes peuvent simultanément envoyer une requête depuis la même IP vers le même domaine. Pour le système anti-bot, cela ressemble à une fréquence anormalement élevée de requêtes depuis une même adresse - et l'IP se retrouve bannie en quelques minutes.
Le deuxième problème est la répartition inégale de la charge. Si les proxies sont simplement partagés "comme ça vient", un serveur peut prendre les IP les plus rapides et récentes, tandis qu'un autre se retrouve avec celles ayant déjà une réputation ternie. En conséquence, la vitesse de parsing sur les serveurs variera considérablement, tout comme le pourcentage d'erreurs.
Le troisième problème est la perte d'état. Si un proxy est banni sur un serveur, les autres serveurs ne le sauront pas et continueront à l'utiliser jusqu'à ce qu'ils soient également bannis. Sans un suivi centralisé de l'état des proxies, chaque serveur "redécouvre l'Amérique" à nouveau, gaspillant des requêtes.
La solution à ces trois problèmes est d'introduire un mécanisme de coordination unique : soit via un stockage d'état centralisé (Redis, PostgreSQL, Consul), soit par un partage déterministe du pool à l'avance, sans chevauchement des plages entre serveurs.
Architecture d'un parseur distribué
Avant de partager les proxies, il faut définir l'architecture générale du cluster. Il existe deux approches de base.
Schéma statique (shared-nothing). Le pool de proxies est partagé entre les serveurs à l'avance, une seule fois au démarrage. Chaque serveur reçoit son segment non chevauchant et travaille de manière autonome, sans faire appel aux autres serveurs. Avantage : simplicité maximale et résilience : la chute d'un serveur n'affecte pas les autres. Inconvénient : si la moitié des proxies est bannie sur un serveur, il ne peut pas "emprunter" des IP fraîches à son voisin.
Schéma dynamique (shared-state). Tous les serveurs accèdent à un stockage commun (le plus souvent Redis), qui conserve l'état de chaque proxy et le délivre sur demande avec verrouillage. Avantage : utilisation uniforme du pool et réaction instantanée aux bans. Inconvénient : le stockage devient un point de défaillance unique, nécessitant une logique distincte en cas d'indisponibilité.
En pratique, pour le parsing de marketplaces et de réseaux sociaux, on choisit souvent un hybride : un broker sur Redis délivre des proxies avec un verrouillage de courte durée (TTL), et en cas d'indisponibilité de Redis, les serveurs basculent temporairement sur un pool de secours local. Cette approche combine la fiabilité du schéma statique avec la flexibilité du schéma dynamique.
Méthodes de partage du pool de proxies entre serveurs
Examinons des algorithmes spécifiques qui peuvent être appliqués pour diviser le pool.
Partage simple par module (round-robin sharding). Les proxies sont numérotés de 0 à N-1, le serveur numéro i prend tous les proxies pour lesquels index % nombre_de_serveurs == i. Simple, mais mal adapté à l'ajout ou à la suppression de serveurs - lors du changement du nombre de machines, presque tout le pool est redistribué.
Hachage cohérent. Chaque serveur et chaque proxy se voit attribuer une position sur un anneau de hachage (généralement par le hachage de l'adresse IP ou de l'ID du serveur). Le proxy est associé au serveur le plus proche sur l'anneau. Lors de l'ajout ou de la suppression d'un serveur, seule une petite partie des proxies est redistribuée, et non l'ensemble du pool - c'est un avantage clé pour les clusters qui se développent dynamiquement (par exemple, dans Kubernetes avec auto-scaling).
Broker centralisé avec verrouillages. Au lieu de lier rigidement les proxies à un serveur, tous les serveurs demandent un proxy "libre" à un service commun (Redis, RabbitMQ), qui le délivre avec un verrouillage temporaire. Après utilisation, le proxy est retourné au pool. C'est l'option la plus flexible, surtout si la charge sur les serveurs est inégale.
Partage par domaines ou tâches. Si le parseur travaille avec plusieurs sources (par exemple, Wildberries, Ozon et Avito simultanément), il est judicieux de partager non seulement les proxies entre les serveurs, mais aussi les proxies entre les sources - afin qu'une même IP ne soit pas visible sur plusieurs plateformes. Cela réduit le risque que l'IP soit ajoutée à une liste noire commune des systèmes anti-fraude, si ces plateformes échangent des données.
Tableau : comparaison des méthodes de sharding des proxies
| Méthode | Complexité | Résilience à l'échelle | Quand appliquer |
|---|---|---|---|
| Partage par module | Faible | Mauvaise | Nombre fixe de serveurs |
| Hachage cohérent | Moyenne | Bonne | Auto-scaling du cluster |
| Broker centralisé | Moyenne-élevée | Excellente | Charge inégale, trafic élevé |
| Partage par domaines | Faible | Moyenne | Parsing de plusieurs plateformes simultanément |
Mise en œuvre d'un broker de proxies sur Redis
L'option la plus pratique pour la plupart des parseurs distribués est un broker centralisé sur Redis. Redis est adapté car il est rapide, prend en charge les opérations atomiques et le TTL "out of the box". L'idée est simple : tous les proxies disponibles sont stockés dans un ensemble Redis, et lors de la délivrance d'un proxy à un worker spécifique, il est verrouillé pour une durée déterminée via une clé avec TTL.
Ci-dessous, un exemple fonctionnel de broker en Python avec la bibliothèque redis-py. Il implémente la délivrance d'un proxy libre avec verrouillage, la libération après utilisation et la marquage du proxy comme banni.
import redis
import time
import random
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
PROXY_POOL_KEY = "proxy:pool" # ensemble de tous les proxies actifs
LOCK_PREFIX = "proxy:lock:" # préfixe de verrouillage d'un proxy spécifique
BANNED_PREFIX = "proxy:banned:" # préfixe de marquage de ban
LOCK_TTL = 30 # secondes pendant lesquelles le proxy est verrouillé
BAN_TTL = 3600 # secondes pendant lesquelles le proxy est considéré comme banni
def load_pool(proxy_list):
"""Charge la liste des proxies dans Redis une fois au démarrage du cluster."""
if proxy_list:
r.sadd(PROXY_POOL_KEY, *proxy_list)
def get_free_proxy(server_id, max_attempts=20):
"""Chaque serveur appelle cette fonction pour obtenir un proxy libre."""
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
# installation atomique du verrou, si le proxy est libre
acquired = r.set(lock_key, server_id, nx=True, ex=LOCK_TTL)
if acquired:
return proxy
return None # aucun proxy libre trouvé, un fallback est nécessaire
def release_proxy(proxy):
"""Libère le proxy après une requête réussie."""
r.delete(LOCK_PREFIX + proxy)
def mark_banned(proxy):
"""Marque le proxy comme banni pendant BAN_TTL secondes, enlève le verrou."""
r.set(BANNED_PREFIX + proxy, 1, ex=BAN_TTL)
r.delete(LOCK_PREFIX + proxy)
r.srem(PROXY_POOL_KEY, proxy) # enlève temporairement du pool
def return_to_pool_if_recovered():
"""Tâche périodique : retourne les proxies dont le ban a expiré."""
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)
Le point clé ici est l'utilisation de SET ... NX EX dans Redis. Cette commande est atomique : si deux serveurs essaient simultanément de verrouiller le même proxy, seul l'un d'eux réussira, l'autre échouera et passera au prochain candidat de la liste. C'est ce qui élimine les conflits entre serveurs lors d'un accès parallèle au pool commun.
Exemple d'utilisation du broker dans un worker de parseur :
import requests
def fetch_with_proxy(url, server_id):
proxy = get_free_proxy(server_id)
if not proxy:
raise RuntimeError("Aucun proxy libre dans le 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
Ce schéma est facilement extensible - vous pouvez ajouter 10 ou 50 serveurs, ils accéderont tous au même instance Redis, et il n'y aura pas de conflits d'IP car le verrouillage est atomique au niveau du stockage, et non au niveau d'un serveur individuel.
Sessions collantes et liaison des proxies aux tâches
Pour certaines tâches, la simple délivrance d'un "proxy libre" n'est pas suffisante. Si le parseur émule le comportement d'un utilisateur - par exemple, en parcourant successivement les fiches produits, en ajoutant au panier, en naviguant dans les pages du catalogue - il est important que toutes les requêtes d'une même session proviennent de la même IP. Sinon, le site remarquera un changement brusque de géo ou d'appareil au milieu de la session et la bloquera comme suspecte.
Pour cela, on introduit le concept de session collante : le proxy est lié non pas à une seule requête, mais à une tâche ou à une session utilisateur spécifique pendant toute sa durée de vie. Dans Redis, cela se réalise par une clé distincte de type session:{task_id} -> proxy avec un TTL égal à la durée attendue de la session.
SESSION_PREFIX = "proxy:session:"
SESSION_TTL = 600 # 10 minutes par session
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
Pour les tâches avec de longues sessions - par exemple, la navigation dans le catalogue de Wildberries en émulation du comportement d'un acheteur réel - il est crucial d'utiliser des proxies qui se comportent le plus possible comme un utilisateur ordinaire. Les proxies résidentiels sont bien adaptés, car ils sont liés à de véritables IP domestiques et suscitent moins de soupçons auprès des systèmes anti-bot des marketplaces par rapport aux adresses de centres de données.
Gestion des bans et rotation dans le cluster
Dans un système distribué, il est important que l'information sur le ban d'un proxy devienne instantanément accessible à tous les serveurs, et pas seulement à celui qui a reçu ce ban. Dans l'exemple ci-dessus, cela est géré par la fonction mark_banned - dès qu'un serveur enregistre un 403 ou un 429 du site cible, le proxy est retiré du pool commun dans Redis, et les autres serveurs cessent automatiquement de l'utiliser.
Pour déterminer un ban, il convient d'analyser non seulement le code HTTP de la réponse, mais aussi le contenu de la page - certains sites retournent un 200 OK, mais avec un captcha ou un placeholder à la place du contenu réel. La logique de détection de ban ressemble généralement à ceci :
- Les codes HTTP 403, 429, 503 - un signe évident de blocage ou de limite de taux.
- Redirection vers une page de captcha ou d'authentification - vérification de l'URL après la requête.
- Diminution brusque de la taille de la réponse ou absence de sélecteurs CSS attendus - signe de placeholder.
- Délai d'attente de connexion - signifie souvent que le proxy ne répond plus du tout.
Après avoir enregistré un ban, il est judicieux de ne pas simplement supprimer le proxy du pool pour toujours, mais de le mettre en "quarantaine" avec un TTL (comme dans l'exemple - BAN_TTL), après quoi le proxy revient automatiquement dans le pool. De nombreux blocages sur les marketplaces et les réseaux sociaux sont temporaires et se lèvent après 30 à 60 minutes.
Si les bans se produisent trop souvent même avec une répartition uniforme de la charge, cela signifie généralement que le pool de proxies lui-même a une mauvaise qualité ou une taille insuffisante pour le volume de requêtes nécessaire. Dans de tels cas, élargir le pool avec des proxies de centres de données pour des tâches moins sensibles (par exemple, vérifier la disponibilité des pages) et utiliser des IP résidentielles plus coûteuses uniquement pour des sessions critiques aide à réduire le pourcentage global de bans sans augmenter les coûts pour tout le trafic.
Surveillance de la santé du pool de proxies
Dans un cluster de plusieurs serveurs, il est facile de perdre la vue d'ensemble : combien de proxies sont actuellement actifs, combien sont bannis, quelle est la vitesse moyenne de réponse de chacun. Sans surveillance, il est difficile de comprendre pourquoi le parsing a ralenti - soit un serveur est tombé, soit il n'y a plus de proxies actifs.
L'ensemble minimal de métriques à collecter dans Redis ou une base de données distincte :
- Nombre de proxies actifs dans le pool à l'heure actuelle.
- Nombre de proxies bannis et temps moyen jusqu'à leur récupération.
- Temps moyen de réponse de chaque proxy pour les N dernières requêtes.
- Pourcentage de requêtes réussies (taux de succès) par serveurs et pour le pool dans son ensemble.
- Répartition de la charge - combien de requêtes chaque serveur a traité pendant la période.
Pour la visualisation, il est pratique de stocker ces métriques dans Prometheus via un simple exportateur, qui lit les données de Redis une fois par minute et les renvoie dans un format compréhensible par Grafana. Cela permet de voir rapidement une chute du taux de succès pour un géo ou un type de proxy spécifique et d'ajouter à temps des IP fraîches dans le pool, avant que le parseur ne s'arrête complètement.
Il est également important de surveiller les pics de bans lors du lancement de nouveaux serveurs dans le cluster - si, lors de l'extension de 3 à 6 serveurs, le pourcentage de bans augmente brusquement, il est probable que le problème soit dans le sharding : les nouveaux serveurs ont commencé à se chevaucher dans les plages de proxies avec les anciens. C'est le premier signe qu'il faut passer d'un partage par module à un hachage cohérent.
Checklist de lancement d'un parseur distribué
Avant de déployer le parseur sur plusieurs serveurs, passez en revue les points suivants :
- Mécanisme de coordination du pool choisi : partage statique, hachage cohérent ou broker centralisé sur Redis.
- Verrouillage atomique des proxies mis en œuvre, excluant l'utilisation d'une même IP par deux serveurs simultanément.
- TTL configuré pour le verrouillage et le ban, afin que les proxies ne "restent pas" dans un état bloqué pour toujours en cas de chute d'un worker.
- Pour les tâches nécessitant l'émulation d'une session utilisateur, liaison des proxies collants pour toute la durée de la tâche.
- Détection des bans configurée non seulement par le code HTTP, mais aussi par le contenu de la réponse.
- Surveillance du taux de succès, des proxies actifs et bannis dans tout le cluster activée.
- Pool de proxies divisé par types de tâches : IP de centres de données pour des requêtes moins sensibles, proxies résidentiels ou proxies mobiles pour des sessions où il est crucial de ressembler le plus possible à un utilisateur réel.
- Charge sur le stockage d'état (Redis) vérifiée - avec un grand nombre de serveurs et une fréquence élevée de requêtes, un cluster Redis peut être nécessaire au lieu d'une seule instance.
Conclusion
La principale raison des conflits lors du passage du parseur à plusieurs serveurs est l'absence d'une source unique de vérité sur l'état du pool de proxies. Dès qu'un broker centralisé avec des verrouillages atomiques (sur Redis ou un stockage similaire) est mis en place, le problème de duplication des IP et de désynchronisation des bans disparaît de lui-même. Il ne reste plus qu'à choisir une stratégie de sharding adaptée à l'échelle de votre cluster : pour un nombre fixe de serveurs, un simple partage suffit, tandis que pour une infrastructure en croissance dynamique, il est préférable de prévoir dès le départ un hachage cohérent.
Si votre parseur distribué travaille avec des marketplaces ou des réseaux sociaux où la crédibilité du trafic au niveau d'une session individuelle est importante, il vaut la peine de diviser le pool par type de tâches : pour le scanning massif des catalogues, des proxies de centres de données peu coûteux conviennent, tandis que pour les sessions avec émulation du comportement d'utilisateur, des proxies résidentiels avec de vraies IP domestiques, qui réduisent considérablement le pourcentage de bans dans les systèmes sensibles à la fraude, sont recommandés.