Пропускная способность решения 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.