Ölçek problemi

Tek bir TC kimlik numarasını doğrulamak kolay. 10 milyon TC kimliği veya VKN'yi üç saat içinde doğrulamak farklı bir problem. İlki algoritma sorusu; ikincisi dağıtık sistem, rate limiting, fault tolerance ve maliyet kontrolü sorusudur. Aynı mimari desen toplu vergi no doğrulaması için de geçerli. Performans testleri için binlerce farklı şirkete ait vergi no üret senaryolarını devreye alıp sentetik veri oluşturabilirsiniz.

Bu yazıda farklı ölçeklerde (10K, 100K, 1M, 10M+ kayıt) kullanılan mimari desenleri ele alacağız. Her ölçekte farklı trade-off'lar öne çıkıyor. Amaç somut sayılar ve çalışan kalıplar vermek.

Önce net ayrım: format doğrulaması milisaniye mertebesinde yerel iştir, sadece batch I/O'su ve bellek sorusudur. Kimlik doğrulaması (NVİ servisi) network bound'dur, rate limit ve retry stratejisi gerektirir. Bu iki katman için mimari farklıdır.

Ölçek 1: 1K–10K kayıt

Bu ölçekte çok fazla mimari gerek yok. Tek bir script bir CSV dosyasını satır satır okur, her satırı validate eder, sonuç CSV'sine yazar. 10K kayıt için format validation tek CPU core'da 1-2 saniye sürer.

import csv
from concurrent.futures import ThreadPoolExecutor

def validate_row(row):
    return {**row, 'valid': is_valid_tckn(row['tckn'])}

with open('input.csv') as f_in, open('output.csv', 'w', newline='') as f_out:
    reader = csv.DictReader(f_in)
    writer = csv.DictWriter(f_out, fieldnames=[*reader.fieldnames, 'valid'])
    writer.writeheader()
    for row in reader:
        writer.writerow(validate_row(row))

NVİ servisi ile gidecekseniz ama küçük hacim varsa, ThreadPoolExecutor ile 5-10 concurrency yeterli. Bu ölçekte backend'e gerek yok; developer laptop'u işi bitirir. Online deneme için Toplu Doğrulama aracımızı kullanabilirsiniz.

Ölçek 2: 100K kayıt

100K'ya çıktığımızda bellek ve süre birden önemli olmaya başlar. CSV'yi tek seferde belleğe yüklemeyin (stream edin). Format validation için hâlâ tek makina yeter, ama NVİ servisine gidilecekse concurrency ve rate limiting devreye girer.

Mimari olarak:

  • Stream processing (generator, yield)
  • Chunk bazlı yazma (her 1K kayıtta bir flush)
  • Progress tracking (kaç bin kayıt işlendi, hata oranı nedir)
  • Partial failure handling (dosyanın yarısında crash olursa kaldığın yerden devam)

Python'da:

import csv, json
from pathlib import Path

def stream_process(in_path, out_path, checkpoint_path, chunk=1000):
    done = set()
    if Path(checkpoint_path).exists():
        done = set(json.loads(Path(checkpoint_path).read_text()))

    buffer = []
    with open(in_path) as f_in, open(out_path, 'a', newline='') as f_out:
        reader = csv.DictReader(f_in)
        writer = csv.DictWriter(f_out, fieldnames=[*reader.fieldnames, 'valid'])
        if f_out.tell() == 0: writer.writeheader()

        for i, row in enumerate(reader):
            if row['tckn'] in done:
                continue
            row['valid'] = is_valid_tckn(row['tckn'])
            buffer.append(row)
            if len(buffer) >= chunk:
                writer.writerows(buffer)
                done.update(r['tckn'] for r in buffer)
                Path(checkpoint_path).write_text(json.dumps(list(done)))
                buffer.clear()
        if buffer:
            writer.writerows(buffer)

Bu kalıpla 100K kayıt yaklaşık 20-40 saniyede format-validate edilir. NVİ ile yapılacaksa, conservative 50 concurrency ile yaklaşık 1-3 saat arası sürer; ancak rate limit aldığınızda süre katlanır.

Ölçek 3: 1M kayıt

Milyonluk ölçekte tek makina hâlâ yeterli (format validation için) ama mimari disiplin artmalı. Artık "CSV okuyup CSV yazıyorum" yetmez; queue, worker pool, idempotency key gibi patternler devreye girer.

