Üretimdeki bir CAPTCHA çözme hattı için felaket kurtarma planı tek bir soruyu yanıtlar: worker'larınız gece yarısı çöktüğünde kuyruktaki görevleri kaç dakika içinde ve kaç görev kaybederek geri getirebilirsiniz? İyi bir plan bu iki sayıyı — kabul edilebilir veri kaybını ve kurtarma süresini — kaza olmadan önce belirler, sonra da kurtarmayı elle değil betikle otomatikleştirir. Bu rehber; kalıcı kuyruk, kontrol noktası, yük devretme (failover) ve yazılı bir runbook ile hattınızı bir arıza sonrası dakikalar içinde ayağa kaldıran somut deseni gösterir.
Felaket kurtarma hedeflerini belirleyin: RPO, RTO, MTTR
Kurtarma otomasyonuna geçmeden önce üç hedefi sayıya dökün. Bu hedefler, "yeterince iyi kurtardık mı?" kararını duygudan çıkarıp ölçülebilir bir eşiğe bağlar.
| Metrik | Tanım | CAPTCHA hattı hedefi |
|---|---|---|
| RPO (Kurtarma Noktası Hedefi) | Kabul edilebilir maksimum veri kaybı | 5 dakikadan az sıraya alınmış görev |
| RTO (Kurtarma Süresi Hedefi) | Hizmeti geri getirmek için maksimum süre | 15 dakikadan az |
| MTTR (Ortalama Kurtarma Süresi) | Ortalama iyileşme süresi | 10 dakikadan az |
RPO'yu pratikte kontrol noktası sıklığınız belirler: her 30 saniyede bir kontrol noktası alıyorsanız, en kötü durumda yaklaşık 30 saniyelik görevi kaybedersiniz. RTO ise tespitten hizmetin geri dönmesine kadar geçen toplam süredir; onu düşük tutmanın yolu, kurtarma adımlarını klavye başında değil betikle çalıştırmaktır.
İpucu: RTO'nuzu asıl belirleyen, kurtarma adımlarının ne kadarının otomatik olduğudur. Elle atılan her adım hedefinizi dakikalarca büyütür; bu yüzden runbook'taki her komutu betiğe dönüştürmeyi amaçlayın.
Kurtarma hedeflerini işlem hattı bileşenlerine eşleyin
Tüm sistemi tek bir blok olarak ele almak yerine her işlem hattı bileşenini ayrı bir RPO ve RTO hedefine eşleyin:
- Hangi verilerin sonradan yeniden oynatılabileceğini, hangi kullanıcıya dönük akışların arıza sonrası anında kurtarılması gerektiğini tanımlayın.
- Yük devretmeyi kimin tetikleyebileceğini, kurtarmayı kimin doğrulayacağını ve normal yönlendirmeye ne zaman dönüleceğini yazılı hale getirin.
- Her bileşen için "son bilinen iyi durum"un nerede saklandığını belgeleyin; kurtarma anında bunu aramak değerli dakikaları yakar.
Hangi arıza senaryolarına hazırlanmalısınız
Kurtarma planı soyut korkulara değil, somut arıza modlarına karşı yazılır. CAPTCHA hatlarında en sık karşılaşılan beş senaryo ve her birine ilk müdahale şudur:
Scenario 1: Worker crash → Restart workers, replay queue
Scenario 2: Queue data loss → Restore from persistent backup
Scenario 3: Network partition → Failover to secondary region
Scenario 4: API key compromised → Rotate key, update workers
Scenario 5: Config corruption → Rollback to last known good
Bu senaryoların ortak yanı şudur: hiçbiri görevlerin yalnızca bellekte durmasını affetmez. Bu yüzden ilk savunma hattınız kalıcı bir kuyruktur.
Görev kalıcılığı katmanı: kuyruğu yalnızca belleğe emanet etmeyin
CAPTCHA görevlerini asla yalnızca bellek içi bir kuyruktan işlemeyin. Görevleri diske yazın ki bir çökme sonrası kaldıkları yerden devam edebilsinler. Aşağıdaki SQLite tabanlı kuyruk; görev durumunu, deneme sayısını ve sonucu kalıcı olarak tutar, yeniden başlatmada processing durumunda takılı kalan görevleri otomatik olarak yeniden sıraya alır.
Python: kalıcı görev kuyruğu
import os
import json
import time
import sqlite3
import threading
import requests
from datetime import datetime
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
class PersistentTaskQueue:
"""SQLite-backed task queue that survives crashes."""
def __init__(self, db_path="captcha_tasks.db"):
self.db_path = db_path
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.lock = threading.Lock()
self._init_db()
def _init_db(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
payload TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
started_at TEXT,
completed_at TEXT,
result TEXT,
attempts INTEGER DEFAULT 0
)
""")
self.conn.commit()
def enqueue(self, task_id, payload):
with self.lock:
self.conn.execute(
"INSERT INTO tasks (id, payload) VALUES (?, ?)",
(task_id, json.dumps(payload))
)
self.conn.commit()
def dequeue(self):
with self.lock:
cursor = self.conn.execute(
"SELECT id, payload FROM tasks "
"WHERE status = 'pending' ORDER BY created_at LIMIT 1"
)
row = cursor.fetchone()
if not row:
return None
task_id, payload = row
self.conn.execute(
"UPDATE tasks SET status = 'processing', "
"started_at = ?, attempts = attempts + 1 WHERE id = ?",
(datetime.utcnow().isoformat(), task_id)
)
self.conn.commit()
return {"id": task_id, "payload": json.loads(payload)}
def complete(self, task_id, result):
with self.lock:
self.conn.execute(
"UPDATE tasks SET status = 'completed', "
"completed_at = ?, result = ? WHERE id = ?",
(datetime.utcnow().isoformat(), json.dumps(result), task_id)
)
self.conn.commit()
def fail(self, task_id, error):
with self.lock:
# Requeue if under retry limit
cursor = self.conn.execute(
"SELECT attempts FROM tasks WHERE id = ?", (task_id,)
)
row = cursor.fetchone()
if row and row[0] < 3:
self.conn.execute(
"UPDATE tasks SET status = 'pending' WHERE id = ?",
(task_id,)
)
else:
self.conn.execute(
"UPDATE tasks SET status = 'failed', "
"result = ? WHERE id = ?",
(json.dumps({"error": error}), task_id)
)
self.conn.commit()
def recover_stale(self, timeout_seconds=600):
"""Reset tasks stuck in 'processing' after a crash."""
with self.lock:
cutoff = datetime.utcnow().timestamp() - timeout_seconds
self.conn.execute(
"UPDATE tasks SET status = 'pending' "
"WHERE status = 'processing' "
"AND started_at < datetime(?, 'unixepoch')",
(cutoff,)
)
count = self.conn.total_changes
self.conn.commit()
return count
@property
def stats(self):
cursor = self.conn.execute(
"SELECT status, COUNT(*) FROM tasks GROUP BY status"
)
return dict(cursor.fetchall())
# On startup: recover tasks that were processing during a crash
queue = PersistentTaskQueue()
recovered = queue.recover_stale(timeout_seconds=600)
print(f"Recovered {recovered} stale tasks after restart")
recover_stale yöntemi buradaki kritik parçadır: bir worker görev üzerindeyken çökerse, o görev sonsuza kadar processing durumunda asılı kalmaz; belirlenen zaman aşımından sonra yeniden pending yapılır ve başka bir worker devralır.
Kontrol noktası ve dayanıklı çözücü
Kalıcı kuyruk tek başına yeterli değildir; toplu işlerin ilerlemesini de düzenli aralıklarla diske almalısınız. Aşağıdaki yönetici dört işi üstlenir:
- Her toplu işlemden önce bir
batch-pendingkontrol noktası yazar. - Her 10 görevde bir ilerleme kontrol noktası alır.
getbalanceçağrısıyla API sağlığını yoklar.- Yeniden başlatmada
recover()ile yarım kalan işi bulur ve yalnızca tamamlanmamış görevleri sürdürür.
JavaScript: kontrol noktası ve kurtarma yöneticisi
const axios = require("axios");
const fs = require("fs");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
class DisasterRecoveryManager {
constructor(checkpointDir = "./dr-checkpoints") {
this.checkpointDir = checkpointDir;
if (!fs.existsSync(checkpointDir)) {
fs.mkdirSync(checkpointDir, { recursive: true });
}
}
checkpoint(label, data) {
const filename = `${this.checkpointDir}/${label}-${Date.now()}.json`;
fs.writeFileSync(filename, JSON.stringify(data, null, 2));
this.pruneOldCheckpoints(label, 10); // Keep last 10
return filename;
}
restore(label) {
const files = fs.readdirSync(this.checkpointDir)
.filter((f) => f.startsWith(label) && f.endsWith(".json"))
.sort()
.reverse();
if (files.length === 0) return null;
const latest = fs.readFileSync(
`${this.checkpointDir}/${files[0]}`, "utf8"
);
return JSON.parse(latest);
}
pruneOldCheckpoints(label, keep) {
const files = fs.readdirSync(this.checkpointDir)
.filter((f) => f.startsWith(label) && f.endsWith(".json"))
.sort();
while (files.length > keep) {
const old = files.shift();
fs.unlinkSync(`${this.checkpointDir}/${old}`);
}
}
async healthCheck() {
try {
const resp = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "getbalance", json: 1 },
timeout: 10000,
});
return {
healthy: resp.data.status === 1,
balance: parseFloat(resp.data.request || 0),
};
} catch (err) {
return { healthy: false, error: err.message };
}
}
}
class ResilientSolver {
constructor() {
this.dr = new DisasterRecoveryManager();
this.pendingTasks = [];
}
async solveBatch(tasks) {
// Checkpoint before starting
this.dr.checkpoint("batch-pending", {
tasks,
startedAt: new Date().toISOString(),
});
const results = [];
for (const task of tasks) {
try {
const result = await this.solveSingle(task);
results.push({ taskId: task.id, ...result });
} catch (err) {
results.push({ taskId: task.id, error: err.message });
}
// Checkpoint progress periodically
if (results.length % 10 === 0) {
this.dr.checkpoint("batch-progress", { results, remaining: tasks.length - results.length });
}
}
// Final checkpoint
this.dr.checkpoint("batch-complete", { results });
return results;
}
async recover() {
// Check for incomplete batch
const progress = this.dr.restore("batch-progress");
const pending = this.dr.restore("batch-pending");
if (progress) {
const completedIds = new Set(progress.results.map((r) => r.taskId));
const remaining = pending?.tasks.filter((t) => !completedIds.has(t.id));
console.log(
`Recovering: ${progress.results.length} done, ${remaining?.length || 0} remaining`
);
return remaining || [];
}
if (pending) {
console.log(`Recovering full batch: ${pending.tasks.length} tasks`);
return pending.tasks;
}
return [];
}
async solveSingle(task) {
const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: task.sitekey,
pageurl: task.pageurl,
json: 1,
},
});
if (resp.data.status !== 1) throw new Error(resp.data.request);
const captchaId = resp.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) return { solution: poll.data.request };
if (poll.data.request !== "CAPCHA_NOT_READY")
throw new Error(poll.data.request);
}
throw new Error("TIMEOUT");
}
}
// Start with recovery check
const solver = new ResilientSolver();
solver.recover().then((remaining) => {
if (remaining.length > 0) {
console.log(`Resuming ${remaining.length} tasks from checkpoint`);
solver.solveBatch(remaining);
}
});
Buradaki idempotency mantığına dikkat edin: kurtarma sırasında tamamlanmış görev kimlikleri bir küme içinde tutulur, böylece aynı CAPTCHA iki kez çözülmez. Bu, hem gereksiz maliyeti hem de hedef sistemde çift gönderim riskini ortadan kaldırır.
Bir kesintiyi somutlaştıran senaryo: İstanbul'da ödeme akışı QA'sı
İstanbul'daki bir e-ticaret ekibinin ödeme akışı QA otomasyonunu düşünün. Gece bakım penceresinde (Europe/Istanbul) worker'lar reCAPTCHA v2 ve Cloudflare Turnstile görevlerini işlerken bölgesel bir ağ kesintisi yaşanıyor. Kalıcı kuyruk olmasaydı, kuyruktaki yüzlerce görev ve bunların token'ları buharlaşır, QA koşusu baştan başlardı. Kalıcı kuyruk ve kontrol noktasıyla ekip, kesinti bittiğinde yalnızca yarım kalan görevleri yeniden oynatır ve RTO hedefinin altında kalır.
İki yerel ayrıntı bu tasarımı daha da önemli kılar. Birincisi maliyet: CaptchaAI thread bazlı ücretlendirir ve TL kurundaki oynaklık göz önüne alındığında öngörülebilir aylık USD maliyeti gerçek bir avantajdır. Kurtarma sırasında biriken kuyruğu hızla eritmek için yeterli eşzamanlılık isterseniz, örneğin ADVANCE ($90/ay, 50 thread) planı yüksek hacimli bir DR senaryosunu rahatça karşılar; daha küçük hatlar BASIC ($15/ay, 5 thread) ile başlayabilir. İkincisi uyumluluk: kazıma veya QA akışlarınız kişisel veri içeriyorsa KVKK yükümlülükleri devreye girer — kurtarma ve yeniden oynatma süreçlerinizi yalnızca yetkili olduğunuz ve sahibi bulunduğunuz veriler üzerindeki iş akışlarıyla sınırlayın.
Felaket kurtarma runbook şablonu
Koddan az önemli olmayan şey, kaza anında kimin ne yapacağını gösteren yazılı bir runbook'tur. Panikle karar vermek yerine adımları izlersiniz. Aşağıdaki şablonu ekibinizin alarm ve dağıtım araçlarına göre uyarlayın:
RUNBOOK: CAPTCHA Pipeline Recovery
====================================
1. DETECT
- Alert fires: [PagerDuty / Slack / Email]
- Symptom: [Queue growing / Workers offline / Error spike]
2. ASSESS
- Check worker health: curl http://workers/health
- Check API status: GET /res.php?action=getbalance
- Check queue depth: SELECT COUNT(*) FROM tasks WHERE status='pending'
3. RECOVER
If: Workers crashed
→ Restart worker containers: docker-compose up -d workers
→ Run stale task recovery: recovery.py --recover-stale
If: Network partition
→ Failover to secondary region
→ Update DNS or load balancer routing
If: API key compromised
→ Generate new key at captchaai.com
→ Update secret store
→ Rolling restart workers
4. VERIFY
- Confirm solve rate > 90%
- Confirm queue draining
- Confirm no duplicate solves
5. POST-MORTEM
- Document root cause
- Update runbook if needed
Sorun giderme
| Sorun | Sebep | Düzeltme |
|---|---|---|
| Çökme sırasında kaybedilen görevler | Yalnızca bellek içi kuyruk | Kalıcı kuyruk kullanın (SQLite veya AOF'lu Redis) |
| Kurtarma sonrası yinelenen çözümler | Eski görevler tekilleştirilmeden yeniden işlendi | idempotency anahtarları ekleyin; görevin zaten çözülüp çözülmediğini kontrol edin |
| Kurtarma süresi RTO'yu aşıyor | Veritabanı yedeği çok eski | Kontrol noktası sıklığını artırın |
| Yanlış bölgeye yük devretme | DNS TTL'si çok yüksek | Planlı yük devretmelerden önce TTL'yi 60 saniyeye düşürün |
Sık sorulan sorular
Felaket kurtarma planımı ne sıklıkla test etmeliyim?
En az üç ayda bir, tercihen otomatik bir tatbikatla. Yazılmış ama hiç çalıştırılmamış bir runbook kaza anında güvenilmezdir; worker'ları kasıtlı olarak durdurup kurtarmanın RTO hedefinizin altında kaldığını doğrulayın.
RPO ve RTO hedeflerimi nasıl belirlerim?
İş etkisinden geriye doğru çalışın: kaç dakikalık görev kaybını ve kaç dakikalık kesintiyi kabul edebilirsiniz? RPO'yu kontrol noktası sıklığınız, RTO'yu ise kurtarma adımlarının ne kadarının otomatik olduğu belirler. Elle müdahale ne kadar azsa RTO o kadar düşer.
Kurtarma sonrası yinelenen çözümleri nasıl önlerim?
Her göreve bir idempotency anahtarı verin ve işlemeden önce görevin zaten çözülüp çözülmediğini kontrol edin. Yukarıdaki kurtarma yöneticisi bunu, tamamlanmış görev kimliklerini bir küme içinde tutarak yapar; böylece aynı CAPTCHA iki kez gönderilmez.
CaptchaAI tarafında geçici bir kesinti olursa ne yapmalıyım?
Görevleri yerelde sıraya alın ve API geri geldiğinde yeniden deneyin. İşlem hattınız geçici kullanılamama durumlarını devre kesiciler ve üstel geri çekilme (exponential backoff) ile zarif biçimde ele almalı; kuyruk kalıcı olduğu için bekleyen görevler kaybolmaz.
Sonraki adımlar
En kötü senaryoyu bugünden planlayın: CaptchaAI API anahtarınızı alın ve felaket kurtarmayı işlem hattınıza ilk günden itibaren yerleştirin.
İlgili rehberler: