Hướng Dẫn Thực Hành

Health check endpoint cho worker giải CAPTCHA

Kubernetes vẫn báo pod Running, nhưng CAPTCHA worker của bạn đã 10 phút không giải xong task nào — API key hết hạn mức, hoặc worker kẹt trong vòng lặp chết. Không có health check endpoint, orchestrator không có cách nào biết điều đó và cứ tiếp tục đẩy task cho một worker gần như đã "chết". Ba endpoint dưới đây — /health/live, /health/ready, /health/dependencies — cho load balancer và Kubernetes đủ tín hiệu để tự phát hiện sự cố và định tuyến lại, không cần con người trực 24/7.

Ba loại health check cần có cho CAPTCHA worker

Mỗi loại probe trả lời một câu hỏi khác nhau — gộp chung cả ba vào một endpoint duy nhất là lý do phổ biến nhất khiến Kubernetes xử lý sai khi worker gặp sự cố.

Liveness: process có còn phản hồi không?

Chỉ kiểm tra process còn "sống" hay không, không liên quan gì đến việc worker có đang giải được task. Nếu liveness fail, Kubernetes khởi động lại container ngay.

Readiness: worker có thể nhận task mới không?

Phản ánh khả năng xử lý thực tế — số dư còn đủ, chưa kẹt lỗi liên tiếp. Nếu readiness fail, orchestrator ngừng định tuyến traffic tới worker này nhưng không restart nó.

Dependency: API CaptchaAI (upstream) có ổn không?

Đo độ trễ và khả năng phản hồi của endpoint res.php. Nếu dependency suy giảm, worker chuyển sang chế độ phục vụ hạn chế thay vì trả lỗi cứng cho toàn bộ request.

Flask: dựng health check endpoint bằng Python

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

Express: health check endpoint gọn cho Node.js

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"));

Cấu hình probe cho CAPTCHA worker trong Kubernetes

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

Mã HTTP trả về của từng health check endpoint

  • /health/live — 200 khi process phản hồi bình thường; 503 nghĩa là process bị treo, cần khởi động lại.
  • /health/ready — 200 khi có thể nhận task mới; 503 là tín hiệu ngừng gửi task tới worker này.
  • /health/dependencies — 200 khi toàn bộ dependency đều ổn; 503 nghĩa là upstream (API CaptchaAI) đang suy giảm.

Xử lý sự cố health check endpoint thường gặp

Vấn đề Nguyên nhân Cách xử lý
Worker liên tục bị khởi động lại Ngưỡng liveness đặt quá thấp Tăng failureThreshold hoặc periodSeconds
Worker bị đánh dấu chưa sẵn sàng ngay lúc mới khởi động Chưa có lượt giải nào nên seconds_since_last_solve bị tính là "quá lâu" Chỉ kiểm tra seconds_since_last_solve sau lần giải đầu tiên
Kiểm tra số dư làm chậm health check endpoint Gọi API ở mọi request Cache số dư với TTL (khuyến nghị 60 giây)
Bản thân health check endpoint bị crash Exception chưa được xử lý trong một check Bọc mỗi lượt kiểm tra trong try/except; trả về trạng thái suy giảm thay vì lỗi 500
Dependency check báo sai (false negative) Mạng chập chờn ngay lúc kiểm tra số dư Dùng giá trị cache theo kiểu stale-while-revalidate trong lúc xác thực lại

Ngưỡng vận hành cho từng loại probe

Ba nguyên tắc giữ cho tín hiệu health check đáng tin, thay vì trở thành cảnh báo giả trên dashboard:

  1. Dùng readiness để chặn task mới, liveness để kích hoạt restart, và cảnh báo riêng cho throughput suy giảm mà chưa fail hẳn.
  2. Gắn health check với độ sâu hàng đợi, tỷ lệ lỗi gần đây và khả năng tiếp cận dependency — đừng chỉ dựa vào uptime của process.
  3. Giữ ngưỡng (threshold) hiển thị công khai cho người trực (on-call) để mỗi lần đổi trạng thái health đều có căn cứ.

Ví dụ thực tế: fleet 20 worker của một đội QA thuê ngoài tại TP.HCM

Một đội QA thuê ngoài tại TP.HCM vận hành khoảng 20 CAPTCHA worker cho pipeline theo dõi giá trên các sàn thương mại điện tử. Trước đây, worker treo im lặng chỉ lộ ra khi kỹ sư trực đêm lướt log thủ công; sau khi thêm 3 endpoint này, Kubernetes tự phát hiện và thay worker treo trong vài giây.

Câu hỏi thường gặp

Health check endpoint có cần bảo vệ bằng authentication không?

Thường thì không — /health/* chỉ nên nghe trên network nội bộ (cluster network của Kubernetes), không expose ra internet. Nếu bắt buộc phải public, giới hạn theo IP hoặc đặt sau reverse proxy nội bộ thay vì gắn API key vào endpoint.

Vì sao worker báo readiness thất bại ngay sau khi vừa khởi động?

Vì logic đang so seconds_since_last_solve với ngưỡng MAX_SECONDS_WITHOUT_SOLVE trước khi worker kịp giải task đầu tiên. Chỉ bật điều kiện này sau khi total_solved > 0, đúng như trong ví dụ Flask/Express ở trên.

Liveness probe có nên gọi API CaptchaAI không?

Không. Chỉ readiness và dependency check mới nên gọi ra ngoài, và luôn cache kết quả. Liveness probe phải trả lời tức thì, không phụ thuộc mạng — nó chỉ chứng minh process còn sống, không chứng minh worker đang giải được task.

Ngưỡng MIN_BALANCE nên đặt bao nhiêu?

Đặt bằng chi phí ước tính cho khoảng 1–2 giờ vận hành ở tốc độ hiện tại, để có đủ thời gian nạp thêm trước khi worker thật sự hết số dư và readiness chuyển sang not_ready.

Bài viết liên quan

Bước tiếp theo

Đưa CAPTCHA worker của bạn lên production ổn định — lấy API key CaptchaAI và thêm health check endpoint ngay hôm nay.

Hướng dẫn liên quan:

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