DevOps والتوسع

CaptchaAI خلف موازن التحميل: أنماط بنية العمّال

يتوسّع CaptchaAI أفقيًا عندما توزّع طلبات الحل على عدّة عمّال (workers) خلف موازن تحميل، بدل تكديسها على عملية واحدة تتحوّل سريعًا إلى عنق زجاجة تحت الضغط. هذا الدليل التشغيلي يجمع أنماط البنية الجاهزة للإنتاج: إعداد NGINX، وخادم العامل بلغتَي Python وJavaScript، واختيار استراتيجية التوجيه المناسبة، وتوزيع الحمل من جانب العميل حين يتعذّر تشغيل موازن خارجي.

كيف تتدفق الطلبات عبر البنية

يبدأ المسار من سكربتات الاستخراج، ثم يمرّ عبر موازن التحميل الذي يوزّع كل طلب على أحد العمّال، وينفّذ كل عامل الاتصال بـ CaptchaAI API ويعيد التوكن المحلول. تمنحك هذه البنية ثلاث فوائد مباشرة:

  • إنتاجية أعلى: طلبات متزامنة أكثر موزّعة على عدة عمّال بدل عملية واحدة تختنق.
  • تبديل تلقائي: استمرار الخدمة عند تعطّل أحد العمّال دون توقّف كامل.
  • توسّع أفقي: إضافة عمّال جدد وقت الحاجة دون لمس بقية النظام.
[Scraper 1] ──┐                      ┌── [Worker 1] ──→ CaptchaAI API
[Scraper 2] ──┤── [Load Balancer] ──┤── [Worker 2] ──→ CaptchaAI API
[Scraper 3] ──┘                      └── [Worker 3] ──→ CaptchaAI API

اختر استراتيجية التوجيه أولًا

قبل كتابة أي إعداد، حدّد كيف سيوزّع الموازن الطلبات. تختلف الاستراتيجيات في مدى مراعاتها لتفاوت مدة مهام الحل:

الاستراتيجية كيف تعمل الأنسب لـ
التوزيع الدوري (Round-Robin) تناوب متسلسل على العمّال العمّال المتساوون في القدرة
أقل الاتصالات (Least Connections) التوجيه إلى الأقل انشغالًا حل CAPTCHA (مدة مهمة متغيّرة)
الموزون (Weighted) توزيع متناسب مع الوزن العمّال المختلطو القدرات
تجزئة IP (IP Hash) العميل نفسه ← العامل نفسه عند الحاجة لتقارب الجلسة
العشوائي (Random) اختيار عشوائي حمل بسيط موزّع بالتساوي

التوصية: استخدم أقل الاتصالات لحل CAPTCHA. تتفاوت مدة المهام بين 5 و120 ثانية، فالتوزيع الدوري يخلق حملًا غير متساوٍ يترك بعض العمّال مثقلين وآخرين خاملين.

مثال عملي: منصّة لمراقبة الأسعار في الخليج تتضاعف طلباتها في موسم الجمعة البيضاء. بدل عملية واحدة تختنق تحت الذروة، توزّع الحمل على خمسة عمّال خلف NGINX؛ ومع خطة ADVANCE ($90 شهريًا، 50 thread متزامنًا) يتوفّر رصيد تزامن كافٍ يتقاسمه العمّال لاستيعاب موجة الطلبات دون تجاوز حدود الخطة.

ملاحظة: عدد الـ threads في خطتك هو سقف التزامن الفعلي على مستوى الحساب؛ موازن التحميل لا يرفع هذا السقف، بل يوزّع استخدامه على العمّال بكفاءة أعلى ويحميك من نقاط الاختناق.

إعداد NGINX موازنًا للحمل

التوزيع الدوري (Round-Robin) — الافتراضي

التوزيع الدوري هو السلوك الافتراضي في NGINX: يمرّر الطلبات بالتناوب على العمّال بالترتيب. يصلح حين تتساوى قدرات العمّال، لكنه لا يراعي أنّ مهام الحل تختلف في مدتها.

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

أقل الاتصالات (Least Connections) — الأنسب لحل CAPTCHA

يوجّه هذا الوضع كل طلب جديد إلى العامل صاحب أقل عدد اتصالات نشطة، وهو الخيار الأمثل هنا لأنّ مدة المهمة متفاوتة. لاحظ الوزن (weight=2) لعامل أعلى قدرة، وفحوص الصحة (max_fails / fail_timeout) التي تُخرج العامل المتعثّر من الدوران مؤقتًا.

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

عمّال احتياطيون للطوارئ (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)

كل عامل هو خادم HTTP بسيط يستقبل مهمة الحل، ويتتبّع عدد المهام النشطة، ويعرض نقطة نهاية /health يقرأها موازن التحميل. عند بلوغ الحد الأقصى للتزامن يردّ العامل بالرمز 503 (WORKER_AT_CAPACITY) ليحوّل الموازن الطلب تلقائيًا إلى عامل آخر.

Python مع Flask

نسخة Flask تستخدم قفل خيوط (lock) لعدّ المهام بأمان، وتستطلع نتيجة الحل من res.php كل خمس ثوانٍ حتى يجهز التوكن.

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

المنطق نفسه في Express: حدّ تزامن، ونقطة /health تعكس نسبة الحمل، واستطلاع دوري للنتيجة كل خمس ثوانٍ.

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

توزيع الحمل من جانب العميل

حين يتعذّر تشغيل موازن خارجي، انقل منطق التوجيه إلى العميل نفسه: يحتفظ بقائمة العمّال وحالتهم، ويختار الأقل انشغالًا، ويعيد المحاولة تلقائيًا على عامل آخر عند الفشل أو عند الرد بالرمز 503.

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

معالجة الأعطال الشائعة

المشكلة السبب المحتمل المعالجة
خطأ 502 Bad Gateway العامل متوقّف أو لم يبدأ راجع سجلّات العامل وتأكّد من ربط المنفذ (port)
توزيع غير متوازن للحمل التوزيع الدوري مع مهام متفاوتة المدة انتقل إلى أقل الاتصالات (least_conn)
فحص الصحة يمرّ رغم امتلاء العامل الفحص لا يقرأ نسبة الحمل الفعلية ضمّن نسبة الحمل في استجابة /health كما في المثال
انقطاع الاتصال قبل عودة النتيجة قيمة proxy_read_timeout قصيرة جدًا ارفعها إلى 300s أو أكثر لعمليات الحل الطويلة

أسئلة شائعة

هل يزيد موازن التحميل من استهلاك الـ threads في خطتي؟

لا. عدد الـ threads هو سقف التزامن على مستوى الحساب في CaptchaAI، لا على مستوى كل عامل. توزيع الطلبات على عدة عمّال يحسّن الإنتاجية والاعتمادية، لكن مجموع الطلبات المتزامنة يظل محكومًا برصيد خطتك.

كم عاملًا أحتاج لتشغيل مستقر؟

ابدأ بعاملَين أو ثلاثة خلف توجيه من جانب العميل، وانتقل إلى موازن مخصّص مثل NGINX أو HAProxy عند تجاوز خمسة عمّال أو عند الحاجة إلى إنهاء SSL وفحوص صحة مركزية.

لماذا أضبط proxy_read_timeout على 300 ثانية؟

لأنّ حل CAPTCHA قد يستغرق من 5 إلى 120 ثانية أثناء الاستطلاع الدوري للنتيجة. مهلة قراءة قصيرة ستقطع الاتصال قبل عودة التوكن وتظهر على شكل أخطاء زائفة.

ماذا يحدث إذا تعطّل عامل أثناء معالجة مهمة؟

يستبعده الموازن بعد فشل فحص الصحة (max_fails / fail_timeout) ويحوّل الطلبات إلى العمّال الأصحّاء، ومع منطق العميل تُعاد المحاولة تلقائيًا على عامل آخر. صمّم مهامك لتكون قابلة لإعادة التنفيذ دون أثر جانبي (idempotent).


الخطوات التالية

أدلة ذات صلة

التعليقات غير مفعّلة لهذا المقال.