DevOps & Scaling

Очереди заданий Kubernetes для решения CAPTCHA в любом масштабе

Пропускная способность решения CAPTCHA в Kubernetes упирается не в число подов, а в две вещи: глубину очереди и количество потоков в вашем тарифе CaptchaAI. Реплик воркера можно поднять хоть сотню, но одновременно в работе будет ровно столько задач, сколько потоков вы оплатили. Рабочая схема на масштабе поэтому состоит из трёх частей: очередь на Redis, воркеры, которые забирают из неё задачи и обращаются к API CaptchaAI, и HorizontalPodAutoscaler, добавляющий реплики, когда очередь растёт. Ниже — готовая конфигурация: деплой воркеров, хранение ключа в Secret, автомасштабирование и постановка задач.


Архитектура очереди: producer, Redis и воркеры

Схема простая и проверенная под нагрузкой. Producer кладёт задачи в очередь Redis, пул воркеров разбирает их параллельно и складывает результаты обратно в Redis, откуда их забирает вызывающая сторона. Redis здесь работает сразу в трёх ролях: брокер очереди, хранилище результатов и источник метрики глубины очереди для автомасштабирования.

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

Развёртывание воркеров решения CAPTCHA

Начните с трёх реплик воркера — этого хватает, чтобы проверить связку под реальной нагрузкой, а дальше масштабированием займётся HPA. Обратите внимание на requests и limits по памяти и CPU: воркер почти всё время ждёт ответа внешнего API, поэтому он лёгкий по ресурсам, и на один узел помещается много реплик.

# 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"

Хранение API-ключа в Secret Kubernetes

API-ключ CaptchaAI не должен попадать ни в образ, ни в манифест деплоя. Держите его в Secret Kubernetes и пробрасывайте в под через secretKeyRef — так ключ не окажется в git и в истории сборок.

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

Очередь на Redis: развёртывание и сервис

Для одного кластера достаточно одного экземпляра Redis. Конфигурация ниже минимальная: под без постоянного тома (при перезапуске очередь очищается) плюс сервис redis-service, по которому воркеры находят брокер. Для продакшена с длинной очередью добавьте PersistentVolume или вынесите Redis в управляемый сервис.

# 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

Код воркера: чтение очереди и вызов API

Логика воркера — это бесконечный цикл. Заблокированное чтение из очереди (blpop) с тайм-аутом достаёт задачу, метод отправляется в in.php, затем воркер опрашивает res.php до готовности и пишет результат в хэш captcha:results. Ошибки не роняют под: они тоже фиксируются в результатах со статусом error. После каждой задачи воркер обновляет метрику captcha:queue_length — именно её читает автомасштабирование.

# 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()

Автомасштабирование по глубине очереди

HPA масштабирует воркеры по внешней метрике — длине очереди Redis, а не по CPU. Значение averageValue: "10" означает целевую нагрузку около десяти задач на под: когда очередь растёт быстрее, чем воркеры её разбирают, HPA добавляет реплики вплоть до maxReplicas, а после спада пиков снова сжимает пул до minReplicas.

# 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"

Постановка задач в очередь

Producer генерирует короткий ID для каждой задачи, кладёт её в очередь через rpush и возвращает список ID. Функция get_results опрашивает хэш результатов, пока не соберёт все ответы или не истечёт тайм-аут, — так вызывающая сторона получает решения в удобном для себя темпе, не блокируя воркеры.

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

Планирование мощности: считайте потоки, а не поды

Здесь и кроется главный нюанс масштабирования. HPA поднимет хоть двадцать реплик, но реальный потолок одновременных решений задаёт число потоков в тарифе CaptchaAI. Тарификация идёт по потокам (одновременным задачам), а не за каждое решение, поэтому расчёт мощности прямой: 50 потоков — это до 50 задач, выполняемых в один и тот же момент; как только одна завершается, поток сразу берёт следующую. Общее число задач в сутки при этом не ограничено.

Возьмём агентство парсинга из Алматы или Киева, которое обрабатывает порядка 30 000 задач в сутки с вечерними пиками. Под средний профиль подходит тариф ADVANCE ($90/мес, 50 потоков); при росте нагрузки — PREMIUM ($170/мес, 100 потоков) или CORPORATE ($240/мес, 150 потоков). Проверить связку и прогнать нагрузочный тест можно на BASIC ($15/мес, 5 потоков), а затем перейти на нужный тариф без переписывания кода.

Практический вывод: держите maxReplicas в HPA согласованным с числом оплаченных потоков. Поды сверх этого числа не увеличат пропускную способность — они будут ждать свободного потока и впустую расходовать ресурсы кластера. Фиксированная цена в USD за потоки удобна командам, которые выставляют счета в местной валюте и не хотят платить за каждое решение отдельно.


Диагностика типичных проблем

Проблема Причина Что делать
Воркеры не стартуют Не создан Secret Выполните команду kubectl create secret.
Поды в CrashLoopBackOff Нет переменных окружения или недоступен Redis Посмотрите логи: kubectl logs <pod>.
HPA не масштабируется Не настроены внешние метрики Установите адаптер метрик или KEDA.
Очередь растёт, обработки нет Воркеры простаивают или упали Проверьте работоспособность подов и перезапустите их.

Часто задаваемые вопросы

Сколько потоков CaptchaAI нужно под мою нагрузку?

Считайте по одновременным задачам, а не по их общему числу. Один поток — одно решение в моменте. Прикиньте пиковое число параллельных запросов и возьмите тариф с тем же числом потоков: например, ADVANCE ($90/мес, 50 потоков) под средний парсинг или PREMIUM ($170/мес, 100 потоков) под высокие пики. Общее число задач в сутки ничем не ограничено — потоки задают лишь то, сколько из них выполняется одновременно.

Почему добавление подов не ускоряет обработку очереди?

Потому что потолок пропускной способности задаёт число оплаченных потоков, а не количество реплик. Когда все потоки заняты, лишние поды просто ждут ответа от API и прироста не дают. Согласуйте maxReplicas в HPA с числом потоков тарифа, а дальше повышайте пропускную способность переходом на более крупный тариф.

Как масштабировать воркеры по длине очереди Redis?

Есть два рабочих варианта. Первый — HPA с внешней метрикой redis_queue_length, как в манифесте выше (нужен адаптер метрик). Второй, обычно проще, — KEDA (Kubernetes Event-Driven Autoscaling): он умеет брать длину очереди Redis как триггер напрямую, без отдельного адаптера.

Что делать, если поды уходят в CrashLoopBackOff?

Сначала посмотрите логи: kubectl logs <pod>. Чаще всего причина — не создан Secret с API-ключом или недоступен Redis, из-за чего воркер падает на старте. Проверьте, что Secret captchaai-secret существует, а сервис redis-service резолвится внутри кластера.


Связанные материалы


Масштабируйте решение CAPTCHA до тысяч задач в час — подключите CaptchaAI к своему кластеру Kubernetes.

Комментарии для этой статьи отключены.