DevOps và Mở Rộng

Cân bằng tải cho worker giải CAPTCHA với CaptchaAI

Một worker giải CAPTCHA chỉ xử lý được vài request cùng lúc — vượt ngưỡng đó, hàng đợi phình ra và pipeline crawl nghẽn lại. Giải pháp chuẩn: đặt nhiều worker sau một load balancer để NGINX/HAProxy phân phối request /solve, tự loại worker lỗi, và cho phép thêm worker mới không cần sửa code gọi API.

Ví dụ thực tế: một đội automation tại TP.HCM theo dõi giá trên Shopee, Lazada và Tiki mỗi giờ phải chạy song song hàng chục worker gửi request tới in.php/res.php của CaptchaAI. Không có load balancer, một worker treo vì quá tải sẽ nghẽn cả đợt crawl kế tiếp; với least connections, request tự dồn sang worker còn rảnh.

Sơ đồ kiến trúc tổng quan

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

Scraper không gọi thẳng CaptchaAI — mọi request đi qua load balancer trước.

Cấu hình NGINX cho worker giải CAPTCHA

Ba biến thể upstream dưới đây khác nhau ở chiến lược định tuyến.

Round-robin (mặc định)

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

proxy_read_timeout 300s trên route /solve là bắt buộc — vượt xa timeout mặc định 60s của NGINX.

Least connections (phù hợp hơn cho việc giải 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;
}

weight=2 cho worker mạnh hơn nhận gấp đôi lưu lượng; max_fails/fail_timeout tự loại worker lỗi trong 30 giây.

Với worker dự phòng (backup)

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
}

Worker đánh dấu backup chỉ nhận request khi cả hai worker chính down — giữ dự phòng chi phí thấp thay vì chạy đủ 3 worker song song suốt ngày.

Chọn chiến lược định tuyến phù hợp

Chiến lược Cách hoạt động Phù hợp nhất với
Round-robin Xoay tuần tự qua từng worker Worker có năng lực đồng đều
Least connections Định tuyến tới worker đang ít kết nối nhất Giải CAPTCHA (thời lượng tác vụ dao động mạnh)
Weighted Phân phối tỷ lệ thuận với trọng số cấu hình Cụm worker có năng lực không đồng đều
IP Hash Cùng một client luôn về cùng một worker Cần duy trì quan hệ phiên (session affinity)
Random Chọn worker ngẫu nhiên Tải phân bố đều, không cần logic phức tạp

Khuyến nghị: dùng least connections. Thời lượng một tác vụ dao động 5 giây–120 giây tùy loại CAPTCHA, nên round-robin dồn nhiều tác vụ chậm vào cùng một worker và tạo tải không đồng đều.

  • Round-robin: worker đồng nhất, thời gian giải nằm trong dải hẹp (chỉ một loại CAPTCHA).
  • Least connections: thời lượng giải dao động mạnh — mặc định an toàn cho hầu hết cụm worker CaptchaAI.
  • IP Hash/backup: chỉ cần khi phải cách ly lỗi hoặc phiên nhạy cảm; request giải CAPTCHA vốn không trạng thái nên hiếm khi cần.

Máy chủ API cho worker (Python và Node.js)

Hai máy chủ dưới đây expose cùng route /solve/health.

Python (Flask)

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)

active_tasks giới hạn request đồng thời; vượt max_concurrent worker trả 503 để load balancer chuyển hướng request khác.

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

Logic giống bản Python, chỉ khác async/await thay vì blocking I/O.

Cân bằng tải phía client (khi chưa cần NGINX)

Chưa muốn dựng load balancer riêng? Tự định tuyến ngay trong code 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"})

Client tự theo dõi worker "healthy" và dồn task sang worker ít việc nhất — logic tương tự least_conn của NGINX nhưng chạy ngay trong code, không cần thêm hạ tầng. Đủ dùng cho 2-4 worker; nhiều hơn thì nên chuyển sang NGINX/HAProxy.

Khắc phục sự cố thường gặp

  • 502 Bad Gateway — worker crash hoặc chưa khởi động xong; kiểm tra log worker và xác minh port binding.
  • Tải phân bố không đều — round-robin với tác vụ có thời lượng dao động; chuyển sang least connections.
  • Health check dương tính giả — health check trả 200 dù worker đã đầy tải; đưa load_pct vào response /health.
  • Timeout kết nốiproxy_read_timeout quá ngắn; đặt tối thiểu 300s cho route /solve.

Câu hỏi thường gặp

Cần tối thiểu bao nhiêu worker mới nên dùng load balancer?

Với 2-4 worker, cân bằng tải phía client là đủ. Từ 5 worker trở lên, hoặc khi cần SSL termination và health check tập trung, chuyển sang NGINX/HAProxy sẽ dễ vận hành hơn.

CaptchaAI tính phí theo thread hay theo số worker?

Theo thread — số CAPTCHA xử lý đồng thời trên toàn tài khoản, không theo số worker hay mỗi lần giải. Gói BASIC ($15/tháng, 5 thread) cho tối đa 5 request cùng lúc dù chạy 2 hay 10 worker; số worker chỉ quyết định cách phân phối.

Có nên dùng sticky session cho worker giải CAPTCHA?

  • Không cần thiết: request giải CAPTCHA không trạng thái, mọi ngữ cảnh (sitekey, pageurl, proxy) đã nằm sẵn trong request.
  • Sticky session chỉ ép một client về một worker cố định, gây lệch tải mà không đổi lại lợi ích nào.

Gặp 502/504 lúc tải cao thì kiểm tra gì trước?

Xem proxy_read_timeout đã đủ (300s là mức an toàn) chưa, rồi kiểm tra /health có phản ánh đúng tải không — nhiều lúc worker "healthy" trên giấy tờ nhưng đã đầy max_concurrent.

Bài viết liên quan và bước tiếp theo

Xem thêm xử lý lỗi callback CaptchaAI, mẫu Puppeteer nâng cao Node.jskiến trúc giải CAPTCHA khối lượng lớn.

Sẵn sàng scale throughput? Lấy API key CaptchaAI, triển khai worker theo mẫu trên, rồi đọc thêm failover có tính sẵn sàng cao, kiến trúc đa vùngchọn kích thước connection pool.

Os comentários estão desativados para este artigo.