DevOps & Mise à l'Échelle

CaptchaAI derrière un équilibreur de charge : modèles d'architecture

Lorsque votre infrastructure de scraping envoie des milliers de requêtes de résolution CAPTCHA, un seul processus de travail crée des goulots d'étranglement. Un équilibreur de charge répartit les requêtes entre plusieurs nœuds de calcul, améliorant ainsi le débit, permettant le basculement et vous permettant d'évoluer horizontalement.

Présentation de l'architecture

[Scraper 1] ──┐                      ┌── [Worker 1] ──→ CaptchaAI API
[Scraper 2] ──┤── [Load Balancer] ──┤── [Worker 2] ──→ CaptchaAI API
[Scraper 3] ──┘                      └── [Worker 3] ──→ CaptchaAI API

Configuration NGINX

Round-Robin (par défaut)

upstream captcha_workers {
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
}

server {
    listen 80;
    server_name captcha.internal;

    location /solve {
        proxy_pass http://captcha_workers;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_connect_timeout 10s;
        proxy_read_timeout 300s;  # CAPTCHA solving can take minutes
    }

    location /health {
        proxy_pass http://captcha_workers;
        proxy_connect_timeout 5s;
        proxy_read_timeout 5s;
    }
}

Moins de connexions (meilleur pour la résolution de CAPTCHA)

upstream captcha_workers {
    least_conn;  # Route to worker with fewest active connections
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;
    server 10.0.1.12:8080 weight=2;  # Higher capacity worker

    # Health checks
    server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
}

Avec des travailleurs de secours

upstream captcha_workers {
    least_conn;
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;
    server 10.0.1.12:8080 backup;  # Only used when others are down
}

Serveur API de travail

Python (flacon)

import os
import time
import threading
import requests
from flask import Flask, request, jsonify

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
app = Flask(__name__)

# Track active tasks for load reporting
active_tasks = 0
tasks_lock = threading.Lock()
max_concurrent = int(os.environ.get("MAX_CONCURRENT", "20"))


@app.route("/solve", methods=["POST"])
def solve():
    global active_tasks
    with tasks_lock:
        if active_tasks >= max_concurrent:
            return jsonify({"error": "WORKER_AT_CAPACITY"}), 503
        active_tasks += 1

    try:
        data = request.json
        result = solve_captcha(data)
        return jsonify(result)
    finally:
        with tasks_lock:
            active_tasks -= 1


@app.route("/health")
def health():
    with tasks_lock:
        load = active_tasks / max_concurrent
    return jsonify({
        "status": "healthy" if load < 0.9 else "overloaded",
        "active_tasks": active_tasks,
        "max_concurrent": max_concurrent,
        "load_pct": round(load * 100, 1)
    }), 200 if load < 0.9 else 503


def solve_captcha(data):
    session = requests.Session()
    payload = {
        "key": API_KEY,
        "method": data.get("method", "userrecaptcha"),
        "googlekey": data.get("sitekey"),
        "pageurl": data.get("pageurl"),
        "json": 1
    }

    if data.get("proxy"):
        payload["proxy"] = data["proxy"]
        payload["proxytype"] = data.get("proxytype", "HTTP")

    resp = session.post("https://ocr.captchaai.com/in.php", data=payload)
    result = resp.json()
    if result.get("status") != 1:
        return {"error": result.get("request")}

    captcha_id = result["request"]
    for _ in range(60):
        time.sleep(5)
        poll = session.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": captcha_id, "json": 1
        }).json()
        if poll.get("status") == 1:
            return {"solution": poll["request"], "captcha_id": captcha_id}
        if poll.get("request") != "CAPCHA_NOT_READY":
            return {"error": poll.get("request")}

    return {"error": "TIMEOUT"}


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080, threaded=True)

JavaScript (Express)

const express = require("express");
const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;
const MAX_CONCURRENT = parseInt(process.env.MAX_CONCURRENT || "20", 10);
const PORT = parseInt(process.env.PORT || "8080", 10);

let activeTasks = 0;
const app = express();
app.use(express.json());

app.post("/solve", async (req, res) => {
  if (activeTasks >= MAX_CONCURRENT) {
    return res.status(503).json({ error: "WORKER_AT_CAPACITY" });
  }
  activeTasks++;

  try {
    const result = await solveCaptcha(req.body);
    res.json(result);
  } catch (err) {
    res.status(500).json({ error: err.message });
  } finally {
    activeTasks--;
  }
});

app.get("/health", (req, res) => {
  const load = activeTasks / MAX_CONCURRENT;
  const status = load < 0.9 ? "healthy" : "overloaded";
  res
    .status(load < 0.9 ? 200 : 503)
    .json({ status, activeTasks, maxConcurrent: MAX_CONCURRENT, loadPct: Math.round(load * 100) });
});

async function solveCaptcha(data) {
  const submitResp = await axios.post("https://ocr.captchaai.com/in.php", null, {
    params: {
      key: API_KEY,
      method: data.method || "userrecaptcha",
      googlekey: data.sitekey,
      pageurl: data.pageurl,
      json: 1,
    },
  });

  if (submitResp.data.status !== 1) {
    return { error: submitResp.data.request };
  }

  const captchaId = submitResp.data.request;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const pollResp = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
    });

    if (pollResp.data.status === 1) {
      return { solution: pollResp.data.request, captchaId };
    }
    if (pollResp.data.request !== "CAPCHA_NOT_READY") {
      return { error: pollResp.data.request };
    }
  }
  return { error: "TIMEOUT" };
}

app.listen(PORT, () => console.log(`Worker listening on port ${PORT}`));

Comparaison des stratégies de routage

Stratégie Comment ça marche Idéal pour
Tournoi à la ronde Rotation séquentielle Travailleurs à capacité égale
Moins de connexions Itinéraire vers le moins chargé Résolution de CAPTCHA (durée de tâche variable)
Pondéré Proportionnel au poids Travailleurs à capacité mixte
Hachage IP Même client – même travailleur Affinité de session requise
Aléatoire Sélection aléatoire Charge simple et uniformément répartie

Recommandation : Utilisez le moins de connexions pour la résolution de CAPTCHA. Les durées des tâches varient (de 5 à 120 s), donc le round-robin crée une charge inégale.

Équilibrage de charge côté client

Lorsque vous ne pouvez pas utiliser d'équilibreur de charge externe, implémentez le routage dans le client :

import random
import requests

class ClientLoadBalancer:
    def __init__(self, workers):
        self.workers = [
            {"url": url, "healthy": True, "active": 0}
            for url in workers
        ]

    def get_worker(self):
        healthy = [w for w in self.workers if w["healthy"]]
        if not healthy:
            raise Exception("No healthy workers")
        return min(healthy, key=lambda w: w["active"])

    def solve(self, task):
        worker = self.get_worker()
        worker["active"] += 1
        try:
            resp = requests.post(
                f"{worker['url']}/solve",
                json=task,
                timeout=300
            )
            if resp.status_code == 503:
                worker["healthy"] = False
                return self.solve(task)  # Retry on another worker
            return resp.json()
        except requests.RequestException:
            worker["healthy"] = False
            return self.solve(task)
        finally:
            worker["active"] -= 1


lb = ClientLoadBalancer([
    "http://10.0.1.10:8080",
    "http://10.0.1.11:8080",
    "http://10.0.1.12:8080"
])
result = lb.solve({"sitekey": "6Le-wvkS...", "pageurl": "https://example.com"})

Dépannage

Problème Parce que Corriger
502 Mauvaise passerelle Le travailleur s'est écrasé ou n'a pas démarré Vérifiez les journaux des travailleurs ; vérifier la liaison du port
Répartition inégale de la charge Round-robin avec des durées de tâches variables Passer au moins de connexions
Bilan de santé faux positif Vérifiez les passes mais le travailleur est à pleine capacité Inclure le pourcentage de charge dans la réponse de santé
Délai de connexion proxy_read_timeout trop court Réglé sur 300 s+ pour la résolution de CAPTCHA

FAQ

Ai-je besoin d’un équilibreur de charge pour 2-3 travailleurs ?

L'équilibrage de charge côté client fonctionne bien pour les petites configurations. Utilisez un équilibreur de charge dédié (NGINX, HAProxy) lorsque vous avez plus de 5 travailleurs ou que vous avez besoin de fonctionnalités telles que la terminaison SSL et les contrôles de santé.

Dois-je utiliser des sessions collantes ?

Non. Les requêtes de résolution CAPTCHA sont sans état : n'importe quel travailleur peut gérer n'importe quelle tâche. Des sessions persistantes créeraient une répartition inégale de la charge.

Comment gérer les travailleurs dans différentes régions ?

Utilisez un équilibreur de charge global (AWS Global Accelerator, Cloudflare Load Balancing) qui achemine vers la région saine la plus proche. Chaque région exécute son propre équilibreur de charge local pour les travailleurs de cette région.

Articles connexes

  • Modèles de gestion des erreurs de rappel Captchaai
  • Nodejs Puppeteer Captchaai Modèles avancés
  • Captcha résolvant des modèles d'architecture à volume élevé

Prochaines étapes

Augmentez votre débit de résolution de CAPTCHA –récupérez votre clé API CaptchaAIet déployer derrière un équilibreur de charge.

Guides associés :

Les commentaires sont désactivés pour cet article.