Önerilen mimari:

  1. CSV'yi bir queue'ya (Redis Stream, RabbitMQ, SQS) düşür
  2. N worker queue'dan satır çeker
  3. Her worker satır üzerinde format validation + (gerekiyorsa) NVİ çağrısı yapar
  4. Sonuç bir output store'a (başka queue, DB tablosu, S3) yazılır
  5. Koordinasyon: başlangıçta job ID üretilir, tüm worker'lar aynı job ID ile çalışır, sonuç bu ID ile toplanır

Idempotency key olarak job_id + row_number kullanın. Worker crash olduğunda aynı satır ikinci kez işlense bile sonuç aynı olur ve duplicate row oluşmaz.

# Redis-based worker
import redis
r = redis.Redis()

def worker(job_id: str):
    while True:
        item = r.brpoplpush(f"job:{job_id}:queue", f"job:{job_id}:processing", timeout=5)
        if item is None: break
        row = json.loads(item)
        dedup_key = f"job:{job_id}:done:{row['idx']}"
        if r.sismember(f"job:{job_id}:done", row['idx']):
            r.lrem(f"job:{job_id}:processing", 1, item)
            continue
        result = {'idx': row['idx'], 'tckn': row['tckn'],
                  'valid': is_valid_tckn(row['tckn'])}
        r.rpush(f"job:{job_id}:output", json.dumps(result))
        r.sadd(f"job:{job_id}:done", row['idx'])
        r.lrem(f"job:{job_id}:processing", 1, item)

brpoplpush kullanımı, crash durumunda "processing" kuyruğunda kalan item'ları başka worker'a devretmeye imkan verir — "reliable queue pattern".

Ölçek 4: 10M+ kayıt

Bu ölçekte tek makina çoğunlukla yetmez (özellikle NVİ çağrıları için — rate limit ile 10M kayıt günler sürer). Distributed processing gerekir.

İki klasik yaklaşım:

  • Spark/Flink: veriyi distributed olarak partition'a böl, her partition'ı bir executor'da işle. Format validation için overkill ama network I/O'yu koordine etmek kolay.
  • Kubernetes Job + queue: Redis/Kafka queue'ya tüm satırları push et, 50-200 pod ile çek. Autoscaling'i HPA ile kuyruk derinliğine bağla.

NVİ rate limit'i olduğu varsayımıyla, ne kadar paralellik koyarsanız koyun upstream hız sabit. Toplam süre: N / throughput. Eğer throughput saniyede 20 çağrı ise 10M kayıt en az 138 saat (5-6 gün) sürer. İş gereksiniminize göre sadece format validation yapmak genellikle daha pragmatik; NVİ çağrılarını sadece "kayıt açma" anında, toplu işte değil anlık yapın.

CSV import/export pratikleri

Toplu işlerin %80'i CSV'den girip CSV'den çıkar. Dikkat edilecek noktalar:

  • Encoding: Türkçe karakterler için utf-8-sig (BOM ile) Excel uyumludur; saf utf-8 bazı Excel sürümlerinde yanlış görünür.
  • Delimiter: virgül (,), noktalı virgül (;) veya tab (\t) — TR Excel genelde ; kullanır. Import'ta sniff edin, export'ta parametre yapın.
  • Quote escape: TCKN'de nadiren ama ad/soyad alanlarında virgül olabilir. CSV parser'ınız "quoted field" standardını (RFC 4180) takip etsin.
  • Leading zero koruma: Excel 02345678901 değerini 2345678901 olarak gösterebilir. Bunu önlemek için CSV'yi ="02345678901" formatında yazmak bir yöntemdir (kirli ama işe yarar) veya kullanıcıları "import as text" akışına yönlendirin.
  • Boyut: 100MB üzeri CSV'ler tarayıcıya upload ettirmeyin. Presigned URL ile S3'e yüklesin, backend orada işlesin.

Worker pool stratejileri

Worker sayısı seçimi için pratik kurallar:

  • CPU bound (sadece format validation): worker = CPU core sayısı.
  • I/O bound (NVİ çağrısı): worker = rate_limit * timeout_saniye. Örneğin 50 rps limit ve 5 saniye timeout varsa 250 concurrent worker makul üst sınır.
  • Mixed: iki katmanı ayırın. Format için bir pool, NVİ için başka bir pool. Format pool NVİ pool'a feed yapar.

Worker sayısını dinamik ayarlamak idealdir: hata oranı artınca azalt (circuit breaker), düşünce artır. Go'da semaphore.Weighted, Python'da asyncio.Semaphore bu iş için birebir.

Partial failure handling

10M'lik bir job'ın %99.9'u başarılı olsa bile 10K kayıt fail etmiştir. Bu kayıtları ne yapacağınızı baştan tasarlayın:

  • Dead letter queue: başarısız satırlar ayrı bir yere yazılsın, manuel review için
  • Retry with backoff: UPSTREAM_ERROR tipi hatalar için otomatik retry; INVALID_FORMAT için retry yok
  • Hata raporu: CSV çıktısına error_code ve error_message kolonları ekleyin
  • Rollback yok, resume var: milyonluk iş yarıda kalırsa baştan başlatmayın; checkpoint'ten devam edin

Hata sınıflandırması için API entegrasyonu yazımızdaki dört sınıfa bakın.

Idempotency

Toplu işin aynısını iki kez çalıştırırsanız ne olur? İdempotent bir design'da sonuç aynıdır, yan etki iki katlanmaz. Pratik uygulamalar:

  • Her satırı (job_id, row_index) ile etiketleyin
  • Output tablosunda (job_id, row_index) unique constraint
  • DB insert'i ON CONFLICT DO NOTHING veya upsert ile yapın
  • Webhook trigger'larını da idempotency-key başlığı ile koruyun

Kullanıcı "import et" düğmesine iki kez bastığında iki farklı job_id üretin, ama row hash'lerine bakarak duplicate olduğunu fark edip uyarı gösterin — iş mantığı kararı.

Observability

Toplu işlerde görünürlük kritik. En az şunları tutun:

  • Progress metric: işlenmiş satır / toplam satır oranı (prometheus counter)
  • Error rate: fail / total, zaman serisi
  • Latency: per-row işlem süresi, p50/p95/p99
  • Queue depth: kuyruk derinliği trend'i (stuck worker tespiti)
  • Upstream health: NVİ response süresi ve hata oranı

Bu metrikler olmadan 1 milyon kayıtlı bir job'ın "takıldığını" ya da "normal" olduğunu anlayamazsınız.

CSV + TC kimlik örneği: tüm parçalar bir arada

Pragmatik bir 500K kayıtlık job'ın tam akışı:

  1. Kullanıcı UI'dan CSV yükler → POST /uploads presigned URL alır, S3'e yükler
  2. Backend POST /jobs {s3_key, type: 'tckn_validate'} ile job başlatır, job_id döner
  3. Controller CSV'yi stream olarak okur, 1K'lık chunk'lar halinde Redis stream'e push eder
  4. 10 worker pod chunk'ları çeker, her satırı validate eder, sonucu output stream'e yazar
  5. Aggregator output stream'den okuyup sonuç CSV'sini S3'e yazar
  6. Job tamamlandığında kullanıcıya e-posta ile download linki gider

Bu mimari 500K kayıt için 2-5 dakika civarı çalışır (saf format validation). Maliyet birkaç kuruş. NVİ ile genişletilirse rate limit belirleyici olur.

Güvenlik ve KVKK notları

Toplu işlerde veri mahremiyeti risk alanı:

  • S3 bucket'ları server-side encrypted olmalı
  • Job payload'ları queue'da encrypted tutulmalı (Kafka topic encryption, Redis ACL)
  • Worker log'larında TCKN'ler maskelenmiş geçmeli
  • Tamamlanan job çıktıları 7-30 gün içinde otomatik silinmeli (S3 lifecycle policy)
  • Audit log'da "kim hangi job'u çalıştırdı" izlenmeli

KVKK açısından derinlemesine rehber: KVKK uyumlu kimlik doğrulama.

Benchmark çıkarmak

Kendi sisteminizde aşağıdaki datapoint'leri çıkarın ve doküman olarak saklayın:

  • Tek core'da saniyede kaç format validation (tipik değer: 500K–2M/saniye)
  • Tek pod için NVİ ile saniyede kaç çağrı (tipik: 10-50/saniye, rate limit'e bağlı)
  • Redis stream'den worker'a ortalama latency (tipik: 5-20ms)
  • End-to-end p95 per-row süre
  • 1M kayıt için tahmini süre = sum / throughput

Bu sayılar olmadan kullanıcıya "işiniz 30 dakika sürecek" taahhüdünde bulunamazsınız.

Sonuç

Toplu doğrulama basit görünen ama ölçek arttıkça patlayan bir problemdir. Doğru ayrım (format vs. kimlik), doğru queue/worker mimarisi, idempotency ve partial failure handling ile milyonluk setler güvenle işlenebilir. Anahtar ilke: önce yerel + ucuz katmandan geç, sonra pahalı uzak katmanı minimum sayıda çağır.

Aracı deneyin: Toplu TCKN/VKN Doğrulama, Toplu Üretim, Kütüphaneler.