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
- Patrones de manejo de errores en callbacks de CaptchaAI
- Patrones avanzados de Node.js Puppeteer con CaptchaAI
- Arquitectura de resolución CAPTCHA en alto volumen
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:
- Alta disponibilidad y failover
- Arquitectura multirregional