Tutorials

CAPTCHA Çözme Worker'ları için Durum Kontrolü Uç Noktaları

Kısa yanıt: bir CAPTCHA çözme worker'ını üretime güvenle almak için ona üç HTTP uç noktası ekleyin — canlılık (/health/live), hazırlık (/health/ready) ve bağımlılık (/health/dependencies). Bu üçlü, Kubernetes'in veya yük dengeleyicinin donmuş, bakiyesi tükenmiş ya da bir döngüde takılmış bir worker'ı otomatik fark edip trafikten çıkarmasını sağlar. Aksi hâlde orkestratörünüz, on dakikadır tek görev çözemeyen "ölü" bir worker'a iş göndermeye devam eder — süreç hâlâ ayakta göründüğü için hiçbir alarm çalmaz.

Fark burada gizlidir: sürecin çalışıyor olması, iş yapabildiği anlamına gelmez. Bir worker'ın PID'si canlıdır ama API anahtarının bakiyesi sıfıra inmiştir; başka bir worker açılış aşamasındadır ama henüz tek görev çözmemiştir. Bu iki durumu ayırt edecek olan, aşağıdaki üç kontrol türüdür.

Canlılık, hazırlık ve bağımlılık: üç kontrol türü

Kontrol Yanıtladığı soru Başarısızlıkta ne yapılır
Canlılık (liveness) Süreç çalışıyor mu? Kapsayıcıyı yeniden başlatın
Hazırlık (readiness) İş kabul edebilir mi? Trafiği yönlendirmeyi durdurun
Bağımlılık (dependency) Yukarı akış servisleri sağlıklı mı? Kontrollü biçimde işlevi azaltın

Canlılık (liveness): süreç ayakta mı?

Canlılık kontrolü hiçbir dış çağrı yapmaz; yalnızca HTTP sunucusunun yanıt verdiğini kanıtlar. Başarısız olursa Kubernetes kapsayıcıyı yeniden başlatır. Bu yüzden içine bakiye ya da API sorgusu koymayın — yavaş bir dış çağrı, aslında sağlıklı olan bir worker'ı gereksiz yere yeniden başlatılmaya iter.

Hazırlık (readiness): iş alabilir mi?

Hazırlık, worker'ın o an görev kabul edip edemeyeceğini söyler. Bakiye düşükse, art arda hatalar birikmişse ya da worker henüz ısınıyorsa 503 döner ve Service'in arkasından çekilir — ama süreç yeniden başlatılmaz. Geçici bir durumu kalıcı bir arıza gibi ele almamanın yolu budur.

Bağımlılık (dependency): yukarı akış sağlıklı mı?

Bağımlılık kontrolü CaptchaAI API'sine erişilebilirliği ölçer. Amaç worker'ı öldürmek değil; sorunun worker'ın kendisinde mi yoksa yukarı akışta mı olduğunu panoda ayırt edebilmektir. Yukarı akış bozulduğunda tüm filoyu birden yeniden başlatmak sorunu çözmez, yalnızca büyütür.

Örnek bir senaryo: İstanbul'daki bir e-ticaret ekibi, kampanya döneminde ödeme akışlarını test eden bir worker filosu çalıştırıyor. CaptchaAI thread tabanlı faturalandırdığı için (BASIC planı $15/ay, 5 thread) plan bakiyesi düştüğünde çözümler durabilir. Böyle bir anda hazırlık kontrolünün 503 dönmesi, Kubernetes'in o worker'a yeni görev yollamasını keser; canlılık kontrolü ise donan süreci yeniden başlatır. İki sinyali karıştırmayın: bakiye düşüklüğü worker'ı hazır değil yapar ama yeniden başlatmayı gerektirmez.

Python: Flask ile durum kontrolü uç noktaları

Aşağıdaki Flask uygulaması, worker'ın çözüm sayacını thread güvenli biçimde tutar ve üç uç noktayı yayınlar. Bakiye sorgusu 60 saniyelik bir önbellekle sınırlıdır; böylece her istek CaptchaAI API'sine gitmez.

import requests
import time
import threading
from flask import Flask, jsonify
from dataclasses import dataclass, field

API_KEY = "YOUR_API_KEY"
RESULT_URL = "https://ocr.captchaai.com/res.php"

app = Flask(__name__)


@dataclass
class WorkerHealth:
    """Tracks worker health metrics."""
    started_at: float = field(default_factory=time.monotonic)
    last_solve_at: float = 0.0
    total_solved: int = 0
    total_failed: int = 0
    consecutive_failures: int = 0
    balance: float | None = None
    balance_checked_at: float = 0.0
    _lock: threading.Lock = field(default_factory=threading.Lock)

    def record_success(self):
        with self._lock:
            self.total_solved += 1
            self.last_solve_at = time.monotonic()
            self.consecutive_failures = 0

    def record_failure(self):
        with self._lock:
            self.total_failed += 1
            self.consecutive_failures += 1

    @property
    def success_rate(self) -> float:
        total = self.total_solved + self.total_failed
        return self.total_solved / total if total > 0 else 1.0

    @property
    def seconds_since_last_solve(self) -> float:
        if self.last_solve_at == 0:
            return time.monotonic() - self.started_at
        return time.monotonic() - self.last_solve_at


health = WorkerHealth()

# Thresholds
MAX_CONSECUTIVE_FAILURES = 10
MAX_SECONDS_WITHOUT_SOLVE = 600  # 10 minutes
MIN_BALANCE = 1.0


def check_balance() -> float | None:
    """Check CaptchaAI balance."""
    now = time.monotonic()
    # Cache balance for 60 seconds
    if health.balance is not None and now - health.balance_checked_at < 60:
        return health.balance

    try:
        resp = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "getbalance", "json": 1,
        }, timeout=10).json()
        health.balance = float(resp.get("request", 0))
        health.balance_checked_at = now
        return health.balance
    except Exception:
        return health.balance  # Return cached value on error


@app.route("/health/live")
def liveness():
    """Liveness probe — is the process responsive?"""
    return jsonify({"status": "ok", "uptime_s": int(time.monotonic() - health.started_at)}), 200


@app.route("/health/ready")
def readiness():
    """Readiness probe — can the worker accept tasks?"""
    issues = []

    # Check consecutive failures
    if health.consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
        issues.append(f"consecutive_failures={health.consecutive_failures}")

    # Check time since last solve
    if health.total_solved > 0 and health.seconds_since_last_solve > MAX_SECONDS_WITHOUT_SOLVE:
        issues.append(f"no_solve_for={int(health.seconds_since_last_solve)}s")

    # Check balance
    balance = check_balance()
    if balance is not None and balance < MIN_BALANCE:
        issues.append(f"low_balance=${balance:.2f}")

    if issues:
        return jsonify({
            "status": "not_ready",
            "issues": issues,
            "stats": {
                "solved": health.total_solved,
                "failed": health.total_failed,
                "success_rate": round(health.success_rate, 3),
            },
        }), 503

    return jsonify({
        "status": "ready",
        "stats": {
            "solved": health.total_solved,
            "failed": health.total_failed,
            "success_rate": round(health.success_rate, 3),
            "balance": balance,
        },
    }), 200


@app.route("/health/dependencies")
def dependencies():
    """Check upstream dependencies."""
    checks = {}

    # CaptchaAI API reachability
    try:
        resp = requests.get(RESULT_URL, params={
            "key": API_KEY, "action": "getbalance", "json": 1,
        }, timeout=10)
        checks["captchaai_api"] = {
            "status": "ok" if resp.status_code == 200 else "degraded",
            "response_ms": int(resp.elapsed.total_seconds() * 1000),
        }
    except Exception as e:
        checks["captchaai_api"] = {"status": "down", "error": str(e)}

    all_ok = all(c["status"] == "ok" for c in checks.values())
    return jsonify({
        "status": "ok" if all_ok else "degraded",
        "checks": checks,
    }), 200 if all_ok else 503


# --- Worker loop (runs in background) ---

def worker_loop():
    """Simulated CAPTCHA solving worker."""
    while True:
        try:
            # ... solve CAPTCHA logic ...
            health.record_success()
        except Exception:
            health.record_failure()
        time.sleep(1)


threading.Thread(target=worker_loop, daemon=True).start()

Node.js: Express ile durum kontrolü uç noktaları

Aynı mantığın Node.js karşılığı. Canlılık uç noktası hiçbir dış çağrı yapmaz; hazırlık ve bağımlılık kontrolleri bakiyeyi yine önbellekten okur.

const express = require("express");

const API_KEY = "YOUR_API_KEY";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

const app = express();

const health = {
  startedAt: Date.now(),
  lastSolveAt: 0,
  totalSolved: 0,
  totalFailed: 0,
  consecutiveFailures: 0,
  balance: null,
  balanceCheckedAt: 0,

  recordSuccess() {
    this.totalSolved++;
    this.lastSolveAt = Date.now();
    this.consecutiveFailures = 0;
  },

  recordFailure() {
    this.totalFailed++;
    this.consecutiveFailures++;
  },

  get successRate() {
    const total = this.totalSolved + this.totalFailed;
    return total > 0 ? this.totalSolved / total : 1;
  },
};

async function checkBalance() {
  if (health.balance !== null && Date.now() - health.balanceCheckedAt < 60000) {
    return health.balance;
  }
  try {
    const url = `${RESULT_URL}?key=${API_KEY}&action=getbalance&json=1`;
    const resp = await (await fetch(url)).json();
    health.balance = parseFloat(resp.request);
    health.balanceCheckedAt = Date.now();
    return health.balance;
  } catch {
    return health.balance;
  }
}

app.get("/health/live", (req, res) => {
  res.json({ status: "ok", uptimeMs: Date.now() - health.startedAt });
});

app.get("/health/ready", async (req, res) => {
  const issues = [];

  if (health.consecutiveFailures >= 10) {
    issues.push(`consecutive_failures=${health.consecutiveFailures}`);
  }

  if (health.totalSolved > 0) {
    const silentMs = Date.now() - health.lastSolveAt;
    if (silentMs > 600_000) {
      issues.push(`no_solve_for=${Math.round(silentMs / 1000)}s`);
    }
  }

  const balance = await checkBalance();
  if (balance !== null && balance < 1.0) {
    issues.push(`low_balance=$${balance.toFixed(2)}`);
  }

  const stats = {
    solved: health.totalSolved,
    failed: health.totalFailed,
    successRate: Math.round(health.successRate * 1000) / 1000,
    balance,
  };

  if (issues.length > 0) {
    return res.status(503).json({ status: "not_ready", issues, stats });
  }
  res.json({ status: "ready", stats });
});

app.get("/health/dependencies", async (req, res) => {
  const checks = {};
  try {
    const start = Date.now();
    const url = `${RESULT_URL}?key=${API_KEY}&action=getbalance&json=1`;
    const resp = await fetch(url);
    checks.captchaaiApi = {
      status: resp.ok ? "ok" : "degraded",
      responseMs: Date.now() - start,
    };
  } catch (e) {
    checks.captchaaiApi = { status: "down", error: e.message };
  }

  const allOk = Object.values(checks).every((c) => c.status === "ok");
  res.status(allOk ? 200 : 503).json({
    status: allOk ? "ok" : "degraded",
    checks,
  });
});

app.listen(8080, () => console.log("Health server on :8080"));

Kubernetes probe yapılandırması

livenessProbe donmuş kapsayıcıyı yeniden başlatır, readinessProbe ise worker geçici olarak iş alamadığında onu Service'in arkasından çeker. İki probe'un failureThreshold ve periodSeconds değerlerini ayrı ayrı ayarlayın — canlılığı hazırlıktan daha toleranslı tutmak, gereksiz yeniden başlatmaları önler.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: captcha-worker
spec:
  replicas: 3
  template:
    spec:
      containers:

        - name: worker
          image: captcha-worker:latest
          ports:

            - containerPort: 8080
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 15
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
            failureThreshold: 2

Yapılandırmada dikkat edilecek birkaç pratik nokta:

  • initialDelaySeconds değerini worker'ın gerçek açılış süresine göre seçin; çok kısa tutmak, daha ilk çözümüne ulaşmadan açılıştaki worker'ları yeniden başlatmaya iter.
  • livenessProbe'un failureThreshold değerini readinessProbe'unkinden yüksek tutun — geçici bir yavaşlama yeniden başlatmayı değil, yalnızca trafikten çıkarmayı tetiklemeli.
  • Hazırlık probe'unu daha sık çalıştırın; böylece bakiyesi tükenen bir worker filodan hızla çekilir ve boşa görev harcanmaz.

Uç noktalara göre HTTP yanıt kodları

Uç nokta 200 503
/health/live Süreç yanıt veriyor Süreç donmuş — yeniden başlat
/health/ready İş kabul edebilir Görev göndermeyi durdur
/health/dependencies Tüm bağımlılıklar tamam Yukarı akış bozulmuş

Operatör için eşik ayarları

  • Hazırlığı yeni işleri durdurmak, canlılığı yeniden başlatmayı tetiklemek, uyarıları ise düşen verimi yakalamak için kullanın — üçünü aynı sinyale bağlamayın.
  • Durum kontrollerini yalnızca sürecin çalışma süresine değil; kuyruk derinliğine, son hata oranına ve bağımlılıkların erişilebilirliğine dayandırın.
  • Eşik değerlerini nöbetçi ekibin görebileceği bir yerde tutun ki durum değişiklikleri gürültü değil, eyleme dönüştürülebilir bir sinyal olsun.

Sorun giderme

Sorun Sebep Düzeltme
Worker sürekli yeniden başlatılıyor Canlılık eşiği fazla düşük failureThreshold veya periodSeconds değerini artırın
Worker açılışta "not-ready" işaretleniyor Henüz çözüm yokken "çok uzun süre" sayılıyor seconds_since_last_solve'yi yalnızca ilk çözümden sonra kontrol edin
Bakiye kontrolü uç noktayı yavaşlatıyor Her istekte API çağrısı yapılıyor Bakiyeyi bir TTL ile önbelleğe alın (60 saniye önerilir)
Durum uç noktasının kendisi çöküyor Kontrolde yakalanmayan istisna Her kontrolü try/except ile sarın; 500 yerine "degraded" dönün
Bağımlılık kontrolünden hatalı negatifler Bakiye sorgusu sırasında geçici ağ kesintisi Önbellekteki değeri stale-while-revalidate yaklaşımıyla kullanın

Sık sorulan sorular

Canlılık ile hazırlık probe'u arasındaki fark nedir?

Canlılık yalnızca "süreç yanıt veriyor mu?" sorusuna bakar ve başarısız olursa kapsayıcıyı yeniden başlatır. Hazırlık ise "bu worker şu an iş alabilir mi?" sorusuna bakar; bakiye düşükse veya art arda hatalar varsa 503 döner ama süreci yeniden başlatmaz — sadece trafik dışına alır.

Durum kontrolü CaptchaAI thread'lerimi tüketir mi?

Hayır. getbalance sorgusu bir çözüm görevi değildir, dolayısıyla aktif thread saymaz ve plan bakiyenizi harcamaz. Yine de sorguyu 60 saniyelik bir önbellekle sınırlayın; her hazırlık isteğinde API'ye gitmek gereksiz gecikme yaratır.

Worker açılışta neden "not-ready" görünüyor?

Çünkü henüz tek bir çözüm yapmadığı için seconds_since_last_solve yüksek çıkar. total_solved > 0 koşulunu ilk çözümden sonra devreye sokun; böylece worker ısınma süresinde boş yere trafikten çıkarılmaz.

Bir worker filosunu tek yerden nasıl izlerim?

Durum uç noktalarının yanına Prometheus biçiminde bir /metrics uç noktası ekleyin ve Grafana panelinde birleştirin. Böylece tüm filonun başarı oranını, bakiyesini ve son çözüm zamanını tek ekranda görürsünüz.

İlgili Makaleler

Sonraki Adımlar

CAPTCHA worker'larınızı üretime hazır hale getirin — CaptchaAI API anahtarınızı alın ve durum kontrolü uç noktalarını ekleyin.

İlgili kılavuzlar:

Bu makale için yorumlar devre dışı bırakılmıştır.