DevOps & Scaling

Filas de tarefas do Kubernetes para solução de CAPTCHA em escala

Quando o volume de CAPTCHAs oscila — milhares de requisições no pico de uma campanha, quase nada de madrugada — manter uma frota fixa de servidores ligada o tempo todo é desperdício. A resposta é uma fila: um produtor empilha tarefas no Redis, um conjunto de workers rodando no Kubernetes consome essa fila e cada desafio é resolvido pela API da CaptchaAI. Quando a fila cresce, o cluster sobe mais pods; quando ela esvazia, ele recolhe. Este guia monta esse padrão do zero, com deployment, autoescalonamento e o código do worker prontos para colar.


Arquitetura da fila: produtor, Redis e workers

O desenho separa quem gera trabalho de quem executa. O produtor não fala com a CaptchaAI diretamente: ele só coloca tarefas no Redis. Os workers puxam da fila no próprio ritmo, resolvem o CAPTCHA e gravam o token em um segundo armazenamento no Redis. Esse desacoplamento é o que permite escalar horizontalmente — a fila absorve os picos sem derrubar ninguém.

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

Deployment dos workers de CAPTCHA

O Deployment define quantos pods de worker sobem e com qual perfil de recursos. Comece com três réplicas: é o suficiente para um fluxo constante e dá margem para o autoescalonamento crescer a partir daí. Cada pod recebe a chave de API por referência a um Secret (nunca embutida na imagem) e a URL do Redis por variável de ambiente. Os limites de memória e CPU são enxutos de propósito — um worker de CAPTCHA passa quase todo o tempo aguardando a resposta da API, então o gargalo é a rede, não o processamento local.

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

Segredo com a chave de API

A chave de API nunca deve viver na imagem do contêiner nem no YAML versionado. Guarde-a em um Secret do Kubernetes e deixe o Deployment injetá-la em tempo de execução. Um comando cria o segredo a partir da chave — substitua YOUR_API_KEY pela sua chave real do painel da CaptchaAI:

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

Redis como fila de tarefas

O Redis faz dois papéis aqui: é a fila de entrada (captcha:queue) e o armazenamento de resultados (captcha:results). Para desenvolvimento e testes, uma única réplica com a imagem redis:7-alpine basta. O Service redis-service dá um nome DNS estável para que os workers encontrem o Redis sem depender do IP do pod. Em produção com carga real, troque esse pod solto por uma instância com persistência e alta disponibilidade (um Redis gerenciado ou um operador no cluster) — a fila é o ponto único de falha do desenho.

# 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

Código do worker: consumir a fila e resolver

O worker é um laço simples. Ele bloqueia em blpop esperando a próxima tarefa, envia o CAPTCHA para o endpoint in.php, consulta o resultado em res.php a cada cinco segundos até o token ficar pronto e grava o desfecho (sucesso ou erro) no hash captcha:results. No fim de cada volta, ele atualiza a métrica captcha:queue_length — é esse número que o autoescalonador observa. Como toda a comunicação usa json=1 e os campos padrão da API, o mesmo worker resolve qualquer tipo suportado sem mudar o código.

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

Autoescalonamento por profundidade de fila (HPA)

O número de réplicas não deve ser fixo: ele precisa acompanhar o tamanho da fila. O HorizontalPodAutoscaler usa uma métrica externa — redis_queue_length — e mantém uma média de dez tarefas em fila por pod (averageValue: "10"). Se a fila dispara para 200 itens, o HPA caminha em direção às 20 réplicas do teto; quando ela drena, ele volta para as duas réplicas mínimas. Para o HPA enxergar essa métrica, você precisa de um adaptador de métricas externas ou, mais simples, do KEDA, que lê o comprimento da fila Redis nativamente.

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

Vale lembrar que os pods não são o único limite de vazão. A CaptchaAI cobra por threads simultâneas — cada thread é um CAPTCHA em processamento — e não por resolução. O ADVANCE (US$ 90/mês, 50 threads) sustenta até 50 resoluções em paralelo, independentemente de quantos workers você suba. Subir 80 pods contra um teto de 50 threads só gera espera; dimensione o maxReplicas conforme o plano contratado, do BASIC (US$ 15/mês, 5 threads) aos tiers VIP.


Produtor: enfileirar e coletar resultados

Do outro lado da fila fica o produtor. submit_tasks gera um identificador curto para cada tarefa, faz rpush na fila e devolve os IDs. get_results consulta o hash de resultados a cada segundo até coletar tudo ou estourar o timeout. Esse par de funções é o que sua aplicação chama: ela fala apenas com o Redis, e os workers fazem o resto.

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

Considerações de produção

Se o seu tráfego é majoritariamente brasileiro, rode o cluster e a fila próximos dos usuários — uma região como a sa-east-1 (São Paulo) reduz o RTT entre produtor, workers e Redis. Antes de subir, valide o comportamento do HPA sob carga em um ambiente de staging autorizado, com dados fictícios e endpoints internos. E quando o pipeline processar dados que acompanham os CAPTCHAs, considere as obrigações da LGPD (ou do RGPD, em Portugal) ao definir o que os workers registram em log.


Solução de problemas

Os sintomas mais comuns aparecem no primeiro deploy. Use a tabela como triagem rápida:

Problema Causa Correção
Workers não sobem Segredo não foi criado Rode o comando kubectl create secret
Pods em CrashLoopBackOff Variáveis de ambiente ou Redis ausentes Verifique os logs com kubectl logs
HPA não escala Métricas externas não configuradas Instale um adaptador de métricas (KEDA)
Fila cresce, mas nada é processado Workers ociosos ou travados Cheque a saúde dos pods e reinicie

Perguntas frequentes

Como dimensiono o plano da CaptchaAI para o número de workers?

Alinhe o maxReplicas ao teto de threads do seu plano. Como a cobrança é por thread simultânea (não por resolução), subir mais pods do que o número de threads disponíveis só cria fila de espera. O ADVANCE (US$ 90/mês, 50 threads) comporta até 50 resoluções em paralelo.

O HPA consegue reduzir a zero quando a fila esvazia?

O HorizontalPodAutoscaler puro não escala até zero — o mínimo aqui é duas réplicas. Se você quer chegar a zero pod em períodos ociosos, use o KEDA, que suporta scale-to-zero a partir do comprimento da fila Redis.

Onde devo rodar os workers para reduzir a latência?

Escolha uma região próxima da origem do seu tráfego. Para usuários no Brasil, sa-east-1 (São Paulo) costuma dar o menor RTT entre a aplicação, o Redis e a API. Mantenha workers, fila e produtor na mesma região para evitar saltos desnecessários.

Preciso me preocupar com a LGPD ao processar tarefas em fila?

Se as tarefas carregam dados pessoais, sim: minimize o que é gravado em log e defina retenção curta para o hash de resultados. Trate a conformidade como decisão de arquitetura, não como algo a resolver depois — e nunca a interprete como aconselhamento jurídico.


Guias relacionados


Escale para milhares de resoluções — crie sua conta na CaptchaAI e rode em Kubernetes.

Os comentários estão desativados para este artigo.