DevOps & Skalierung

Auto-Scaling CAPTCHA-Solver-Worker

Ein selbstskalierender Worker-Pool richtet die Zahl aktiver Worker nach drei messbaren Größen aus: Warteschlangentiefe, Auslastung und Guthaben. Steigt die Last, kommen Worker hinzu; ebbt sie ab, fahren Sie wieder herunter – und zahlen keine leerlaufenden Threads mit. Ein fest dimensionierter Pool kann das nicht: Er ist entweder überdimensioniert und teuer oder in Spitzenzeiten zu klein und wird zum Flaschenhals. Dieser Leitfaden zeigt drei Muster in Python – Thread-Pool, Prozesspool und guthabenbewusste Skalierung – und wann sich welches lohnt.


Welche Signale die Skalierung steuern

Auto-Scaling ist nur so gut wie die Signale, auf die es reagiert. Für CAPTCHA-Worker haben sich fünf Kennzahlen bewährt. Warteschlangentiefe und Auslastung treiben das Hochskalieren, Latenz und Fehlerquote decken Überlast auf, und das Guthaben ist die harte Bremse.

Signal Hochskalieren bei Herunterskalieren bei
Warteschlangentiefe > 20 ausstehende Aufgaben < 5 ausstehende Aufgaben
Worker-Auslastung > 80 % ausgelastet < 20 % ausgelastet
Lösungslatenz P95 > 60 Sekunden P95 < 20 Sekunden
Fehlerquote > 5 % (frische Worker starten) stabil < 1 %
Guthaben Guthaben < 1 $ (Skalierung stoppen)

Drei Leitplanken haben sich in der Praxis bewährt:

  • Hochskalieren nur unter zwei Bedingungen – die Warteschlange wächst und die Worker sind ausgelastet.
  • Herunterskalieren träge – erst nach anhaltend niedriger Last, sonst pendelt der Pool ständig (State Thrashing).
  • Guthaben als harte Bremse – unterhalb der Schwelle kommen keine neuen Worker hinzu.

Ein wichtiger Punkt für die Obergrenze: CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung. Ihre maximale Worker-Anzahl sollte deshalb zur Thread-Zahl Ihres Plans passen. Der BASIC-Plan (15 $/Monat, 5 Threads) trägt bis zu 5 parallele Lösungen, ADVANCE (90 $/Monat, 50 Threads) deckt bis zu 50 ab. Mehr Worker als Threads bringen keinen Durchsatz, sondern nur Wartezeit. Preise in US-Dollar.


Skalierungsstrategien im Überblick

Bevor Sie Code schreiben, lohnt die Entscheidung für ein Modell. CAPTCHA-Lösen ist überwiegend I/O-lastig – Ihre Worker warten auf die Antwort von CaptchaAI, nicht auf die CPU. Damit ist der Thread-Pool in den meisten Fällen die richtige Wahl.

Strategie Am besten für Latenz Komplexität
Thread-Pool I/O-lastige Workloads (API-Aufrufe) niedrig niedrig
Prozesspool CPU-lastige Vorverarbeitung mittel mittel
Kubernetes HPA Cloud-native Deployments höher hoch
KEDA ereignisgesteuerte Skalierung mittel mittel

Als Faustregel für die Auswahl:

  • Ein Knoten, reine API-Aufrufe → Thread-Pool.
  • Ein Knoten mit Bildvorverarbeitung → Prozesspool.
  • Mehrere Knoten → Kubernetes HPA oder KEDA.

Für einen einzelnen VPS – etwa bei Hetzner oder netcup – reicht ein Thread- oder Prozesspool. Erst wenn Sie über mehrere Knoten verteilen, spielen Kubernetes HPA oder KEDA ihre Stärken aus.


Thread-Pool dynamisch skalieren

Der Thread-basierte Ansatz passt die Anzahl der Worker-Threads innerhalb eines einzelnen Prozesses an. Ein Hintergrund-Loop prüft alle 10 Sekunden Warteschlangentiefe und Auslastung und fügt Threads hinzu oder entfernt sie – ideal für reine API-Aufrufe.

import os
import time
import threading
import requests
import json
import redis


