Tutorials

Sunucusuz CAPTCHA Çözüm Takibi için DynamoDB

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:

  1. Görevi in.php üzerinden CaptchaAI'ye gönderir ve dönen captcha_id değerini ACTIVE# kaydıyla işaretler.
  2. Sonucu res.php üzerinden beş saniyelik aralıklarla sorgular.
  3. Çözüm gelince kalıcı SOLVE# ve SITE# 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_ms ve polls alanları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:

Bu makale için yorumlar devre dışı bırakılmıştır.