Un worker de résolution CAPTCHA dont le processus tourne n'est pas forcément un worker qui travaille : il peut avoir épuisé son solde CaptchaAI, rester coincé sur une file saturée ou boucler sur un timeout sans plus rien résoudre. Trois endpoints de santé — liveness, readiness et vérification des dépendances — donnent à Kubernetes et à votre équilibreur de charge le signal pour redémarrer, retirer du routage ou dégrader le service. Sans eux, l'orchestrateur continue d'alimenter une instance morte.
Liveness, readiness et dépendances : trois rôles à séparer
« Le processus répond » et « le worker peut résoudre un CAPTCHA » sont deux questions distinctes, avec deux réactions différentes.
| Contrôle | Question posée | Réponse en cas d'échec |
|---|---|---|
| Liveness | Le processus répond-il encore ? | Redémarrez le conteneur |
| Readiness | Peut-il accepter du travail ? | Retirez-le du routage |
| Dépendances | Les services en amont sont-ils sains ? | Dégradez proprement |
Quelle sonde pour quel type de panne ?
- Processus totalement muet → liveness : redémarrez le conteneur.
- Worker vivant mais à écarter du trafic → readiness : sortez-le du routage sans le tuer.
- Dépendance en amont dégradée → dépendances : dégradez ou alertez avant la cascade.
- Déploiement Kubernetes ou derrière un LB → les trois sondes combinées.
Sur une flotte hébergée en région eu-west-3 (Paris) chez OVHcloud ou Scaleway, gardez la liveness locale et instantanée : un pic de latence de l'API ne doit pas déclencher de redémarrage. Côté logs, ne consignez ni clé API ni donnée personnelle (obligations RGPD).
Implémenter les endpoints en Python avec Flask
Le worker maintient un objet d'état (résolutions, échecs consécutifs, dernière résolution, solde en cache) que les trois routes Flask consultent. La liveness ne touche à rien d'externe ; la readiness interroge le solde CaptchaAI, avec un cache de 60 secondes pour ne pas transformer chaque sonde en appel réseau.
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()
Les mêmes endpoints en Node.js avec Express
Même logique en JavaScript : un objet health et trois routes qui renvoient 200 ou 503. successRate vaut 1 tant qu'aucune tâche n'a été traitée, pour ne pas pénaliser un worker au démarrage.
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"));
Brancher les sondes sur Kubernetes
Câblez ensuite les routes dans le manifeste du déploiement, chaque sonde avec son propre rythme et son seuil. Laissez initialDelaySeconds couvrir le démarrage du worker pour ne pas le déclarer défaillant trop tôt.
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
Codes de réponse : 200 ou 503, jamais 500
/health/live— 200 si le processus répond, 503 s'il est gelé (à redémarrer)./health/ready— 200 s'il peut accepter du travail, 503 sinon (arrêtez les tâches)./health/dependencies— 200 si l'amont est sain, 503 s'il est dégradé.
Ne renvoyez jamais 500 : un 500 signale un bug de l'endpoint, pas l'état du worker.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
| Worker redémarré en boucle | Seuil de liveness trop strict | Augmentez failureThreshold ou periodSeconds |
| Worker non prêt au démarrage | Aucune résolution comptée, jugée « trop ancienne » | Testez seconds_since_last_solve après la première résolution |
| Sonde ralentie par le solde | Appel API à chaque requête | Mettez le solde en cache (TTL 60 s) |
| L'endpoint de santé plante | Exception non gérée dans un contrôle | try/except par contrôle ; renvoyez « dégradé », pas un 500 |
| Faux négatifs sur les dépendances | Micro-coupure réseau pendant la vérification | Servez la valeur en cache (stale-while-revalidate) |
FAQ
Quelle différence entre une sonde liveness et une sonde readiness ?
La liveness demande « le processus est-il vivant ? » : en cas d'échec, Kubernetes redémarre le conteneur. La readiness demande « peut-il traiter une tâche ? » : en cas d'échec, il est retiré du routage sans être tué. Un worker au solde épuisé est vivant mais non prêt.
Un worker sans résolution récente est-il forcément en panne ?
Non. Au démarrage ou pendant une accalmie, un worker peut rester silencieux sans être défaillant. Le seuil seconds_since_last_solve ne doit donc être évalué qu'après la première résolution réussie.
Comment éviter que la vérification du solde ne ralentisse la sonde ?
Mettez le solde en cache (TTL 60 secondes) et servez la valeur en cache si l'appel échoue. La liveness ne doit jamais appeler l'API : réservez les appels réseau à la readiness et aux dépendances.
Peut-on exposer ces endpoints derrière un load balancer managé ?
Oui. Un LB managé (OVHcloud, Scaleway ou équivalent) interroge /health/ready pour choisir les workers qui reçoivent du trafic, comme la readinessProbe de Kubernetes. Exposez aussi une route /metrics Prometheus pour suivre la flotte dans Grafana.
Articles connexes
Prochaines étapes
Passez vos workers en production : récupérez votre clé API CaptchaAI et ajoutez vos trois sondes.
Guides associés :
- le pattern disjoncteur pour les appels d'API CAPTCHA
- le pattern cloison pour isoler la résolution CAPTCHA
- le suivi des taux de résolution avec Prometheus et Grafana