class AutoScalingPool:
    """Dynamically scale CaptchaAI worker threads."""

    def __init__(self, api_key, redis_url="redis://localhost:6379"):
        self.api_key = api_key
        self.redis = redis.from_url(redis_url)
        self.base = "https://ocr.captchaai.com"
        self.queue_key = "captcha:tasks"
        self.results_key = "captcha:results"

        self.min_workers = 2
        self.max_workers = 20
        self.workers = []
        self.active_count = 0
        self.lock = threading.Lock()
        self.running = True

    def start(self):
        """Start the pool with minimum workers."""
        for _ in range(self.min_workers):
            self._add_worker()

        # Start scaler in background
        scaler = threading.Thread(target=self._scaling_loop, daemon=True)
        scaler.start()
        print(f"Pool started with {self.min_workers} workers")

    def _add_worker(self):
        """Add a worker thread."""
        if len(self.workers) >= self.max_workers:
            return
        t = threading.Thread(target=self._worker_loop, daemon=True)
        t.start()
        self.workers.append(t)

    def _remove_worker(self):
        """Signal one worker to stop (lazy removal)."""
        if len(self.workers) <= self.min_workers:
            return
        self.workers.pop()  # Thread will exit on next idle cycle

    def _worker_loop(self):
        """Worker loop: fetch and process tasks."""
        while self.running and threading.current_thread() in self.workers:
            result = self.redis.blpop(self.queue_key, timeout=10)
            if result is None:
                continue

            _, raw = result
            task = json.loads(raw)
            task_id = task["id"]

            with self.lock:
                self.active_count += 1

            try:
                token = self._solve(task["method"], task["params"])
                self.redis.hset(self.results_key, task_id, json.dumps({
                    "status": "success", "token": token,
                }))
            except Exception as e:
                self.redis.hset(self.results_key, task_id, json.dumps({
                    "status": "error", "error": str(e),
                }))
            finally:
                with self.lock:
                    self.active_count -= 1

    def _scaling_loop(self):
        """Periodically adjust worker count."""
        while self.running:
            time.sleep(10)

            queue_depth = self.redis.llen(self.queue_key)
            current = len(self.workers)
            utilization = (
                self.active_count / current * 100 if current > 0 else 0
            )

            # Scale up: queue growing and workers busy
            if queue_depth > 20 and utilization > 70:
                new_count = min(current + 2, self.max_workers)
                while len(self.workers) < new_count:
                    self._add_worker()
                print(f"Scaled up: {current} → {len(self.workers)} workers")

            # Scale down: queue empty and workers idle
            elif queue_depth < 5 and utilization < 20:
                target = max(current - 1, self.min_workers)
                while len(self.workers) > target:
                    self._remove_worker()
                if len(self.workers) < current:
                    print(f"Scaled down: {current} → {len(self.workers)} workers")

    def _solve(self, method, params, timeout=120):
        data = {"key": self.api_key, "method": method, "json": 1}
        data.update(params)

        resp = requests.post(
            f"{self.base}/in.php", data=data, timeout=30,
        )
        result = resp.json()

        if result.get("status") != 1:
            raise RuntimeError(result.get("request"))

        captcha_id = result["request"]
        start = time.time()

        while time.time() - start < timeout:
            time.sleep(5)
            resp = requests.get(f"{self.base}/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": captcha_id,
                "json": 1,
            }, timeout=15)
            data = resp.json()
            if data["request"] != "CAPCHA_NOT_READY":
                if data.get("status") == 1:
                    return data["request"]
                raise RuntimeError(data["request"])

        raise TimeoutError("Solve timeout")

    def stats(self):
        return {
            "workers": len(self.workers),
            "active": self.active_count,
            "queue": self.redis.llen(self.queue_key),
        }


# Usage
pool = AutoScalingPool(os.environ["CAPTCHAAI_KEY"])
pool.start()

# Monitor
while True:
    print(pool.stats())
    time.sleep(30)

So greift die Hochskalier-Regel

Der Loop skaliert nur dann hoch, wenn die Warteschlange wächst und die vorhandenen Worker ausgelastet sind – so vermeiden Sie unnötige Threads, wenn die Last kurz anzieht, aber schnell wieder abflacht. Beim Herunterskalieren entfernt _remove_worker() Threads erst, wenn Warteschlange und Auslastung gemeinsam unter die Schwelle fallen.


Prozesse statt Threads: CPU-isolierte Worker

Sobald Ihre Worker neben dem Lösen auch rechenintensive Arbeit erledigen – etwa Bildvorverarbeitung –, bremst der Python-GIL einen Thread-Pool aus. Dann skalieren Sie Prozesse statt Threads. Jeder Prozess läuft isoliert auf einem eigenen CPU-Kern.

import multiprocessing
import time
import redis
import os


