« Répartition de charge des proxys » est une expression qui signifie aujourd’hui tout autre chose qu’autrefois. Dans l’ancien modèle, vous achetiez une liste d’IP et construisiez un rotateur : choisir une IP, suivre sa santé, la retirer quand elle meurt, la remettre plus tard. Les équipes écrivent encore ce middleware par habitude, et sur un gateway moderne c’est en grande partie du code mort.
Sur un gateway de proxys résidentiels, le pool s’équilibre tout seul. Un point de terminaison, et le fournisseur attribue une sortie parmi des millions d’IP par requête ou par session, côté serveur. Vous ne répartissez pas des IP. Ce que vous architecturez vraiment, c’est la couche au-dessus : comment le travail se mappe aux identités, quelle concurrence une cible tolérera, et ce qui se passe quand une requête échoue. Réussissez ces trois-là et le système scale ; ratez-les et aucune qualité de proxy ne vous sauve.
Ceci est un deep-dive sur ces trois couches, pour les équipes qui font tourner des pipelines de collecte en production.
Ce que fait le gateway, et où commence votre architecture
Il vaut la peine d’être précis sur le partage, car il détermine ce que vous devriez et ne devriez pas construire :
Le gateway gère : la sélection de l’IP de sortie dans le pool, la correspondance géo, la santé des IP individuelles, et la mécanique de rotation. Vous exprimez l’intention via le nom d’utilisateur (country, city, sid, ttl) et il la résout en une sortie.
Vous gérez : l’unité d’identité (ce qui obtient sa propre IP, et pour combien de temps), le contrôle de concurrence (à quel point vous poussez chaque cible), la politique de réessai (ce qui arrive en cas d’échec), et l’observabilité.
L’erreur d’architecture la plus courante est de construire une couche de rotation de proxys qui duplique le gateway. Si vous vous surprenez à maintenir une table de santé d’IP, arrêtez, c’est le travail du fournisseur, et votre version a une vue du pool moins bonne que la sienne. Votre travail commence au niveau de la politique de requêtes.
Couche 1 : architecture de rotation, choisir l’unité d’identité
C’est la décision de conception à laquelle tout le reste est suspendu. La question n’est pas « à quelle fréquence dois-je tourner ? » mais « qu’est-ce qu’une identité, et quel travail lui appartient ? »
Les deux primitives :
- Rotation par requête (omettre
sid) : chaque requête obtient une IP de sortie fraîche. Diversité d’IP maximale, zéro réutilisation de connexions, idéal pour de grands ensembles de récupérations indépendantes où aucune requête ne dépend de la précédente. - Session sticky (
sid+ttl) : toutes les requêtes portant cet id de session partagent une IP de sortie jusqu’à expiration du TTL. Nécessaire quand un flux doit ressembler à un utilisateur, et c’est aussi ce qui rend les connexions poolées réutilisables (voir sticky vs rotatif).
La règle de conception : tournez à la frontière d’une unité logique de travail, pas arbitrairement. Une unité de travail est ce qui doit être cohérent en interne, les avis paginés d’un produit, un flux de recherche, la session d’un compte. Mappez-le proprement :
un worker = une unité de travail = un sid = une IP de sortie (pour son TTL)Dérivez l’id de session de l’unité de travail plutôt qu’aléatoirement, pour que le comportement soit reproductible et que vous puissiez décider délibérément si un réessai garde ou change d’identité :
def session_id(job_id: str, attempt: int = 0) -> str: # Même job → même identité. Incrémentez `attempt` pour obtenir délibérément une nouvelle IP. return f"{job_id}-a{attempt}"
def proxy_for(job_id, attempt=0, country="us", ttl=600): sid = session_id(job_id, attempt) user = f"{USER}-country-{country}-sid-{sid}-ttl-{ttl}" url = f"http://{user}:{PASS}@p.shifter.io:443" return {"http": url, "https": url}Ce seul paramètre attempt fait un vrai travail d’architecture : il fait de « réessayer sur une identité différente » une opération de première classe et intentionnelle plutôt qu’un accident.
Couche 2 : architecture de concurrence, et pourquoi elle est par hôte
La contrainte sur la concurrence n’est presque jamais votre forfait proxy (les connexions concurrentes ne sont typiquement pas la limite). C’est ce que la cible tolère. Ce qui signifie qu’une limite globale unique de concurrence a la mauvaise forme : elle laisse une cible permissive affamer une cible fragile, et laisse une fragile brider tout votre pipeline.
Limitez par hôte, pas globalement :
import asynciofrom collections import defaultdict
# Un sémaphore par hôte cible, ajusté à ce que cet hôte tolère_limits = {"tough-site.example": 4, "open-site.example": 32}_sems = defaultdict(lambda: asyncio.Semaphore(8)) # défaut raisonnablefor host, n in _limits.items(): _sems[host] = asyncio.Semaphore(n)
async def fetch(client, url, host): async with _sems[host]: # backpressure appliquée par hôte return await client.get(url, timeout=30)Deux propriétés que cela vous achète. Isolation : une cible lente ou hostile ne peut pas consommer tous les workers. Réglabilité : vous pouvez monter la concurrence sur les cibles qui s’en fichent et la baisser sur celle qui bloque, indépendamment.
Et résistez à l’envie de monter les chiffres. Au-delà de la tolérance d’une cible, le parallélisme supplémentaire n’achète pas du débit, il achète des blocages, et les blocages vous coûtent plus en réessais que la concurrence n’a jamais gagné (réduire la latence couvre cet arbitrage).
Couche 3 : architecture de réessais, la partie décisive
La plupart des pipelines échouent ici. Un naïf for attempt in range(3): retry() transforme un mauvais après-midi en tempête de réessais qui amplifie les blocages et brûle la bande passante. Une vraie couche de réessais fait quatre choses.
1. Classer avant de réessayer. Tous les échecs ne se valent pas, et la réponse diffère pour chacun :
| Échec | Exemple | Bonne réponse |
|---|---|---|
| Transport | timeout, connexion réinitialisée | Réessayer, la même identité convient |
| Rate limit | 429 | Reculer fort, ralentir tout l’hôte |
| Blocage | 403, CAPTCHA, blocage doux avec 200 | Réessayer sur une nouvelle identité, ne pas réutiliser l’IP |
| Permanent | 404, 400 | Ne pas réessayer, enregistrer et passer |
Notez le blocage doux : un 200 avec une page de CAPTCHA est un échec. Si vous classez sur le seul code de statut, vous compterez des blocages comme des succès et votre pipeline se remplira silencieusement de déchets (pourquoi les scrapers se font bloquer).
2. Reculer avec du jitter. Le backoff exponentiel sans jitter synchronise vos workers en une horde qui frappe la cible par vagues. Ajoutez toujours de l’aléatoire.
3. Changer d’identité sur les blocages. Réessayer un blocage sur la même IP sticky, c’est juste redemander le même refus. Incrémentez le compteur de tentatives pour que l’id de session change et que le gateway vous donne une sortie différente.
import random, asyncio
async def fetch_with_policy(client, url, job_id, host, max_attempts=4): for attempt in range(max_attempts): proxies = proxy_for(job_id, attempt=attempt) # nouvelle identité par tentative try: r = await client.get(url, proxies=proxies, timeout=30) kind = classify(r) # ok | ratelimit | block | permanent if kind == "ok": return r if kind == "permanent": return None # ne pas gaspiller de tentatives if kind == "ratelimit": await slow_down(host) # élargir le rythme de l'hôte except (asyncio.TimeoutError, ConnectionError): pass # transport : réessayer backoff = (2 ** attempt) + random.uniform(0, 1) # jitter, toujours await asyncio.sleep(backoff) await dead_letter(job_id, url) # le garer, ne pas bloquer le pipeline return None4. Abandonner élégamment. Après N tentatives, garez l’élément dans une file de dead-letter et passez. Les réessais illimités ne sauvent pas un job ; ils convertissent un échec en charge soutenue contre une cible qui vous refuse déjà.
Deux motifs de plus à construire une fois à l’échelle : un circuit breaker par hôte (si le taux de réussite s’effondre, cessez d’envoyer un moment plutôt que de moudre), et des éléments de travail idempotents, pour qu’un réessai soit toujours sûr à exécuter.
La forme de référence
Assemblé, le pipeline ressemble à ceci :
file de travail ↓limiteur par hôte (sémaphore + rythme) ↓worker → id de session (sid) → gateway → cible ↓classer la réponse ├── ok → valider le contenu → émettre ├── ratelimit → ralentir l'hôte → backoff + remise en file ├── blocage → nouvelle identité → backoff + remise en file └── permanent → enregistrer, jeter ↓ (après N tentatives)file de dead-letterChaque flèche est une décision que vous prendriez sinon implicitement et mal. Rendue explicite, le système se dégrade élégamment au lieu de s’effondrer.
Observabilité : vous ne pouvez pas opérer cela à l’aveugle
Instrumentez au minimum : taux de réussite par hôte (avec validation de contenu, pas les codes de statut), latence p50/p95, taux de réessai et répartition par type d’échec, et octets par enregistrement réussi. Ces quatre vous disent où agir, un taux de réessai qui monte sur un hôte signifie resserrer la concurrence de cet hôte ; des octets-par-enregistrement qui montent signifient que vous tirez du payload que vous ne parsez pas (réduire les coûts de bande passante). Les méthodes de mesure sont dans comment tester la vitesse, le taux de réussite et la précision de localisation d’un proxy.
Anti-motifs à éviter
- Construire un rotateur d’IP par-dessus un gateway. Redondant, et moins bien informé que la répartition du fournisseur.
- Une limite globale de concurrence. Couple des cibles sans rapport ; limitez par hôte.
- Réessayer les blocages sur la même IP sticky. Même IP, même réponse.
- Backoff sans jitter. Synchronise les workers en vagues.
- Réessais illimités. Amplifient les blocages, brûlent la bande passante, ne retardent que l’inévitable.
- Traiter 200 comme un succès. Les blocages doux sont des 200 ; validez le contenu ou vos données pourrissent silencieusement.
- Tourner en milieu de flux. Changer d’IP dans une session à plusieurs étapes est un signal de détection, tournez aux frontières d’unité.
FAQ
Dois-je construire moi-même la répartition de charge des proxys ? Pas au niveau IP. Un gateway attribue les sorties du pool côté serveur, donc un rotateur d’IP ou une table de santé de votre côté est redondant. Architecturez la couche au-dessus : unités d’identité, concurrence par hôte, et politique de réessai.
Combien de requêtes concurrentes devrais-je lancer ? Cela dépend de la cible, pas de votre forfait. Posez une limite par hôte selon ce que cet hôte tolère, et réglez chacune indépendamment. Au-delà de ce point, plus de parallélisme produit des blocages, pas du débit.
Un réessai devrait-il utiliser la même IP ou une nouvelle ? Cela dépend de l’échec. Les erreurs de transport (timeouts) peuvent réessayer sur la même identité. Les blocages et CAPTCHA devraient réessayer sur une nouvelle identité, en changeant l’id de session, puisque c’est l’IP qui a été refusée.
Comment empêcher les réessais d’aggraver le blocage ? Classez les échecs, reculez exponentiellement avec du jitter, plafonnez les tentatives, envoyez en dead-letter ce qui n’aboutira pas, et ajoutez un circuit breaker par hôte. Des réessais illimités et non classés, c’est ainsi qu’un petit problème de blocage en devient un grand.
Que devrais-je surveiller dans un pipeline de proxys ? Taux de réussite par hôte (avec validation de contenu), latence p50/p95, taux de réessai avec répartition par type d’échec, et octets par enregistrement réussi. Ces quatre font remonter presque tout problème qui vaille la peine d’être corrigé.
En résumé
Sur un gateway, répartir la charge du pool n’est pas votre problème, et prétendre que si conduit les équipes à construire la mauvaise chose. L’architecture qui détermine vraiment si un pipeline de collecte scale vit dans trois décisions : ce qui constitue une identité et quel travail lui appartient, quelle concurrence chaque cible individuelle tolère, et comment les échecs sont classés, temporisés, ré-identifiés, et finalement abandonnés. Réussissez-les, instrumentez-les, et le pipeline se dégrade élégamment sous pression au lieu de tomber.
Le gateway fait le reste. Si vous construisez cela, le gateway résidentiel vous donne rotation et sessions sticky via le nom d’utilisateur, pour que votre architecture reste dans votre code, pas dans une couche de gestion de proxys, et la qualité du pool détermine à quelle fréquence ce chemin de réessai est seulement sollicité (réputation d’IP). Pour les considérations à grande échelle, voir le meilleur réseau de proxys résidentiels pour le scraping à grande échelle, et la page tarifs propose les forfaits au Go pour tester la conception contre votre propre charge de travail.