DevOps & Scaling

Files d'attente de tâches Kubernetes pour la résolution de CAPTCHA à grande échelle

Pour traiter des CAPTCHA à grande échelle sans surveiller chaque processus, faites tourner un pool de worker pods qui consomment une file Redis et laissez Kubernetes ajouter des pods quand la file s'allonge. Ce guide déploie ce schéma de bout en bout, avec une mise à l'échelle pilotée par la profondeur de la file via l'API CaptchaAI. Il tient à l'identique sur OVHcloud Managed Kubernetes, Scaleway Kapsule ou un EKS en eu-west-3 (Paris).

Un seul worker suffit tant que le volume reste modeste, mais dès quelques centaines de résolutions par minute il devient le goulot d'étranglement : la file gonfle, les délais s'allongent, et un plantage interrompt tout le traitement. Répartir la charge sur plusieurs pods pilotés par Kubernetes règle les trois problèmes d'un coup — capacité, latence et résilience.


Architecture de la file d'attente

Producer → Redis Queue → Worker Pods (auto-scaled) → CaptchaAI API
                              ↓
                       Results Store (Redis)

Le producteur pousse les tâches dans Redis, les workers les dépilent et réécrivent le résultat dans un magasin partagé. La file découple producteur et workers : vous ajoutez, redémarrez ou supprimez des pods sans jamais toucher au code qui soumet les tâches. Chaque résultat étant indexé par identifiant, un worker qui redémarre en cours de traitement ne fait perdre qu'une seule tâche, pas tout le lot.


Déployer les worker pods

Démarrez avec trois réplicas et des ressources modestes : un worker passe l'essentiel de son temps à attendre l'API et consomme peu de CPU.

# k8s/worker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: captcha-worker
  labels:
    app: captcha-worker
spec:
  replicas: 3
  selector:
    matchLabels:
      app: captcha-worker
  template:
    metadata:
      labels:
        app: captcha-worker
    spec:
      containers:

        - name: worker
          image: your-registry/captcha-worker:latest
          env:

            - name: CAPTCHAAI_KEY
              valueFrom:
                secretKeyRef:
                  name: captchaai-secret
                  key: api-key

            - name: REDIS_URL
              value: "redis://redis-service:6379"
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "250m"

Créer le secret CaptchaAI

Stockez la clé API dans un Secret, jamais dans l'image ni dans un manifeste versionné :

kubectl create secret generic captchaai-secret \
  --from-literal=api-key=YOUR_API_KEY

Déployer Redis comme file d'attente

Un seul pod Redis exposé par un Service interne suffit pour démarrer :

# k8s/redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:

        - name: redis
          image: redis:7-alpine
          ports:

            - containerPort: 6379
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
  name: redis-service
spec:
  selector:
    app: redis
  ports:

    - port: 6379

Le code du worker

Chaque worker boucle sur un blpop bloquant, résout la tâche via CaptchaAI, écrit le résultat dans captcha:results et met à jour la longueur de la file.

# worker.py
import os
import json
import time
import redis
import requests


class CaptchaWorker:
    """Kubernetes worker that processes CAPTCHA tasks from Redis."""

    def __init__(self):
        self.api_key = os.environ["CAPTCHAAI_KEY"]
        self.redis = redis.from_url(
            os.environ.get("REDIS_URL", "redis://localhost:6379"),
        )
        self.base = "https://ocr.captchaai.com"

    def run(self):
        """Main worker loop."""
        hostname = os.environ.get("HOSTNAME", "unknown")
        print(f"Worker {hostname} started")

        while True:
            result = self.redis.blpop("captcha:queue", timeout=30)
            if result is None:
                continue

            _, raw = result
            task = json.loads(raw)
            task_id = task.get("id", "unknown")

            print(f"[{hostname}] Processing {task_id}")
            start = time.time()

            try:
                token = self._solve(task["method"], task["params"])
                duration = time.time() - start
                self.redis.hset("captcha:results", task_id, json.dumps({
                    "status": "success",
                    "token": token,
                    "duration": f"{duration:.1f}s",
                    "worker": hostname,
                }))
                print(f"[{hostname}] {task_id} solved in {duration:.1f}s")

            except Exception as e:
                self.redis.hset("captcha:results", task_id, json.dumps({
                    "status": "error",
                    "error": str(e),
                    "worker": hostname,
                }))
                print(f"[{hostname}] {task_id} failed: {e}")

            # Update queue length metric
            queue_len = self.redis.llen("captcha:queue")
            self.redis.set("captcha:queue_length", queue_len)

    def _solve(self, method, params, timeout=120):
        resp = requests.post(f"{self.base}/in.php", data={
            "key": self.api_key,
            "method": method,
            "json": 1,
            **params,
        }, 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")


if __name__ == "__main__":
    CaptchaWorker().run()

Le même worker traite reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile ou GeeTest v3 : seul le champ method change. La boucle interne interroge res.php toutes les 5 secondes jusqu'à un plafond de 120 secondes ; ajustez ces valeurs selon le type de CAPTCHA, certains défis se résolvant plus vite que d'autres. Ne loggez ni le token ni de données personnelles — un réflexe RGPD utile en France comme en Belgique.


Mise à l'échelle automatique avec le HPA

Faites évoluer les workers selon la profondeur de la file plutôt que le CPU. Au-delà de dix tâches en attente par pod, le HorizontalPodAutoscaler ajoute des réplicas jusqu'à maxReplicas.

Si configurer un adaptateur de métriques externes vous rebute, KEDA lit directement la longueur d'une liste Redis comme déclencheur et gère même la mise à l'échelle jusqu'à zéro. Dans les deux cas, allongez la fenêtre de stabilisation (scaleDown) pour éviter que les pods ne montent et descendent à chaque pic bref.

# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: captcha-worker-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: captcha-worker
  minReplicas: 2
  maxReplicas: 20
  metrics:

    - type: External
      external:
        metric:
          name: redis_queue_length
          selector:
            matchLabels:
              queue: captcha
        target:
          type: AverageValue
          averageValue: "10"

Le producteur de tâches

Le producteur pousse les tâches et récupère les résultats par identifiant, sans connaître les workers. La fonction get_results interroge le hash de résultats jusqu'à un délai limite ; en production, préférez un abonnement Redis Pub/Sub ou un webhook à ce polling serré.

import json
import uuid
import redis


def submit_tasks(redis_url, tasks):
    """Submit CAPTCHA tasks to the queue."""
    r = redis.from_url(redis_url)
    task_ids = []

    for task in tasks:
        task_id = str(uuid.uuid4())[:8]
        task["id"] = task_id
        r.rpush("captcha:queue", json.dumps(task))
        task_ids.append(task_id)

    return task_ids


def get_results(redis_url, task_ids, timeout=180):
    """Wait for and collect results."""
    r = redis.from_url(redis_url)
    results = {}
    deadline = time.time() + timeout

    while len(results) < len(task_ids) and time.time() < deadline:
        for tid in task_ids:
            if tid in results:
                continue
            raw = r.hget("captcha:results", tid)
            if raw:
                results[tid] = json.loads(raw)
        time.sleep(1)

    return results

Observabilité et maîtrise des coûts

Le worker publie captcha:queue_length à chaque itération : exposez cette valeur à Prometheus pour suivre la profondeur de la file, le taux de réussite et le temps de résolution médian par pod. Ces trois signaux suffisent à décider quand ajuster maxReplicas ou le seuil du HPA.

Côté facturation, gardez en tête que CaptchaAI facture au thread concurrent, pas au CAPTCHA résolu, avec des résolutions illimitées par thread. Au-delà d'un certain nombre de pods, c'est donc votre allocation de threads — et non Kubernetes — qui plafonne le débit réel : inutile de dépasser 20 workers si votre plan n'ouvre que 50 threads.


Dépannage

Problème Cause probable Correctif
Les workers ne démarrent pas Secret non créé Lancez la commande kubectl create secret
Pods en CrashLoopBackOff Variables d'environnement ou Redis manquants Inspectez les logs avec kubectl logs
Le HPA ne monte pas en charge Métriques externes non configurées Installez un adaptateur de métriques (KEDA)
File qui grossit sans traitement Workers inactifs ou plantés Vérifiez l'état des pods et redémarrez

FAQ

Combien de threads CaptchaAI faut-il pour alimenter mes workers ?

Alignez le nombre de résolutions simultanées sur votre allocation de threads, pas sur le nombre de pods. BASIC ($15/mois, 5 threads) convient aux tests ; ADVANCE ($90/mois, 50 threads) ou PREMIUM ($170/mois, 100 threads) soutiennent un débit continu, avec résolutions illimitées par thread.

Comment régler le seuil du HPA selon ma file ?

Partez d'averageValue: "10". Si la file reste au-dessus de la cible, abaissez la valeur ; si les pods oscillent, remontez-la et allongez la fenêtre de stabilisation.

Où déployer les workers pour limiter la latence en Europe ?

Rapprochez le cluster de votre orchestrateur — eu-west-3 (Paris) sur AWS, ou OVHcloud/Scaleway en France. La résolution reste toutefois dominée par le temps de traitement de l'API.

Que se passe-t-il si une résolution dépasse le timeout ?

Le worker lève une exception, écrit un statut error dans captcha:results et passe à la tâche suivante. Prévoyez côté producteur un retry avec backoff exponentiel.

CaptchaAI prend-il en charge tous les types poussés dans la file ?

Les types courants sont pris en charge : reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile et GeeTest v3. hCaptcha et FunCaptcha ne sont pas encore pris en charge — ne les routez pas vers la file en attendant.


Guides connexes


Passez à des milliers de résolutions – adoptez CaptchaAI pour Kubernetes.

Les commentaires sont désactivés pour cet article.