class ProcessScaler:
    """Scale worker processes based on queue depth."""

    def __init__(self, worker_fn, redis_url="redis://localhost:6379"):
        self.worker_fn = worker_fn
        self.redis = redis.from_url(redis_url)
        self.processes = []
        self.min_workers = 2
        self.max_workers = 16

    def run(self, check_interval=15):
        """Run the scaler loop."""
        # Start minimum workers
        for _ in range(self.min_workers):
            self._spawn()

        while True:
            time.sleep(check_interval)
            self._cleanup_dead()

            queue_depth = self.redis.llen("captcha:tasks")
            current = len(self.processes)

            # Scale up
            if queue_depth > current * 5 and current < self.max_workers:
                to_add = min(
                    max(1, queue_depth // 10),
                    self.max_workers - current,
                )
                for _ in range(to_add):
                    self._spawn()
                print(f"Scaled up to {len(self.processes)} workers")

            # Scale down
            elif queue_depth < 3 and current > self.min_workers:
                to_remove = min(2, current - self.min_workers)
                for _ in range(to_remove):
                    p = self.processes.pop()
                    p.terminate()
                print(f"Scaled down to {len(self.processes)} workers")

    def _spawn(self):
        p = multiprocessing.Process(target=self.worker_fn)
        p.start()
        self.processes.append(p)

    def _cleanup_dead(self):
        self.processes = [p for p in self.processes if p.is_alive()]
        # Ensure minimum
        while len(self.processes) < self.min_workers:
            self._spawn()

Prozesse sauber halten

_cleanup_dead() entfernt beendete Prozesse aus der Liste und stellt sicher, dass immer mindestens die Mindestanzahl läuft. Ohne diese Bereinigung sammeln sich Zombie-Prozesse an. Beachten Sie außerdem:

  • terminate() beendet Prozesse hart – laufende Lösungen gehen verloren.
  • Der Check-Intervall von 15 Sekunden dämpft zu hektisches Skalieren.
  • Die Obergrenze von 16 Prozessen sollte zur Thread-Zahl Ihres Plans passen.

Guthaben-bewusste Skalierung

Die wichtigste Bremse ist das Guthaben. Ohne diese Prüfung skaliert Ihr Pool munter weiter, während das Konto leerläuft – und die neuen Worker liefern nur noch Fehler. Fragen Sie den Kontostand vor jedem Hochskalieren ab und stoppen Sie, sobald er unter Ihre Schwelle fällt.

def check_balance(api_key, min_balance=2.0):
    """Check if balance is sufficient for scaling."""
    resp = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": api_key,
        "action": "getbalance",
        "json": 1,
    }, timeout=15)
    balance = float(resp.json()["request"])

    if balance < min_balance:
        print(f"Balance ${balance:.2f} below ${min_balance} — halting scale-up")
        return False
    return True

Sinnvolle Schwellen richten sich nach Ihrem Verbrauch:

  • Puffer großzügig wählen – bei hohem Durchsatz eher 5 $ als 2 $, damit die Abfrageverzögerung nicht ins Minus führt.
  • Nur das Hochskalieren stoppen – laufende Worker beenden ihre Aufgaben, statt mitten im Solve abzubrechen.
  • Guthaben getrennt überwachen – ein separater Alert warnt Sie, bevor die Skalierung überhaupt greift.

Diese Prüfung binden Sie direkt in die Skalierungsschleife ein, sodass ein niedriges Guthaben das Hochskalieren blockiert, laufende Worker aber ungestört zu Ende arbeiten:

# In _scaling_loop:
if queue_depth > 20 and utilization > 70:
    if check_balance(self.api_key, min_balance=2.0):
        # Scale up
        ...
    else:
        print("Scaling paused — low balance")

Häufige Probleme beim Auto-Scaling

Die meisten Störungen im Betrieb lassen sich auf schlecht gewählte Schwellenwerte oder fehlende Bereinigung zurückführen.

Problem Ursache Lösung
Der Worker-Pool wächst endlos Die Warteschlange leert sich nie Prüfen Sie, ob die Worker Aufgaben wirklich abarbeiten
Herunterskalieren zu aggressiv Schwellenwert zu niedrig Verzögerung beim Herunterskalieren auf über 30 Sekunden anheben
Zombie-Prozesse Prozesse werden nicht bereinigt _cleanup_dead() regelmäßig aufrufen
Guthaben schwindet schnell Zu viele parallele Worker Guthabenprüfung in die Skalierungslogik einbauen

Häufige Fragen

Welcher CaptchaAI-Plan begrenzt meine parallele Worker-Anzahl?

Die Thread-Zahl Ihres Plans ist die Obergrenze. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung. BASIC (15 $/Monat) bietet 5 Threads, STANDARD (30 $/Monat) 15, ADVANCE (90 $/Monat) 50. Setzen Sie max_workers nie höher als die Thread-Zahl – zusätzliche Worker warten sonst nur.

Wann lohnt sich Kubernetes HPA gegenüber einem Thread-Pool?

Erst bei verteilten Deployments über mehrere Knoten. Auf einem einzelnen VPS – etwa bei Hetzner oder IONOS – ist ein Thread-Pool einfacher und latenzärmer. Kubernetes HPA oder KEDA lohnen sich, sobald Sie horizontal über mehrere Maschinen skalieren.

Wie stoppe ich die Skalierung bei niedrigem Guthaben?

Fragen Sie mit action=getbalance den Kontostand ab, bevor Sie hochskalieren, und setzen Sie eine Schwelle (im Beispiel 2 $). Fällt das Guthaben darunter, blockiert check_balance() das Hochskalieren. Laufende Worker beenden ihre Aufgaben trotzdem sauber.

Thread-Pool oder Prozesspool für CAPTCHA-Worker?

Threads für reine API-Aufrufe – CaptchaAI ist I/O-lastig und der Thread-Pool bleibt leichtgewichtig. Prozesse nur, wenn Sie zusätzlich rechenintensive Bildvorverarbeitung betreiben und den GIL umgehen müssen.


Verwandte Leitfäden


Skalieren Sie mit Augenmaß – sichern Sie sich Ihren CaptchaAI-API-Schlüssel und starten Sie noch heute.

Kommentare sind für diesen Artikel deaktiviert.