Lambda üzerinde CAPTCHA çözen bir iş akışında en kritik karar, çözüm sonuçlarını nereye yazacağınızdır. Çoğu sunucusuz kurulumda yanıt DynamoDB'dir: açıp kapatacağınız bir bağlantı havuzu yoktur, süresi dolan kayıtları TTL otomatik siler ve trafik ister onda birine insin ister on katına çıksın gecikme sabit kalır. Bu rehberde tablo tasarımını, öğe yapısını ve sorgu kalıplarını uçtan uca kuracaksınız.
Örnek olarak İstanbul merkezli bir e-ticaret ekibinde ödeme akışının QA otomasyonunu yazdığınızı düşünün: her test koşusu bir doğrulama tetikler, siz de hangi çözümün ne kadar sürdüğünü ve hangilerinin hata döndürdüğünü kayıt altına almak istersiniz. DynamoDB bu izleme katmanını Lambda'nın yanında, ek altyapı yönetmeden çalıştırır.
Neden DynamoDB, RDS Değil?
Sunucusuz mimaride en büyük sürtünme bağlantı yönetimidir. RDS gibi bağlantı tabanlı bir veritabanı, her Lambda çağrısının yeni bir bağlantı açması nedeniyle havuz katmanı (RDS Proxy) gerektirir; bu da ek maliyet ve yapılandırma demektir. DynamoDB bu sorunu üç noktada ortadan kaldırır:
- Bağlantısızlık: Her çağrı bağımsız bir HTTP isteğidir; eşzamanlı yüzlerce Lambda örneği aynı tabloyu ek ayar olmadan kullanır.
- Yerleşik TTL: Süresi dolan çözüm kayıtları kendiliğinden temizlenir, arka planda cron işi çalıştırmazsınız.
- Talebe göre ölçek: İsteğe bağlı faturalandırma yalnızca gerçekleşen okuma ve yazma kadar ücret çıkarır.
Bu üç özellik, DynamoDB'yi CAPTCHA izleme için sunucusuzun doğal seçeneği yapar.
Tablo Tasarımı
Tek Tablo Modeli
Tek bir DynamoDB tablosu; çözüm geçmişini, uçuştaki görevleri ve toplu istatistikleri birlikte tutar. Erişim kalıplarını bölüm ve sıralama anahtarına gömerek ayrı tablolar yönetme yükünden kurtulursunuz:
| Bölüm Anahtarı (PK) | Sıralama Anahtarı (SK) | Ne için |
|---|---|---|
SOLVE#{captcha_id} |
META |
Tekil çözüm kaydı |
SITE#{sitekey} |
SOLVE#{timestamp} |
Sitekey başına çözüm geçmişi |
STATS#{date} |
TYPE#{captcha_type} |
Gün bazında toplu istatistik |
ACTIVE#{captcha_id} |
TASK |
Uçuştaki görevin takibi |
Öğe Yapısı ve TTL Pencereleri
Tabloda birbirinden bağımsız iki TTL penceresi çalışır. Uçuştaki görev kaydı (ACTIVE#) yalnızca 10 dakikalık kısa bir TTL taşır; çözüm tamamlandığında kayıt zaten silindiği için bu, takılı kalan görevlere karşı bir güvenlik ağıdır. Kalıcı çözüm kayıtları ve sitekey geçmişi ise 90 günlük TTL ile saklanır — trend analizine yetecek kadar uzun, süresiz depolama maliyeti çıkarmayacak kadar kısa. Bu ayrım sayesinde etkin görev tablosu her zaman küçük ve hızlı kalır.
Tablo Tanımı
TTL, tek bir GSI ve isteğe bağlı faturalandırmayı açık tutan tanım şöyledir:
{
"TableName": "CaptchaSolves",
"KeySchema": [
{ "AttributeName": "PK", "KeyType": "HASH" },
{ "AttributeName": "SK", "KeyType": "RANGE" }
],
"AttributeDefinitions": [
{ "AttributeName": "PK", "KeyType": "S" },
{ "AttributeName": "SK", "KeyType": "S" },
{ "AttributeName": "GSI1PK", "KeyType": "S" },
{ "AttributeName": "GSI1SK", "KeyType": "S" }
],
"GlobalSecondaryIndexes": [
{
"IndexName": "GSI1",
"KeySchema": [
{ "AttributeName": "GSI1PK", "KeyType": "HASH" },
{ "AttributeName": "GSI1SK", "KeyType": "RANGE" }
],
"Projection": { "ProjectionType": "ALL" }
}
],
"BillingMode": "PAY_PER_REQUEST",
"TimeToLiveSpecification": {
"AttributeName": "ttl",
"Enabled": true
}
}
Python ile Uygulama
Kurulum
Kaynağı ve tabloyu işlev kapsamının dışında (global) oluşturun; böylece sıcak çağrılar aynı istemciyi yeniden kullanır ve soğuk başlangıç maliyetini her istekte tekrar ödemezsiniz:
import os
import time
from datetime import datetime, timezone
import boto3
import requests
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ.get("DYNAMODB_TABLE", "CaptchaSolves"))
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
Çözümü Gönderin ve Takip Edin
Ana fonksiyon akışı üç adıma iner:
- Görevi
in.phpüzerinden CaptchaAI'ye gönderir ve dönencaptcha_iddeğeriniACTIVE#kaydıyla işaretler. - Sonucu
res.phpüzerinden beş saniyelik aralıklarla sorgular. - Çözüm gelince kalıcı
SOLVE#veSITE#kayıtlarını yazar, uçuştaki görevi siler ve günlük istatistiği günceller.
Uçuştaki görev kısa ömürlü bir TTL taşıdığı için, akış bir yerde takılsa bile kayıt kendiliğinden temizlenir:
def solve_and_track(sitekey, pageurl, captcha_type="recaptcha_v2", project=None):
now = datetime.now(timezone.utc)
timestamp = now.isoformat()
ttl_90_days = int(now.timestamp()) + (90 * 24 * 3600)
# Submit to CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
# Store error record
table.put_item(Item={
"PK": f"SITE#{sitekey}",
"SK": f"SOLVE#{timestamp}",
"captcha_type": captcha_type,
"pageurl": pageurl,
"status": "error",
"error": data.get("request"),
"submitted_at": timestamp,
"project": project or "default",
"ttl": ttl_90_days,
"GSI1PK": f"STATUS#error",
"GSI1SK": timestamp
})
return {"error": data.get("request")}
captcha_id = data["request"]
# Track active task
table.put_item(Item={
"PK": f"ACTIVE#{captcha_id}",
"SK": "TASK",
"sitekey": sitekey,
"pageurl": pageurl,
"captcha_type": captcha_type,
"submitted_at": timestamp,
"ttl": int(now.timestamp()) + 600 # Auto-clean in 10 min
})
# Poll for result
polls = 0
for _ in range(60):
time.sleep(5)
polls += 1
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
solved_at = datetime.now(timezone.utc).isoformat()
elapsed_ms = int(
(datetime.now(timezone.utc) - now).total_seconds() * 1000
)
# Store success record
table.put_item(Item={
"PK": f"SOLVE#{captcha_id}",
"SK": "META",
"captcha_type": captcha_type,
"sitekey": sitekey,
"pageurl": pageurl,
"status": "solved",
"submitted_at": timestamp,
"solved_at": solved_at,
"elapsed_ms": elapsed_ms,
"polls": polls,
"project": project or "default",
"ttl": ttl_90_days,
"GSI1PK": f"STATUS#solved",
"GSI1SK": timestamp
})
# Also store in site history
table.put_item(Item={
"PK": f"SITE#{sitekey}",
"SK": f"SOLVE#{timestamp}",
"captcha_id": captcha_id,
"status": "solved",
"elapsed_ms": elapsed_ms,
"ttl": ttl_90_days
})
# Remove active task
table.delete_item(Key={
"PK": f"ACTIVE#{captcha_id}", "SK": "TASK"
})
# Update daily stats
update_daily_stats(captcha_type, True, elapsed_ms)
return {"solution": result["request"]}
if result.get("request") != "CAPCHA_NOT_READY":
table.put_item(Item={
"PK": f"SITE#{sitekey}",
"SK": f"SOLVE#{timestamp}",
"captcha_id": captcha_id,
"status": "error",
"error": result.get("request"),
"ttl": ttl_90_days
})
table.delete_item(Key={
"PK": f"ACTIVE#{captcha_id}", "SK": "TASK"
})
update_daily_stats(captcha_type, False, 0)
return {"error": result.get("request")}
table.delete_item(Key={"PK": f"ACTIVE#{captcha_id}", "SK": "TASK"})
update_daily_stats(captcha_type, False, 0)
return {"error": "TIMEOUT"}
def update_daily_stats(captcha_type, success, elapsed_ms):
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
update_expr = "SET total_solves = if_not_exists(total_solves, :zero) + :one"
expr_values = {":zero": 0, ":one": 1}
if success:
update_expr += ", successful = if_not_exists(successful, :zero) + :one"
update_expr += ", total_elapsed = if_not_exists(total_elapsed, :zero) + :elapsed"
expr_values[":elapsed"] = elapsed_ms
else:
update_expr += ", failed = if_not_exists(failed, :zero) + :one"
table.update_item(
Key={"PK": f"STATS#{date_str}", "SK": f"TYPE#{captcha_type}"},
UpdateExpression=update_expr,
ExpressionAttributeValues=expr_values
)
Atomik update_item ifadesi sayaçları eşzamanlı çağrılar arasında güvenle artırır.
İpucu:
elapsed_msvepollsalanlarını her başarılı çözümde yazmak, ileride çözüm süresi trendlerini tek sorguyla çıkarmanızı sağlar — sonradan şema değiştirmek zorunda kalmazsınız.
Sorgu Kalıpları
İzleme katmanının değeri sorgulardadır. Şu üç kalıp panonuzu beslemeye yeter: sitekey geçmişi, günlük istatistik ve uçuştaki görevler.
def get_site_history(sitekey, limit=50):
"""Get recent solves for a specific site key."""
response = table.query(
KeyConditionExpression="PK = :pk",
ExpressionAttributeValues={":pk": f"SITE#{sitekey}"},
ScanIndexForward=False,
Limit=limit
)
return response["Items"]
def get_daily_stats(date_str=None):
"""Get stats for a specific date (default: today)."""
if not date_str:
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
response = table.query(
KeyConditionExpression="PK = :pk",
ExpressionAttributeValues={":pk": f"STATS#{date_str}"}
)
return response["Items"]
def get_active_tasks():
"""List all currently active CAPTCHA tasks."""
response = table.query(
IndexName="GSI1",
KeyConditionExpression="GSI1PK = :pk",
ExpressionAttributeValues={":pk": "STATUS#polling"}
)
return response["Items"]
Node.js ile Uygulama
Aynı akışın AWS SDK v3 ve Axios ile Node.js karşılığı şöyledir:
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand, QueryCommand, UpdateCommand } = require("@aws-sdk/lib-dynamodb");
const axios = require("axios");
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.DYNAMODB_TABLE || "CaptchaSolves";
const API_KEY = process.env.CAPTCHAAI_API_KEY;
async function solveAndTrack(sitekey, pageurl, type = "recaptcha_v2") {
const now = new Date();
const timestamp = now.toISOString();
const ttl = Math.floor(now.getTime() / 1000) + 90 * 24 * 3600;
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: { key: API_KEY, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
await client.send(new PutCommand({
TableName: TABLE,
Item: { PK: `SITE#${sitekey}`, SK: `SOLVE#${timestamp}`, status: "error", error: submit.data.request, ttl },
}));
return { error: submit.data.request };
}
const captchaId = submit.data.request;
let polls = 0;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
polls++;
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) {
const elapsed = Date.now() - now.getTime();
await client.send(new PutCommand({
TableName: TABLE,
Item: {
PK: `SOLVE#${captchaId}`, SK: "META", captcha_type: type,
sitekey, pageurl, status: "solved", submitted_at: timestamp,
solved_at: new Date().toISOString(), elapsed_ms: elapsed, polls, ttl,
},
}));
return { solution: poll.data.request };
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
return { error: poll.data.request };
}
}
return { error: "TIMEOUT" };
}
async function getSiteHistory(sitekey, limit = 50) {
const result = await client.send(new QueryCommand({
TableName: TABLE,
KeyConditionExpression: "PK = :pk",
ExpressionAttributeValues: { ":pk": `SITE#${sitekey}` },
ScanIndexForward: false,
Limit: limit,
}));
return result.Items;
}
Sık Karşılaşılan Sorunlar
Üretime almadan önce dört tuzağı gözden geçirin; hepsi tabloyu yanlış anahtar tasarımıyla kurmaktan kaynaklanır:
| Sorun | Sebep | Çözüm |
|---|---|---|
ProvisionedThroughputExceededException |
Saniyede çok fazla yazma | İsteğe bağlı faturalandırmaya geçin ya da WCU'yu artırın |
| TTL öğeleri anında silinmiyor | DynamoDB'de TTL silmesi nihaidir (~48 saat) | Gerçek zamanlı temizlik için TTL'ye güvenmeyin; süresi dolan öğeleri sorguda filtreleyin |
STATS#{date} üzerinde sıcak bölüm |
Tüm worker'lar aynı bölüme yazıyor | Rastgele son ek ekleyin: STATS#{date}#shard{0-9} |
| Sorgu çok fazla öğe döndürüyor | Bölüm anahtarı fazla geniş | SK koşulları ekleyerek sonucu daraltın |
Maliyeti Düşük Tutmak
Sıcak bölüm ve throughput sorunlarını çözdükten sonra, faturayı da birkaç ayarla düşük tutabilirsiniz. En büyük tasarruf isteğe bağlı faturalandırma ve TTL'yi baştan açık tutmaktan gelir:
| Yöntem | Etkisi |
|---|---|
| Değişken iş yüklerinde isteğe bağlı faturalandırma kullanın | Kapasiteyi önceden ayırmazsınız |
| Otomatik kayıt temizliği için TTL'yi etkinleştirin | Depolama maliyetini düşürür |
| Sorgularda yalnızca ihtiyaç duyulan nitelikleri projeleyin | Okuma ünitesi tüketimini azaltır |
BatchWriteItem ile toplu yazın |
Daha az API çağrısı |
| Analiz için DynamoDB Streams'i kullanın | Toplama yükünü Lambda'ya devreder |
Not: Aşağıdaki maliyet kalemleri DynamoDB tarafındadır; CaptchaAI çözüm ücreti ayrı ve thread bazlıdır. İkisini tek faturada karıştırmayın.
Günde 10.000 çözümlük bir iş yükünde depolama ve erişim aylık 1 doların altında kalır; asıl bütçe kalemi CaptchaAI thread planınızdır. Türkiye'deki geliştiriciler faturayı USD ödediğinden, thread bazlı sabit aylık fiyat kur dalgalanmasına karşı öngörülebilir bir giderdir — düşük hacimli takip için BASIC ($15/ay, 5 thread) başlangıç noktasıdır.
Sık Sorulan Sorular
Lambda soğuk başlangıcı DynamoDB yazımını yavaşlatır mı?
Soğuk başlangıç, Lambda'nın kendi başlatma süresidir; DynamoDB çağrısını doğrudan yavaşlatmaz. boto3 istemcisini işlev kapsamı dışında (global) oluşturursanız sıcak çağrılar bağlantıyı yeniden kullanır ve bu maliyeti tekrar ödemezsiniz.
TTL, süresi dolan kayıtları tam olarak ne zaman siler?
TTL silmeleri anlık değildir — DynamoDB süresi dolan öğeleri genellikle 48 saat içinde kaldırır. Kaydın süresi dolduğu anda görünmemesi gerekiyorsa sorgularınızda ttl alanını da filtreleyin.
Bu iş akışı için hangi CaptchaAI planı yeterli?
İş yükünü eşzamanlı thread sayısı belirler, çözüm sayısı değil. Düşük hacimli takip için BASIC ($15/ay, 5 thread) yeterlidir; paralel çözüm arttıkça STANDARD ($30/ay, 15 thread) veya üstüne çıkabilirsiniz. DynamoDB tarafında ise ücret çözüm başına değil, yazılan kayıt başınadır.
Tüm CAPTCHA türlerini tek yerden sorgulayabilir miyim?
Evet. Durum bazında sorgulamak için GSI1 dizinini kullanın; türler arası toplama içinse DynamoDB Streams ve STATS# bölümüne yazan bir Lambda işlevi ekleyin.
Sonraki Adımlar
Kendiliğinden ölçeklenen sunucusuz bir CAPTCHA takip katmanı kurun — CaptchaAI API anahtarınızı alın.
İlgili kılavuzlar: