DevOps y Escalado

CaptchaAI detrás de un load balancer: patrones de arquitectura

Cuando tu infraestructura de scraping envía miles de solicitudes de resolución CAPTCHA, un solo worker crea cuellos de botella. Un load balancer distribuye las solicitudes entre varios workers, mejorando el throughput, habilitando failover y permitiendo escalar horizontalmente.

Descripción general de la arquitectura

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

Elegir el algoritmo de balanceo

Algoritmo Úselo cuando Riesgo principal
Round-robin Todos los workers tienen capacidad similar y latencias parecidas Un worker lento sigue recibiendo tráfico al mismo ritmo
least_conn Los tiempos de resolución varían mucho entre tareas Requiere health checks fiables para no favorecer workers degradados
backup Necesita capacidad de emergencia sin usarla a diario El worker de respaldo puede quedarse frío si nunca recibe tráfico
Sticky / afinidad El worker conserva estado local o sesión de navegador Puede desequilibrar la carga si no expira correctamente

Configuración NGINX

Round-Robin (predeterminado)

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;
    }
}

Menos conexiones (mejor para resolver 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;
}

Con trabajadores de respaldo

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
}

Servidor API de trabajador

Pitón (frasco)

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 (rápido)

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}`));

Comparación de estrategias de enrutamiento

Estrategia Cómo funciona Mejor para
Todos contra todos Rotación secuencial Trabajadores de igual capacidad
Menos conexiones Ruta al menos cargado Resolución de CAPTCHA (duración variable de la tarea)
ponderado Proporcional al peso Trabajadores de capacidad mixta
Hash de IP Mismo cliente -> mismo trabajador Se necesita afinidad de sesión
Aleatorio Selección aleatoria Carga simple y distribuida uniformemente

Recomendación: Utilice menos conexiones para resolver CAPTCHA. La duración de las tareas varía (de 5 a 120 segundos), por lo que la operación por turnos crea una carga desigual.

Equilibrio de carga del lado del cliente

Cuando no pueda utilizar un equilibrador de carga externo, implemente el enrutamiento en el cliente:

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"})

Solución de problemas

Problema causa Solución
502 Puerta de enlace incorrecta El trabajador se estrelló o no comenzó Verificar los registros de los trabajadores; verificar el enlace del puerto
Distribución de carga desigual Round-robin con duraciones de tareas variables Cambiar a conexiones mínimas
Control de salud falso positivo Verifique los pases pero el trabajador está al límite de su capacidad Incluir porcentaje de carga en la respuesta de salud
Tiempo de espera de conexión proxy_read_timeout demasiado corto Configurado en 300+ para resolver CAPTCHA

Preguntas frecuentes

¿Necesito un equilibrador de carga para 2 o 3 trabajadores?

El equilibrio de carga del lado del cliente funciona bien para configuraciones pequeñas. Utilice un equilibrador de carga dedicado (NGINX, HAProxy) cuando tenga más de 5 trabajadores o necesite funciones como terminación SSL y comprobaciones de estado.

¿Debería utilizar sesiones adhesivas?

No. Las solicitudes de resolución CAPTCHA no tienen estado: cualquier worker puede atender cualquier tarea. Las sesiones adhesivas crearían una distribución desigual de la carga.

¿Cómo gestiono workers en diferentes regiones?

Usa un load balancer global (AWS Global Accelerator, Cloudflare Load Balancing) que enruté a la región sana más cercana. Cada región ejecuta su propio load balancer local para los workers de esa región.

Artículos relacionados


Escala tu resolución con CaptchaAI

Mejora tu throughput de resolución CAPTCHA. Obtén tu clave API CaptchaAI e implémentala detrás de un load balancer.

Guías relacionadas:

Los comentarios están deshabilitados para este artículo.