Ein Rolling Update tauscht die Worker einer CAPTCHA-Flotte nacheinander aus: Ein Worker nimmt keine neuen Aufgaben mehr an, arbeitet die laufenden Abfragen zu Ende, startet mit der neuen Version und muss einen Health-Check bestehen – erst dann folgt der nächste. Solange genügend Worker im Zustand RUNNING bleiben, bemerkt die Warteschlange den Versionswechsel nicht.
Der Unterschied zu einem gewöhnlichen HTTP-Service liegt in der Laufzeit einer Aufgabe: CaptchaAI löst ein reCAPTCHA v2 in unter 60 Sekunden, Cloudflare Turnstile in unter 10 Sekunden. Wird ein Worker mitten in dieser Zeitspanne hart beendet, geht nicht nur die Aufgabe verloren – das Formular dahinter läuft in ein Timeout, und der Test- oder Scraping-Lauf beginnt von vorn.
Was CAPTCHA-Worker beim Deployment anders macht
Drei Eigenschaften unterscheiden eine Löser-Flotte von einer normalen Job-Queue:
- Lange Aufgabenlaufzeit. Ein Deployment, das nach fünf Sekunden weiterzieht, ist hier zu ungeduldig. Das Drain-Fenster richtet sich nach der langsamsten Abfrage, nicht nach dem Durchschnitt.
- Kurzlebige Tokens. Ein gelöstes reCAPTCHA-Token ist nur rund zwei Minuten gültig. Ein Worker, der Tokens über einen Neustart hinweg mitnimmt, verteilt abgelaufene Werte.
- Threads als Kapazitätsgrenze. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung. Wie viele Worker Sie herausnehmen dürfen, ergibt sich aus dem Thread-Kontingent, nicht aus der CPU-Auslastung.
Kapazität planen: Threads statt Instanzen zählen
Rechnen Sie vorher aus, wie viel Durchsatz während des Updates fehlt. Beispiel: zwölf Worker mit je vier parallelen Aufgaben ergeben 48 gleichzeitige Threads – das passt in den Tarif ADVANCE (90 $/Monat, 50 Threads; Preise in US-Dollar). Nehmen Sie zwei Worker heraus, bleiben 40 Threads, also rund 83 % der Kapazität.
Als Faustregel gilt: max_unavailable bei kleinen Flotten auf 1 setzen, ab etwa zwölf Workern auf 10–25 % erhöhen. Wer mehr als die Hälfte gleichzeitig austauscht, fährt kein Rolling Update mehr, sondern einen Neustart mit Verzögerung. Stößt die Flotte regelmäßig an ihr Thread-Kontingent, ist die automatische Skalierung der Worker-Flotte der nächste Schritt.
Rolling Update Schritt für Schritt
Der Ablauf ist für jeden Worker identisch und wird strikt der Reihe nach abgearbeitet:
Workers: [W1-old] [W2-old] [W3-old] [W4-old]
Step 1: [W1-drain] [W2-old] [W3-old] [W4-old]
Step 2: [W1-NEW✓] [W2-old] [W3-old] [W4-old]
Step 3: [W1-NEW✓] [W2-drain] [W3-old] [W4-old]
Step 4: [W1-NEW✓] [W2-NEW✓] [W3-old] [W4-old]
...until all updated
- Drainen: Der Worker wechselt in den Zustand
DRAININGund fällt aus dem Routing. Laufende Abfragen dürfen zu Ende laufen. - Neue Version ausrollen: Erst bei
active_tasks == 0oder nach dem Drain-Timeout wird Image oder Paket ersetzt. - Health-Check: Der neue Worker muss nachweisen, dass er die API erreicht und eine echte Abfrage lösen kann.
- Weitergehen oder zurückrollen: Nur ein grüner Health-Check gibt den nächsten Worker frei; ein roter stoppt das Rollout.
Rolling-Update-Orchestrator in Python
Der Orchestrator hält den Zustand jedes Workers in einem Enum, routet Aufgaben nur an Worker im Zustand RUNNING und blockiert in drain(), bis keine Aufgabe mehr aktiv ist:
import os
import time
import signal
import threading
import requests
from dataclasses import dataclass, field
from enum import Enum
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
class WorkerState(Enum):
RUNNING = "running"
DRAINING = "draining"
STOPPED = "stopped"
UPDATING = "updating"
@dataclass
class Worker:
worker_id: str
version: str
state: WorkerState = WorkerState.RUNNING
active_tasks: int = 0
tasks_completed: int = 0
session: requests.Session = field(default_factory=requests.Session)
def solve(self, task):
if self.state != WorkerState.RUNNING:
return {"error": "WORKER_NOT_ACCEPTING"}
self.active_tasks += 1
try:
result = self._do_solve(task)
self.tasks_completed += 1
return result
finally:
self.active_tasks -= 1
def _do_solve(self, task):
resp = self.session.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": task.get("method", "userrecaptcha"),
"googlekey": task["sitekey"],
"pageurl": task["pageurl"],
"json": 1
})
data = resp.json()
if data.get("status") != 1:
return {"error": data.get("request")}
captcha_id = data["request"]
for _ in range(60):
time.sleep(5)
result = self.session.get(
"https://ocr.captchaai.com/res.php",
params={
"key": API_KEY,
"action": "get",
"id": captcha_id,
"json": 1
}
).json()
if result.get("status") == 1:
return {"solution": result["request"]}
if result.get("request") != "CAPCHA_NOT_READY":
return {"error": result.get("request")}
return {"error": "TIMEOUT"}
def drain(self, timeout=120):
"""Stop accepting tasks and wait for active tasks to complete."""
self.state = WorkerState.DRAINING
start = time.time()
while self.active_tasks > 0:
if time.time() - start > timeout:
print(f"Worker {self.worker_id}: drain timeout with "
f"{self.active_tasks} tasks remaining")
break
time.sleep(1)
self.state = WorkerState.STOPPED
@property
def is_healthy(self):
return self.state == WorkerState.RUNNING
class RollingUpdateOrchestrator:
def __init__(self, workers):
self.workers = {w.worker_id: w for w in workers}
self.lock = threading.Lock()
def get_available_worker(self):
"""Route tasks only to RUNNING workers."""
with self.lock:
for worker in self.workers.values():
if worker.state == WorkerState.RUNNING:
return worker
return None
def rolling_update(self, new_version, health_check_fn=None,
max_unavailable=1, drain_timeout=120):
"""Update workers one at a time with health gates."""
worker_ids = list(self.workers.keys())
updated = []
failed = []
for i in range(0, len(worker_ids), max_unavailable):
batch = worker_ids[i:i + max_unavailable]
for wid in batch:
worker = self.workers[wid]
print(f"[{wid}] Draining (v{worker.version})...")
# Step 1: Drain active tasks
worker.drain(timeout=drain_timeout)
# Step 2: "Deploy" new version
print(f"[{wid}] Deploying v{new_version}...")
worker.state = WorkerState.UPDATING
worker.version = new_version
time.sleep(2) # Simulate deployment
# Step 3: Start and health check
worker.state = WorkerState.RUNNING
if health_check_fn:
healthy = health_check_fn(worker)
if not healthy:
print(f"[{wid}] Health check FAILED — rolling back")
failed.append(wid)
self._rollback(updated)
return {
"status": "rolled_back",
"failed_at": wid,
"updated": updated,
}
updated.append(wid)
print(f"[{wid}] Updated to v{new_version} ✓")
return {"status": "complete", "updated": updated, "failed": failed}
def _rollback(self, updated_ids):
"""Roll back already-updated workers."""
for wid in updated_ids:
worker = self.workers[wid]
print(f"[{wid}] Rolling back...")
worker.state = WorkerState.STOPPED
time.sleep(1)
worker.version = "rollback"
worker.state = WorkerState.RUNNING
@property
def status(self):
return {
wid: {
"version": w.version,
"state": w.state.value,
"active_tasks": w.active_tasks,
}
for wid, w in self.workers.items()
}
# Create fleet
workers = [Worker(f"w{i}", "1.2.0") for i in range(6)]
orchestrator = RollingUpdateOrchestrator(workers)
def health_check(worker):
"""Verify worker can solve a test CAPTCHA."""
# In production, send a real test task
return worker.state == WorkerState.RUNNING
# Execute rolling update
result = orchestrator.rolling_update(
new_version="1.3.0",
health_check_fn=health_check,
max_unavailable=1,
drain_timeout=60
)
print(f"Rolling update result: {result}")
Drei Details lohnen einen zweiten Blick:
drain(timeout=120)wartet aufactive_tasks == 0und bricht nach dem Timeout kontrolliert ab, statt unbegrenzt zu blockieren.get_available_worker()arbeitet unter einemLock. Ohne diese Sperre nimmt ein Worker noch eine Aufgabe an, während er bereits drainen soll.health_check_fnist bewusst austauschbar. In der Produktion sollte die Funktion eine vollständige Abfrage überin.phpundres.phpdurchlaufen.
Schlägt der Check fehl, setzt _rollback() die bereits aktualisierten Worker zurück; das Rollout endet mit dem Status rolled_back – eine ältere, aber einheitliche Version ist besser als eine halb aktualisierte Flotte.
Rolling Update in Node.js mit Fortschritt und Abbruchschwelle
Läuft das Deployment aus einer Node.js-Pipeline, ist derselbe Ablauf mit async/await kompakter. Diese Variante bricht ab, sobald mehr als ein Viertel der Flotte den Health-Check nicht besteht:
const axios = require("axios");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
class RollingUpdater {
constructor(workerCount, currentVersion) {
this.workers = Array.from({ length: workerCount }, (_, i) => ({
id: `worker-${i}`,
version: currentVersion,
state: "running",
activeTasks: 0,
}));
this.progress = { total: workerCount, completed: 0, failed: 0 };
}
async update(newVersion, options = {}) {
const {
maxUnavailable = 1,
drainTimeout = 60000,
healthCheckRetries = 3,
} = options;
console.log(
`Starting rolling update: v${this.workers[0].version} → v${newVersion}`
);
for (let i = 0; i < this.workers.length; i += maxUnavailable) {
const batch = this.workers.slice(i, i + maxUnavailable);
for (const worker of batch) {
try {
// Drain
console.log(`[${worker.id}] Draining...`);
worker.state = "draining";
await this.waitForDrain(worker, drainTimeout);
// Deploy
console.log(`[${worker.id}] Deploying v${newVersion}...`);
worker.state = "updating";
worker.version = newVersion;
// Health check
worker.state = "running";
const healthy = await this.healthCheck(worker, healthCheckRetries);
if (!healthy) {
worker.state = "failed";
this.progress.failed++;
console.log(`[${worker.id}] FAILED health check`);
if (this.progress.failed > Math.floor(this.workers.length * 0.25)) {
console.log("Too many failures — aborting rolling update");
return { status: "aborted", progress: this.progress };
}
continue;
}
this.progress.completed++;
console.log(
`[${worker.id}] Updated ✓ (${this.progress.completed}/${this.progress.total})`
);
} catch (err) {
console.error(`[${worker.id}] Error: ${err.message}`);
this.progress.failed++;
}
}
}
return { status: "complete", progress: this.progress };
}
async waitForDrain(worker, timeout) {
const start = Date.now();
while (worker.activeTasks > 0 && Date.now() - start < timeout) {
await new Promise((r) => setTimeout(r, 1000));
}
}
async healthCheck(worker, retries) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const resp = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "getbalance", json: 1 },
timeout: 10000,
});
if (resp.data.status === 1) return true;
} catch {
// Retry
}
await new Promise((r) => setTimeout(r, 5000));
}
return false;
}
}
// Execute
const updater = new RollingUpdater(8, "1.2.0");
updater
.update("1.3.0", { maxUnavailable: 2, drainTimeout: 30000 })
.then((result) => console.log("Result:", JSON.stringify(result, null, 2)));
Der Health-Check nutzt action=getbalance als günstigen Test für API-Schlüssel und Netzwerkpfad. Für kritische Deployments sollte danach eine echte Lösung folgen – etwa gegen ein Testformular unter https://staging.example-app.test/login.
Praxisbeispiel: nächtliches Rollout mit GitLab CI
Ein Berliner E-Commerce-Team betreibt zwölf Löser-Worker auf Hetzner-Cloud-Instanzen. In der GitLab-CI-Pipeline startet der Job deploy:workers um 03:00 Uhr und ruft den Orchestrator mit max_unavailable=2 und drain_timeout=120 auf.
Der Health-Check läuft zweistufig: Erst prüft getbalance Guthaben und Erreichbarkeit, danach löst der neue Worker ein reCAPTCHA v2 gegen das eigene Staging-Login. Fällt die Erfolgsquote in den zehn Minuten nach dem Rollout unter den Vorwochenwert, startet dieselbe Pipeline die Rollback-Stage. Damit das reproduzierbar bleibt, beschreiben Sie die Worker als Code: mit Ansible für die Worker-Bereitstellung oder mit Terraform für die Infrastruktur.
Update-Strategien im Vergleich
| Strategie | Ausfallzeit | Rollback-Tempo | Komplexität | Geeignet für |
|---|---|---|---|---|
| Rolling Update | keine | mittel | niedrig | die meisten Deployments |
| Blue-Green | keine | sofort | mittel | kritische Dienste |
| Canary | keine | schnell | hoch | große Flotten (ab 50 Workern) |
| Recreate | kurz | entfällt | sehr niedrig | Entwicklungsumgebungen |
Unter 20 Workern ist das Rolling Update meist die pragmatischste Wahl: keine zweite Umgebung wie bei Blue-Green, keine Traffic-Aufteilung wie bei Canary.
Typische Probleme beim Rolling Update
| Problem | Ursache | Lösung |
|---|---|---|
| Aufgaben brechen während des Rollouts ab | Drain-Timeout kürzer als die längste Abfrage | Timeout an der langsamsten Aufgabe ausrichten; bei reCAPTCHA v2 sind 120 Sekunden sicher |
| Worker läuft, verarbeitet aber keine Aufgaben | Routing, API-Schlüssel oder Queue-Anbindung stimmen nicht mehr | Queue-Tiefe, Guthaben und Fehlerrate pro Worker gemeinsam prüfen |
| Fehlerquote steigt erst nach dem Rollout | Neue Version ändert Session-, Proxy- oder Wiederholungslogik | Läufe beider Versionen vergleichen und bei Bedarf zurückrollen |
| Health-Check bleibt dauerhaft rot | Secrets oder Netzwerkpfade weichen von der Zielumgebung ab | Denselben Check vorab in exakt derselben Umgebung ausführen |
| Rollback hinterlässt gemischte Versionen | Nur ein Teil der Worker wurde zurückgesetzt | Aktualisierte Worker-IDs mitschreiben |
Häufige Fragen
Gehen laufende CAPTCHA-Lösungen bei einem Rolling Update verloren?
Nein, sofern das Drain-Fenster lang genug ist. Ab dem Zustand DRAINING nimmt der Worker keine neuen Aufgaben an, liefert die übermittelten Abfragen aber noch aus. Erst bei active_tasks == 0 oder nach dem Timeout wird der Prozess ersetzt.
Wie lange sollte das Drain-Timeout sein?
An der langsamsten Abfrage plus Puffer. reCAPTCHA v2 wird in unter 60 Sekunden gelöst, Turnstile in unter 10 Sekunden – mit 120 Sekunden sind auch Wiederholungsversuche abgedeckt.
Verbraucht ein Rolling Update zusätzliche Threads?
Nein. Drainende Worker nehmen keine neuen Aufgaben an, die Zahl gleichzeitiger Threads sinkt sogar leicht. Wichtig ist die Gegenrichtung: Es muss genug Rest-Kapazität für den Rückstau bleiben.
Woran erkenne ich, dass ein Rollback nötig ist?
An drei Signalen: Der Health-Check schlägt mehrfach hintereinander fehl, die Fehlerquote der neuen Version liegt über der der alten, oder die Warteschlange wächst schneller, als sie abgebaut wird. Legen Sie diese Schwellenwerte vor dem Rollout fest.
Funktioniert das auch mit Docker, Kubernetes oder systemd?
Ja. Kubernetes bringt Rolling Updates über maxUnavailable und readinessProbe mit – die Probe sollte eine echte Lösung testen, nicht nur einen offenen Port. Auf klassischen VMs, etwa bei Hetzner oder netcup, übernimmt ein Skript wie oben die Reihenfolge.
Fazit
Ein Rolling Update für CAPTCHA-Worker steht und fällt mit zwei Zahlen: dem Drain-Timeout – längste Lösungszeit plus Puffer – und der Zahl gleichzeitig ausgetauschter Worker, bei kleinen Flotten 1, sonst 10–25 %. Kommt ein Health-Check dazu, der eine echte Abfrage löst, endet ein fehlgeschlagenes Rollout im Rollback statt im Incident.