La mise à l'échelle verticale (serveur plus gros) atteint un plafond. La mise à l'échelle horizontale (plus de travailleurs) permet à votre capacité de résolution de CAPTCHA d'augmenter linéairement avec la demande. La clé est de savoir quand mettre à l’échelle et automatiser le processus.
Quand effectuer une mise à l'échelle horizontale
| Signal | Seuil | Action |
|---|---|---|
| La profondeur de la file d’attente augmente | > 50 tâches en attente | Ajouter des travailleurs |
| Latence de résolution moyenne | > 45 secondes | Ajouter des travailleurs (l'API n'est pas le goulot d'étranglement) |
| Utilisation du processeur du travailleur | > 70% soutenu | Ajouter des travailleurs |
| Taux d'erreur | > 5% avec ERROR_NO_SLOT_AVAILABLE |
Trop de tâches simultanées par travailleur |
| La file d'attente se vide lentement | Objectif de débit < 80 % | Ajoutez des travailleurs ou augmentez la simultanéité |
Architecture à grande échelle
[Queue Monitor] ──watches──→ [Task Queue]
│ ↕
│ scale signal [Worker 1]
↓ [Worker 2]
[Auto Scaler] ──adds──→ [Worker 3]
│ [Worker N...]
↓
[Cost Manager] ──caps──→ max workers
Python – Contrôleur de mise à l'échelle automatique
import os
import time
import math
import threading
import subprocess
import requests
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
class ScalingMetrics:
"""Collect metrics that drive scaling decisions."""
def __init__(self):
self.queue_depth = 0
self.active_workers = 0
self.tasks_per_minute = 0
self.avg_solve_time = 30 # seconds
self.error_rate = 0.0
self.lock = threading.Lock()
def update(self, queue_depth, active_workers, tasks_per_minute,
avg_solve_time, error_rate):
with self.lock:
self.queue_depth = queue_depth
self.active_workers = active_workers
self.tasks_per_minute = tasks_per_minute
self.avg_solve_time = avg_solve_time
self.error_rate = error_rate
@property
def snapshot(self):
with self.lock:
return {
"queue_depth": self.queue_depth,
"active_workers": self.active_workers,
"tasks_per_minute": self.tasks_per_minute,
"avg_solve_time": self.avg_solve_time,
"error_rate": self.error_rate,
}
class HorizontalAutoScaler:
def __init__(self, min_workers=2, max_workers=20,
tasks_per_worker=10, cooldown=120):
self.min_workers = min_workers
self.max_workers = max_workers
self.tasks_per_worker = tasks_per_worker
self.cooldown = cooldown
self.current_workers = min_workers
self.last_scale_time = 0
self.metrics = ScalingMetrics()
def calculate_desired_workers(self):
snapshot = self.metrics.snapshot
# Method 1: Queue-based scaling
queue_based = math.ceil(
snapshot["queue_depth"] / self.tasks_per_worker
)
# Method 2: Throughput-based scaling
if snapshot["tasks_per_minute"] > 0 and snapshot["queue_depth"] > 0:
drain_time = snapshot["queue_depth"] / snapshot["tasks_per_minute"]
if drain_time > 5: # More than 5 minutes to drain
throughput_based = self.current_workers + 2
else:
throughput_based = self.current_workers
else:
throughput_based = self.current_workers
# Method 3: Error-rate scaling (reduce if errors are high)
if snapshot["error_rate"] > 0.1:
error_based = max(
self.min_workers,
self.current_workers - 1
)
else:
error_based = self.current_workers
# Take the maximum of queue and throughput based, limited by error
desired = max(queue_based, throughput_based)
if snapshot["error_rate"] > 0.1:
desired = min(desired, error_based)
# Clamp to bounds
return max(self.min_workers, min(self.max_workers, desired))
def should_scale(self, desired):
if desired == self.current_workers:
return False
if time.time() - self.last_scale_time < self.cooldown:
return False
return True
def scale(self, desired):
if not self.should_scale(desired):
return
direction = "up" if desired > self.current_workers else "down"
diff = abs(desired - self.current_workers)
print(f"Scaling {direction}: {self.current_workers} → {desired} "
f"(+{diff if direction == 'up' else -diff})")
if direction == "up":
self._add_workers(diff)
else:
self._remove_workers(diff)
self.current_workers = desired
self.last_scale_time = time.time()
def _add_workers(self, count):
"""Launch new worker containers."""
for i in range(count):
worker_id = f"captcha-worker-{self.current_workers + i}"
# In production: use Docker API, K8s API, or cloud SDK
print(f" Launching {worker_id}")
def _remove_workers(self, count):
"""Drain and stop workers."""
for i in range(count):
worker_id = f"captcha-worker-{self.current_workers - 1 - i}"
print(f" Draining and removing {worker_id}")
def run_loop(self, interval=30):
"""Main auto-scaling loop."""
print(f"Auto-scaler started: min={self.min_workers}, "
f"max={self.max_workers}")
while True:
desired = self.calculate_desired_workers()
self.scale(desired)
snapshot = self.metrics.snapshot
print(f" Workers: {self.current_workers}, "
f"Queue: {snapshot['queue_depth']}, "
f"TPM: {snapshot['tasks_per_minute']}, "
f"Errors: {snapshot['error_rate']:.1%}")
time.sleep(interval)
# Start auto-scaler
scaler = HorizontalAutoScaler(
min_workers=2,
max_workers=20,
tasks_per_worker=10,
cooldown=120 # 2-minute cooldown between scaling
)
# Run in background
scaling_thread = threading.Thread(target=scaler.run_loop, daemon=True)
scaling_thread.start()
JavaScript - Mise à l'échelle horizontale basée sur Docker
const { exec } = require("child_process");
const { promisify } = require("util");
const execAsync = promisify(exec);
class DockerHorizontalScaler {
constructor(options = {}) {
this.serviceName = options.serviceName || "captcha-worker";
this.minReplicas = options.minReplicas || 2;
this.maxReplicas = options.maxReplicas || 15;
this.currentReplicas = this.minReplicas;
this.scaleUpThreshold = options.scaleUpThreshold || 50;
this.scaleDownThreshold = options.scaleDownThreshold || 10;
this.cooldownMs = options.cooldownMs || 120000;
this.lastScaleTime = 0;
}
async evaluate(metrics) {
const now = Date.now();
if (now - this.lastScaleTime < this.cooldownMs) {
return { action: "cooldown", current: this.currentReplicas };
}
let desired = this.currentReplicas;
// Scale up: queue growing
if (metrics.queueDepth > this.scaleUpThreshold) {
const needed = Math.ceil(metrics.queueDepth / 10);
desired = Math.min(this.maxReplicas, Math.max(desired, needed));
}
// Scale down: queue mostly empty
if (
metrics.queueDepth < this.scaleDownThreshold &&
this.currentReplicas > this.minReplicas
) {
desired = Math.max(this.minReplicas, this.currentReplicas - 1);
}
if (desired !== this.currentReplicas) {
await this.scaleTo(desired);
return { action: "scaled", from: this.currentReplicas, to: desired };
}
return { action: "no_change", current: this.currentReplicas };
}
async scaleTo(replicas) {
const clamped = Math.max(
this.minReplicas,
Math.min(this.maxReplicas, replicas)
);
console.log(`Scaling ${this.serviceName}: ${this.currentReplicas} → ${clamped}`);
try {
// Docker Compose scaling
await execAsync(
`docker compose up -d --scale ${this.serviceName}=${clamped} --no-recreate`
);
this.currentReplicas = clamped;
this.lastScaleTime = Date.now();
} catch (err) {
console.error(`Scale failed: ${err.message}`);
}
}
status() {
return {
service: this.serviceName,
current: this.currentReplicas,
min: this.minReplicas,
max: this.maxReplicas,
lastScale: new Date(this.lastScaleTime).toISOString(),
};
}
}
// Monitor loop
const scaler = new DockerHorizontalScaler({
serviceName: "captcha-worker",
minReplicas: 2,
maxReplicas: 15,
cooldownMs: 120000,
});
async function monitorAndScale() {
// In production, fetch from your queue/monitoring system
const metrics = {
queueDepth: 75, // Example
errorRate: 0.02,
avgSolveTime: 25,
};
const result = await scaler.evaluate(metrics);
console.log("Scale decision:", result);
console.log("Status:", scaler.status());
}
setInterval(monitorAndScale, 30000);
Mise à l'échelle soucieuse des coûts
class CostAwareScaler(HorizontalAutoScaler):
def __init__(self, hourly_cost_per_worker=0.05, budget_per_hour=2.0,
**kwargs):
super().__init__(**kwargs)
self.hourly_cost = hourly_cost_per_worker
self.budget = budget_per_hour
def calculate_desired_workers(self):
desired = super().calculate_desired_workers()
# Cap by budget
max_affordable = int(self.budget / self.hourly_cost)
if desired > max_affordable:
print(f" Budget cap: wanted {desired}, "
f"can afford {max_affordable}")
desired = max_affordable
return desired
Liste de contrôle de mise à l'échelle
| Zone | Considérez |
|---|---|
| File d'attente | File d'attente persistante (Redis, SQS) - pas en mémoire |
| Travailleurs | Apatride : n'importe quel travailleur s'acquitte de n'importe quelle tâche |
| Bilans de santé | L'équilibreur de charge sait quels travailleurs sont en bonne santé |
| Vidange | Les travailleurs terminent leurs tâches en vol avant l'arrêt |
| Surveillance | Profondeur de file d'attente, latence, taux d'erreur visibles |
| Coût | Les plafonds budgétaires empêchent une mise à l’échelle incontrôlée |
Dépannage
| Problème | Parce que | Corriger |
|---|---|---|
| Oscillation de mise à l'échelle | Seuil trop proche de la charge actuelle | Ajouter une hystérésis : augmentation à 50, réduction à 10 |
| Les nouveaux travailleurs n’aident pas | L'API CaptchaAI est le goulot d'étranglement | Vérifiez les limites de débit de l'API ; optimiser la simultanéité par travailleur |
| Les travailleurs sont inactifs après la mise à l’échelle | La file d'attente s'est épuisée avant que les nouveaux travailleurs ne soient prêts | Réduisez le temps de recharge ; échelle par petits incréments |
| Hausse des coûts | Pas de plafond max_workers | Définissez toujours max_workers ; ajouter des limites budgétaires |
FAQ
Quand dois-je plutôt effectuer une mise à l’échelle verticale ?
Évoluez verticalement (serveur plus grand) lorsque le goulot d'étranglement concerne chaque processus (prétraitement d'image lié au processeur, mémoire pour les instances de navigateur). Évoluez horizontalement lorsque le goulot d'étranglement est le débit (plus de tâches qu'un processus ne peut gérer).
De combien de travailleurs ai-je besoin ?
Formule approximative : workers = peak_tasks_per_minute × avg_solve_time_seconds / 60 / tasks_per_worker. Pour 100 tâches/minute à 30s, résolvez le temps avec 10 tâches par travailleur : 100 × 30 / 60 / 10 = 5 workers.
Dois-je utiliser Kubernetes pour la mise à l’échelle automatique ?
Kubernetes HPA (Horizontal Pod Autoscaler) fonctionne bien pour les travailleurs CAPTCHA. Utilisez des métriques personnalisées (profondeur de file d'attente) plutôt qu'une mise à l'échelle basée sur le processeur pour une mise à l'échelle automatique plus réactive.
Prochaines étapes
Adaptez votre résolution CAPTCHA à n'importe quel débit -récupérez votre clé API CaptchaAIet mettre en œuvre une mise à l’échelle automatique horizontale.
Guides associés :
- Travailleurs à mise à l'échelle automatique
- Architecture d'équilibrage de charge
- Architecture multi-régions