Principal Engineer El Kitabı — Python, Reactive, Kafka & Dağıtık Sistemler

7 Mart 2026 · netologist · 40 dakika, 8397 kelime ·

Python · Reactive Programming · Kafka Event Streaming · Distributed Systems · Resilience

Büyük ölçekli, Python tabanlı dağıtık sistemleri tasarlamak, inşa etmek ve işletmek isteyen mühendisler için author-level derinlikte teknik referans.


İçindekiler

  1. Author Seviyesinde Python
  2. Reactive Programming
  3. Kafka ve Event-Driven Mimari
  4. Distributed Systems Temelleri
  5. Resilience Mühendisliği
  6. Hepsini Birleştirmek: Referans Mimari
  7. İleri Okuma

1. Author Seviyesinde Python

1.1 Data Model — Python’ın Gerçek Sözleşmesi

Python’daki “magic method"lar sadece syntactic sugar değildir; interpreter’ın senin objelerinle müzakere ettiği gerçek arayüz sözleşmesidir. __new__ ile __init__ arasındaki farkı, descriptor protokolünü ve MRO’yu (Method Resolution Order, C3 linearization) anlamak, orta seviye ile ileri seviye Python bilgisini birbirinden ayıran şeydir.

class Descriptor:
    """Data descriptor: hem __get__ hem __set__ tanımlar."""
    def __set_name__(self, owner, name):
        self._name = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self._name, None)

    def __set__(self, obj, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f"{self._name} sayısal olmalı")
        setattr(obj, self._name, value)


class Point:
    x = Descriptor()
    y = Descriptor()

    def __init__(self, x, y):
        self.x = x
        self.y = y

Neden önemli: @property, ORM’ler (SQLAlchemy, Django) ve dataclasses hepsi descriptor protokolü üzerine inşa edilmiştir. Data descriptor’lar (__set__ tanımlayanlar) instance __dict__‘ine göre önceliklidir; non-data descriptor’lar (sadece __get__, örneğin fonksiyon/metodlar) önceliksizdir. Instance attribute’ların methodları gölgeleyebilmesinin ama property’leri gölgeleyememesinin tam sebebi budur.

1.2 Metaclass’lar — Gerçekten Ne Zaman Gerekli

Metaclass “bir sınıfın sınıfı"dır. Bunları tüm bir sınıf hiyerarşisi boyunca invariant zorlamak veya otomatik kayıt (registration) için kullan — estetik için değil.

class PluginMeta(type):
    registry: dict[str, type] = {}

    def __new__(mcs, name, bases, namespace, **kwargs):
        cls = super().__new__(mcs, name, bases, namespace)
        if bases:  # base sınıfın kendisini atla
            PluginMeta.registry[name] = cls
        return cls


class Plugin(metaclass=PluginMeta):
    pass


class CSVExporter(Plugin):
    pass

# PluginMeta.registry == {"CSVExporter": <class CSVExporter>}

__init_subclass__ (PEP 487), meşru metaclass kullanım senaryolarının %90’ını çok daha az karmaşıklıkla çözer:

class Plugin:
    registry: dict[str, type] = {}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.registry[cls.__name__] = cls

Pratik kural: önce __init_subclass__‘a yönel. Metaclass’a sadece sınıf oluşturulma anının kendisine müdahale etmen gerektiğinde (örn. sınıf objesi oluşmadan önce namespace’i validate etmek) veya sıralı/özel namespace’ler için __prepare__ kontrolü gerektiğinde başvur.

1.3 Concurrency Modelleri: GIL, Thread’ler, Process’ler ve Async

Python’ın concurrency hikayesinde üç ayrı eksen var, ve bunları birbirine karıştırmak en yaygın mimari hatadır:

ModelEn uygun olduğu yerKısıtlama
threadingI/O-bound işlerGIL bytecode yürütmesini seri hale getirir (çoğunlukla — bkz. 1.3.1)
multiprocessingCPU-bound işlerProcess spawn/IPC overhead’i, pickling maliyeti
asyncioYüksek eşzamanlı I/O (binlerce bağlantı)Tek thread’li; bloklayan tek bir çağrı her şeyi durdurur

1.3.1 GIL ve Kaldırılması (PEP 703 / Python 3.13+)

Global Interpreter Lock, aynı anda sadece bir thread’in Python bytecode’u çalıştırmasını garanti eder. Bu, veri yapılarını daha yüksek seviyede race condition’lardan korumaz (x += 1 atomik değildir) ve I/O ile bazı C-extension çağrıları (NumPy vb.) sırasında serbest bırakılır.

Python 3.13, deneysel free-threaded build‘i tanıttı (--disable-gil, PEP 703). 3.13/3.14 itibarıyla bu hâlâ opt-in ve gerçek ekosistem maliyetleri var (C-extension ABI uyumluluğu, tek-thread’li kodda per-object locking overhead’i). 2026’daki bir principal engineer duruşu: free-threading’i, tüm dependency zincirini kontrol etmiyorsan ve benchmark etmediysen, henüz varsayılan bir production kabulü değil, geleceğe yönelik tasarlanacak bir kapasite olarak ele al.

1.3.2 asyncio İç Yapısı

Event loop, şunların etrafında kurulmuş tek-thread’li cooperative bir scheduler’dır:

import asyncio

async def fetch(name: str, delay: float) -> str:
    await asyncio.sleep(delay)
    return f"{name} tamamlandı"

async def main():
    async with asyncio.TaskGroup() as tg:  # Python 3.11+: structured concurrency
        t1 = tg.create_task(fetch("A", 1))
        t2 = tg.create_task(fetch("B", 2))
    print(t1.result(), t2.result())

asyncio.run(main())

Structured concurrency (TaskGroup, PEP 654 ExceptionGroup), asyncio’nun en kötü tarihsel tuzağını düzeltti: exception’ları sessizce kaybolan öksüz (orphaned) task’lar. Bir TaskGroup, alt task’lardan biri başarısız olursa kardeşlerinin iptal edilmesini ve tüm exception’ların bir ExceptionGroup‘a toplanmasını garanti eder — artık “fırlat ve unut” sızıntısı yok.

async def riskli():
    raise ValueError("patladı")

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(riskli())
            tg.create_task(asyncio.sleep(5))
    except* ValueError as eg:
        print("Yakalandı:", eg.exceptions)

Kütüphaneler arası gerçekten structured, cancellation-safe concurrency için pek çok production ekibi hâlâ anyio‘yu (asyncio/trio’yu birleştiren) veya doğrudan trio‘yu tercih ediyor; çünkü trio’nun nursery modeli structured concurrency’yi yapısal olarak zorunlu kılıyor, unstructured bir çekirdeğe sonradan eklenmiş opt-in bir özellik olarak değil.

1.3.3 Ölçekte Yaygın asyncio Tuzakları

sem = asyncio.Semaphore(50)

async def bounded_fetch(url):
    async with sem:
        return await fetch_url(url)

async def main(urls):
    async with asyncio.TaskGroup() as tg:
        for url in urls:
            tg.create_task(bounded_fetch(url))

1.4 Ölçekte Typing

Büyük kod tabanları için modern Python (3.11+) typing:

from typing import Protocol, TypeVar, Generic, ParamSpec, overload
from collections.abc import Callable

T = TypeVar("T")
P = ParamSpec("P")

class Repository(Protocol[T]):
    """Structural typing: kalıtım gerekmez, statik kontrollü duck typing."""
    async def get(self, id: str) -> T | None: ...
    async def save(self, entity: T) -> None: ...

class EventHandler(Generic[T]):
    def __init__(self, handler: Callable[[T], None]) -> None:
        self._handler = handler

def retry(fn: Callable[P, T]) -> Callable[P, T]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        for attempt in range(3):
            try:
                return fn(*args, **kwargs)
            except Exception:
                if attempt == 2:
                    raise
        raise RuntimeError("buraya ulaşılamaz")
    return wrapper

Dependency inversion için ABC yerine Protocol. Event-driven bir sistemde KafkaConsumer, InMemoryQueue ve TestStub sınıfların hepsi, ortak bir base class olmadan bir MessageSource Protocol’ünü tatmin edebilir — Python’da ports/adapters (hexagonal architecture) mimarisini dürüst tutan şey tam olarak budur.

Birden fazla kişinin bakımını yaptığı, birkaç bin satırı aşan herhangi bir kod tabanı için CI’da mypy --strict veya pyright pazarlığa açık değildir — statik olarak yakalanan tip hataları, aksi halde distributed bir consumer group’ta sabah 3’te ortaya çıkacak tam olarak o bug sınıfıdır.

1.5 Performans Mühendisliği

  1. Optimize etmeden önce profille. cProfile + snakeviz, ya da production’da process’i yeniden başlatmadan sampling profiling için py-spy (py-spy dump --pid <pid>; py-spy top --pid <pid>).
  2. Veri yapılarının gerçek karmaşıklığını bil. list.pop(0) O(n)‘dir; collections.deque kullan. Membership testleri: set/dict ortalama O(1), list O(n).
  3. Yüksek kardinaliteli objeler (milyonlarca instance) için __slots__ — per-instance __dict__‘i kaldırır, belleği %40–50 azaltır ve attribute erişimini hızlandırır.
  4. Gerçek CPU-bound hot path’ler için Cython / C extension / Rust (PyO3 üzerinden) — ama sadece profiling darboğazın pure Python’da kaçınılmaz olduğunu kanıtladıktan sonra.
  5. Batch’le ve vektörize et. Veri üzerinde satır satır Python döngüleri, neredeyse her zaman NumPy/Polars vektörize operasyonlarla değiştirilebilir, çoğu zaman 10-100x daha hızlı.
from dataclasses import dataclass

@dataclass(slots=True, frozen=True)
class Event:
    id: str
    payload: bytes
    timestamp: float

1.6 Design Pattern’lar, Pythonic Yöntemle

Klasik GoF pattern’ları Python’da genelde çöker çünkü dil zaten first-class fonksiyonlara, closure’lara ve duck typing’e sahiptir:

from functools import lru_cache

@lru_cache(maxsize=None)
def get_settings() -> "Settings":
    return Settings()  # bir kere oluşturulur, tekrar kullanılır — temiz bir singleton

1.7 Derinlemesine: Çekirdek asyncio Primitifleri, Tam Olarak

Bu bölüm var, çünkü şu dokuz kavram — async def, await, event loop, blocking I/O, non-blocking I/O, concurrency, timeout, cancellation, semaphore — çoğu Python mühendisinin zihinsel modelinin production yükü altında tam olarak nerede çöktüğü yerdir. Her biri burada bir principal engineer’ın ihtiyaç duyduğu derinlikte ele alınıyor: “ne yapar” değil, “kaputun altında gerçekte ne dir ve sessizce nerede başarısız olur.”

1.7.1 async def — Gerçekte Ne Oluşturur

async def, çağrıldığında çalışan bir fonksiyon tanımlamaz. Bir coroutine fonksiyonu tanımlar — onu çağırmak, gövdenin tek bir satırını bile çalıştırmadan anında bir coroutine objesi döndürür.

async def fetch_data(url: str) -> str:
    print("Bu satır henüz çalışmıyor")
    return "data"

coro = fetch_data("https://example.com")
print(type(coro))          # <class 'coroutine'>
print(coro)                # <coroutine object fetch_data at 0x...>
# "Bu satır henüz çalışmıyor" hiç yazdırılmadı — gövde çalışmadı.

Coroutine objesi, bir şey onu sürene kadar hareketsizdir — ya await edilir, ya asyncio.create_task() içine sarılır, ya da asyncio.run()‘a geçirilir. Bu yüzden fetch_data("url") (yani await‘i unutmak) birinci sınıf gerçek dünya bug’ıdır: Python hiçbir hata fırlatmaz, sessizce kullanılmayan bir coroutine objesi oluşturur ve en iyi ihtimalle bir RuntimeWarning: coroutine was never awaited alırsın — ya da daha kötüsü, coroutine hiç çalışmaz ve programın sessizce hiçbir şey yapmaz.

async def main():
    result = fetch_data("https://example.com")  # BUG: await eksik
    print(result)  # "data" değil, "<coroutine object ...>" yazdırır

Kaputun altında: bir coroutine, generator’larla aynı makine üzerine inşa edilmiştir (PEP 492, async def/await‘i, yield için zaten var olan generator protokolünün üzerine kurdu). İç yapıda, bir coroutine objesinin bir send() metodu vardır; event loop, onu StopIteration fırlatana (dönüş değerini taşıyarak) veya bir şey beklemek için kontrolü geri verene kadar ilerletmek için tekrar tekrar send(None) çağırır.

1.7.2 await — Gerçek Askıya Alma Noktası

await, yalnızca bir async def gövdesi içinde yasaldır (başka yerde kullanmak SyntaxErrordır). Tam olarak üç şey yapar:

  1. await edilen objenin __await__() metodunu çağırır ve bir iterator elde eder.
  2. O iterator’ı sürer, iterator her yield ettiğinde kontrolü event loop’a geri verir (gerçek askıya alma budur — kontrol gerçekten loop’a döner, o da başka task’ları çalıştırabilir).
  3. Await edilen operasyonun sonucu hazır olduğunda, tam o noktada çalışmaya devam eder; iterator’ın son StopIteration.value‘su, ifadenin sonucu olur.
class Awaitable:
    def __await__(self):
        print("askıya alınıyor...")
        yield  # kontrolü burada event loop'a geri verir
        print("devam ediliyor!")
        return 42

async def main():
    result = await Awaitable()
    print(result)  # 42

asyncio.run(main())
# Çıktı:
# askıya alınıyor...
# devam ediliyor!
# 42

“Awaitable” nedir? __await__ uygulayan her şey: coroutine’ler, asyncio.Future, asyncio.Task (bir Future alt sınıfı) ve bu protokol üzerine inşa edilmiş kütüphanelerdeki objeler. Düz bir generator, uygun şekilde decorate edilmedikçe awaitable değildir — bu, senkron generator’ları yanlışlıkla async koda karıştırırken “object is not awaitable” TypeError‘larının yaygın bir kaynağıdır.

Kritik zihinsel model: await expr, “bu coroutine’i burada askıya al, event loop’un başka faydalı iş yapmasına izin ver, ve expr‘in bir sonucu olduğunda tam burada devam et” anlamına gelir. “Blokla ve bekle” değildir — bu ayrım, asyncio’nun tek bir thread üzerinde on binlerce eşzamanlı bağlantıya hizmet edebilmesinin tüm sebebidir.

1.7.3 Event Loop — Gerçekte Ne Yapar, Adım Adım

Event loop, tek-thread’li, cooperative bir scheduler’dır. Çekirdek algoritması, gerçekte ne olduğuna basitleştirilmiş hâliyle:

loop.run_forever():
    while durdurulmadı:
        timeout = bir sonraki zamanlanmış callback'e kadar geçen süreyi hesapla()
        events = selector.select(timeout)       # BURADA I/O hazır olmasını bekleyerek bloklanır
        for (fd, event) in events:
            callback = kayıtlı_callback[fd]
            callback()                           # örn. bu socket'i bekleyen bir Task'ı devam ettirir
        run_ready_callbacks()                    # call_soon ile zamanlanmış şeyler
        run_due_scheduled_callbacks()             # call_later ile zamanlanmış şeyler

Event loop’un OS thread’ini gerçekten “blokladığı” tek ve tek yer, selector.select(timeout) içindedir — OS’un I/O hazır olma bildirimini bekler (Linux’ta epoll, BSD/macOS’ta kqueue, Windows’ta proactor loop üzerinden IOCP). Geri kalan her şey saf Python bytecode yürütmesidir, bir seferde bir coroutine adımı, asla gerçekten paralel değil.

import asyncio

async def show_loop_identity():
    loop = asyncio.get_running_loop()
    print(f"Çalıştığı loop: {loop}")
    print(f"Çalışıyor mu: {loop.is_running()}")

asyncio.run(show_loop_identity())

Task zamanlaması, somut olarak: asyncio.create_task(coro), bir coroutine’i bir Task içine sarar, ilk adımını loop.call_soon() ile zamanlar ve anında döner — task şimdi çalışmaya başlamaz, loop’un bir sonraki iterasyonunda çalışmaya başlar. Bu yüzden şu yaygın hata sessizce başarısız olur:

async def main():
    task = asyncio.create_task(slow_operation())
    # main() burada `task`'ı await etmeden dönerse,
    # task uçuş halindeyken garbage-collect edilebilir ve
    # "Task was destroyed but it is pending" uyarısı alırsın.
    return "tamamlandı"

Kural: oluşturulan bir task’ı her zaman ya await et, ya güçlü bir referans tut ve kapsayan scope çıkmadan önce await et, ya da bir TaskGroup kullan — asla bir task’ı fırlatıp await edilmeden scope dışına düşmesine izin verme.

1.7.4 Blocking I/O — Gerçekte Neye Mal Olur

“Blocking” şu anlama gelir: çağıran thread, operasyon tamamlanana kadar işletim sistemi tarafından askıya alınır ve o sırada başka hiçbir şey yapamaz — bu, altta yatan sistem çağrısının bir özelliğidir, Python’ın değil.

import time
import asyncio

async def kotu_handler():
    time.sleep(2)          # BLOCKING: TÜM event loop'u 2 saniye dondurur
    return "tamamlandı"

async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(kotu_handler())
        tg.create_task(kotu_handler())
        tg.create_task(kotu_handler())
    # Bunlar eşzamanlı ÇALIŞMAZ — toplam süre ~2 değil ~6 saniyedir,
    # çünkü time.sleep() kontrolü asla loop'a geri vermez.

Bir asyncio uygulamasını sessizce sabote eden klasik bloklayan çağrılar: time.sleep(), requests.get() (senkron requests kütüphanesi), senkron dosya I/O’su (yavaş bir dosya sistemi/network mount’unda open().read()), senkron veritabanı driver’ları (asyncpg değil, psycopg2) ve herhangi bir CPU-bound saf-Python hesaplaması (sıkı bir döngü, büyük bir payload’ı JSON-parse etmek, büyük bir string üzerinde regex).

Çözüm — bir thread pool’a offload et, bu gerçekten paralel çalışır (bloklayan syscall sırasında GIL serbest bırakılır) ve event loop diğer coroutine’lere hizmet vermeye devam eder:

import asyncio

async def iyi_handler():
    loop = asyncio.get_running_loop()
    result = await loop.run_in_executor(None, time.sleep, 2)  # bir thread'de çalışır
    return "tamamlandı"

async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(iyi_handler())
        tg.create_task(iyi_handler())
        tg.create_task(iyi_handler())
    # Şimdi bunlar gerçekten örtüşür — toplam süre ~2 saniyedir.

CPU-bound bloklayan iş için (I/O-bound değil), bir thread pool GIL yüzünden yardımcı olmaz — bunun yerine, process sınırlarını aşmanın IPC/pickling maliyetini kabul ederek loop.run_in_executor(ProcessPoolExecutor(), cpu_agir_fn, arg) kullan.

1.7.5 Non-Blocking I/O — Gerçek Mekanizma

Non-blocking I/O, altta yatan syscall’ın, thread’i askıya almak yerine anında ya veriyle ya da “operasyon bloklanırdı” sinyaliyle (EWOULDBLOCK/EAGAIN) dönmesi anlamına gelir. asyncio’nun transport’ları soketleri non-blocking moda ayarlar (socket.setblocking(False)) ve ne zaman tekrar deneyeceğini bilmek için OS’un hazır olma bildirim API’sini kullanır.

import asyncio

async def non_blocking_fetch(host: str, port: int, request: bytes) -> bytes:
    reader, writer = await asyncio.open_connection(host, port)  # non-blocking bağlantı
    writer.write(request)
    await writer.drain()                    # non-blocking yazma, backpressure'a saygılı
    response = await reader.read(-1)        # non-blocking okuma
    writer.close()
    await writer.wait_closed()
    return response

asyncio.open_connection, StreamReader.read() ve StreamWriter.drain()‘ın hepsi altta selector-tabanlı non-blocking transport üzerine inşa edilmiştir — sözdizimsel olarak (await sayesinde) bloklayan çağrılar gibi görünürler, ama kaputun altında selector’a bir callback kaydederler ve kontrolü loop’a geri verirler, yalnızca OS hazır olduğunu bildirdiğinde devam ederler.

Temel ayrım, tam olarak ifade edilmiş:

Blocking I/ONon-blocking I/O
Kim beklerOS thread’i, kernel tarafından askıya alınırHiç kimse — syscall her durumda anında döner
Bu sırada ne olurO thread’de başka hiçbir şey çalışamazEvent loop başka coroutine’ler çalıştırır
“Bekleme” nasıl ifade edilirThread çağrıdan basitçe dönmezBir callback kaydedilir; kontrol anında çağırana döner
Çok sayıda eşzamanlı operasyonun maliyetiEşzamanlı operasyon başına bir OS thread’i (pahalı: her biri ~1-8MB stack)Toplamda bir thread, binlerce eşzamanlı operasyon (ucuz: coroutine objeleri ~KB ölçeğinde)

1.7.6 Concurrency — Tam Olarak, ve Parallelism’den Ayrımı

Concurrency (eşzamanlılık), bir programın yapısının bir özelliğidir: birden fazla mantıksal görev, tam olarak aynı anda çalışıp çalışmadıklarına bakılmaksızın, yürütmeleri iç içe geçmiş şekilde devam eder. Parallelism (paralellik), yürütmenin bir özelliğidir: görevler ayrı çekirdeklerde gerçekten tam olarak aynı anda çalışır.

Concurrency (asyncio, tek thread):
Task A: ----[çalış]----[bekle]----[çalış]----[bitti]
Task B: --[bekle]----[çalış]----[bekle]----[çalış]--[bitti]
        (TEK thread üzerinde iç içe — asla aynı anda iki instruction)

Parallelism (multiprocessing, birden fazla çekirdek):
Core 1: Task A: ----------[sürekli çalış]----------[bitti]
Core 2: Task B: ----------[sürekli çalış]----------[bitti]
        (gerçekten eşzamanlı)

asyncio sana paralellik olmadan concurrency verir — I/O-bound iş yükleri için doğru araç olmasının (görevlerin çoğu zamanı hesaplama yapmak değil beklemek ile geçirdiği yerlerde) ve CPU-bound iş yükleri için yanlış araç olmasının (görevlerin zamanlarını gerçekten CPU kullanarak geçirdiği, tek bir thread’de iç içe geçmenin sıfır hızlanma sağladığı yerlerde — orada gerçek paralellik için multiprocessing gerekir, 1.3.1’deki GIL tartışmasına tabi) tam sebebi budur.

1.7.7 Timeout — Kesin Semantik ve Modern API

Bir timeout, await edilen bir operasyonun bir başarısızlık olarak ele alınmadan önce ne kadar sürebileceğini sınırlar. İki API var; aralarındaki farkı bil:

import asyncio

# Eski API: tek bir awaitable'ı sarar
async def wait_for_ile():
    try:
        result = await asyncio.wait_for(slow_operation(), timeout=5.0)
    except TimeoutError:
        print("Zaman aşımı — altta yatan task senin için iptal edildi")

# Modern API (3.11+): birden fazla await boyunca kompoze edilebilen bir context manager
async def timeout_context_ile():
    try:
        async with asyncio.timeout(5.0):
            step1 = await do_step_one()
            step2 = await do_step_two()   # 5 saniyelik bütçe HER İKİ adımı birlikte kapsar
    except TimeoutError:
        print("Blok içinde bir yerde zaman aşımı oldu")

asyncio.timeout() (3.11+) daha iyi varsayılandır çünkü kompoze edilebilir: timeout’ları iç içe geçirebilirsin, deadline’ı dinamik olarak yeniden zamanlayabilirsin (asyncio.timeout_at()), ve timeout’un operasyon tam tamamlandığı anda tetiklendiği durumu (3.11’in yeniden yazımından önce wait_for‘un tarihsel olarak kenar durumlarda daha az zarif ele aldığı gerçek bir race condition) doğru şekilde ele alır.

Timeout’ta gerçekte ne olur, tam olarak: altta yatan task iptal edilir (bkz. 1.7.8) — await noktasında TimeoutError fırlatılır ve coroutine, yok edilmeden önce bir finally bloğunda temizlik yapmak için bir şans alır. Bir timeout, mekanik olarak, saate bağlanmış bir cancellation’dır.

1.7.8 Cancellation — Gerçek Yayılma Mekanizması

asyncio’da cancellation cooperative‘dir, preemptive değil. task.cancel() çağırmak task’ı anında durdurmaz — bir CancelledError‘ın task’ın bir sonraki await noktasında fırlatılmasını zamanlar.

import asyncio

async def worker():
    try:
        print("başlıyor")
        await asyncio.sleep(10)   # cancellation BURADA etkili olur, .cancel()'da değil
        print("iptal edilirse bu satır asla çalışmaz")
    except asyncio.CancelledError:
        print("ölmeden önce temizlik yapılıyor")
        raise   # KRİTİK: özel bir sebebin olmadıkça yeniden fırlat

async def main():
    task = asyncio.create_task(worker())
    await asyncio.sleep(0.1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("main() task'ın iptal edildiğini görüyor")

asyncio.run(main())
# Çıktı:
# başlıyor
# ölmeden önce temizlik yapılıyor
# main() task'ın iptal edildiğini görüyor

CancelledError‘ı yeniden fırlatmak neden önemli: onu yutmak (yakalayıp yeniden fırlatmamak), task’ın onu await eden herkese normal şekilde tamamlanmış gibi görünmesini sağlar; bu, TaskGroup‘un ve wait_for‘un cancellation durumunu doğru izleme yeteneğini bozar ve bir Task‘ı, event loop’un dahili muhasebesinin beklemediği bir durumda sıkışmış bırakabilir. CancelledError, özellikle geniş bir except Exception:‘ın onu yanlışlıkla yutmaması için Exception‘dan değil BaseException‘dan miras alır.

Asla yield etmeyen, bekleyen bir await, pratikte iptal edilemez: bir coroutine sıkı bir senkron döngüde takılıysa (hiç await noktası yok), üzerinde .cancel() çağırmanın, bir await‘e ulaşana kadar hiçbir etkisi yoktur. Bu, coroutine’ler içindeki bloklayan çağrıların neden tehlikeli olduğunun bir başka açısıdır — sadece yavaş değildirler, task’ı cancellation ve timeout’lara tamamen duyarsız hale getirirler.

Kritik bir bölümü cancellation’dan korumak (shielding):

async def worker():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        # worker()'ın kendisi tekrar iptal ediliyor olsa bile bu temizlik TAMAMLANMALI
        await asyncio.shield(critical_cleanup())
        raise

asyncio.shield(), dış bir cancellation’ın shield’lenmiş iç awaitable’a yayılmasını önler — ama dikkat, dış task’ın kendisi hâlâ iptal edilmiştir; yalnızca shield’lenen operasyon kısa kesilmekten korunur.

1.7.9 Semaphore — Kesin Mekanik ve Doğru Kullanım

asyncio.Semaphore(n), n‘de başlayan dahili bir sayaç tutar. acquire() onu azaltır (sıfırın altına düşerse bekler); release() onu artırır. Concurrency’yi sınırlamanın standart primitif’idir — mantıksal olarak kaç tanesi zamanlanmış olursa olsun, bir bölümün “içinde” aynı anda kaç coroutine olabileceğini sınırlar.

import asyncio

sem = asyncio.Semaphore(3)  # en fazla 3 eşzamanlı operasyon

async def sinirli_fetch(url: str, session):
    async with sem:               # girişte acquire, çıkışta release — exception'da bile
        print(f"{url} çekiliyor, şu anki aktif sayı <= 3")
        return await session.get(url)

async def main(urls: list[str]):
    async with asyncio.TaskGroup() as tg:
        for url in urls:
            tg.create_task(sinirli_fetch(url, session))
    # 10.000 url olsa bile, aynı anda en fazla 3 istek uçuş halindedir.

Semaphore vs BoundedSemaphore: BoundedSemaphore, release() acquire()‘dan daha fazla çağrılırsa ValueError fırlatır — düz bir Semaphore‘un, sayacın amaçlanan maksimumun üzerine tırmanmasına basitçe izin vererek sessizce gizleyeceği belirli bir bug sınıfını (çift release) yakalayan savunmacı bir varyant.

Neden sabit sayıda task değil de semaphore: semaphore, kaç coroutine var olduğunu (coroutine objeleri hafif olduğu için tam 10.000 olabilir, ucuzca) kaçının eşzamanlı çalıştığından (3’e sınırlı) ayırır — bu, “her şeye fan out et, ama gerçek concurrency’yi kısıtla” için doğru pattern’dir; işi elle 3’lük gruplara bölmenin (slotlar boşken bile grup sınırları arasında gereksiz yere serileştiren) aksine.

Fakir adamın connection pool sınırlayıcısı olarak semaphore:

db_semaphore = asyncio.Semaphore(20)  # DB pool'unun max bağlantı sayısıyla eşleşir

async def query(sql: str):
    async with db_semaphore:
        async with pool.acquire() as conn:
            return await conn.fetch(sql)

Bu, uygulamanın, altta yatan connection pool’un gerçekte hizmet verebileceğinden daha fazla mantıksal eşzamanlı sorgu açmasını önler — bu olmadan, fazla coroutine’ler basitçe connection pool’un kendi dahili bekleme mantığı içinde görünmez şekilde kuyruğa girer, bu da uygulama katmanındaki açık bir semaphore’dan çok daha zor gözlemlenebilir ve akıl yürütülebilir.

1.7.10 Dokuz Kavramı Bir Araya Getirmek

import asyncio
import time

sem = asyncio.Semaphore(5)                     # concurrency'yi sınırla

async def fetch_one(item_id: int, client) -> dict:
    async with sem:                             # semaphore: eşzamanlı işi kısıtla
        try:
            async with asyncio.timeout(3.0):    # timeout: HERHANGİ bir çağrının ne kadar sürebileceğini sınırla
                loop = asyncio.get_running_loop()
                # bloklayan legacy client, event loop'u durdurmasın diye offload edildi:
                response = await loop.run_in_executor(None, client.get_sync, item_id)
                return response
        except TimeoutError:
            return {"item_id": item_id, "error": "timeout"}
        except asyncio.CancelledError:
            # cancellation: temizlik yap, sonra yay — asla yutma
            print(f"fetch_one({item_id}) iptal edildi — temizleniyor")
            raise

async def fetch_all(item_ids: list[int], client) -> list[dict]:
    async with asyncio.TaskGroup() as tg:       # structured concurrency: öksüz task yok
        tasks = [tg.create_task(fetch_one(i, client)) for i in item_ids]
    return [t.result() for t in tasks]

async def main():
    client = LegacyBlockingClient()
    start = time.monotonic()
    results = await fetch_all(list(range(100)), client)
    print(f"{len(results)} item, en fazla 5 eşzamanlı ve her biri 3sn'ye sınırlı "
          f"olarak {time.monotonic() - start:.2f}sn'de çekildi.")

asyncio.run(main())   # event loop: oluşturulur, main() tamamlanana kadar çalışır, sonra yıkılır

Dokuz kavramın her biri burada, var olma sebebi olan işi tam olarak yaparak görünür: async def/await coroutine’leri tanımlar ve sürer; event loop (asyncio.run tarafından örtük olarak oluşturulur) her şeyi zamanlar ve sürer; blocking I/O (legacy client) loop’u durdurmasın diye kasıtlı olarak offload edilir, geri kalanı non-blocking tutar; concurrency (paralellik değil) 100 mantıksal fetch’in tek bir thread’de iç içe geçmesine izin verir; semaphore gerçekte kaçının uçuşta olduğunu sınırlar; timeout her bir çağrının en kötü durumunu sınırlar; ve cancellation doğru şekilde ele alınır (temizlik için yakalanır, sonra yeniden fırlatılır) sessizce yutulmak yerine.


2. Reactive Programming

2.1 Reactive Manifesto, Tam Olarak

Reactive sistemler dört özellikle tanımlanır ve bunlar bir kontrol listesi değil, bir bağımlılık zinciri oluşturur:

Responsive ← (üzerine kurulur) Resilient + Elastic ← (şu yolla sağlanır) Message-Driven

2.2 Reactive Streams: Gerçek Spesifikasyon

Reactive Streams spesifikasyonu (RxJava, Project Reactor, Akka Streams’in ve kavramsal olarak RxPY’nin altında yatan), dört arayüz tanımlar:

Publisher<T>.subscribe(Subscriber<T>)
Subscriber<T>.onSubscribe(Subscription)
Subscriber<T>.onNext(T)
Subscriber<T>.onError(Throwable)
Subscriber<T>.onComplete()
Subscription.request(n: Long)
Subscription.cancel()

Kritik, sık gözden kaçan detay: backpressure pull-based’dir, push-based değil. Bir Subscriber, Publisher‘a şu anda tam olarak kaç item işleyebileceğini söylemek için request(n) çağırır. Bu, naif “producer olabildiğince hızlı push eder” modelini tersine çevirir ve hızlı producer’ların yavaş consumer’ları boğmasını engelleyen tek mekanizmadır — manifesto’nun “elastic” özelliğinin somut uygulaması.

2.3 Python’da Reactive Programming: RxPY

import reactivex as rx
from reactivex import operators as ops
from reactivex.scheduler import ThreadPoolScheduler

pool = ThreadPoolScheduler(max_workers=8)

source = rx.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

source.pipe(
    ops.filter(lambda x: x % 2 == 0),
    ops.map(lambda x: x * x),
    ops.buffer_with_count(2),
    ops.observe_on(pool),
).subscribe(
    on_next=lambda x: print(f"Alındı: {x}"),
    on_error=lambda e: print(f"Hata: {e}"),
    on_completed=lambda: print("Tamamlandı"),
)

Her author-level mühendisin isimlerini değil, semantiklerini ezbere bilmesi gereken operatör aileleri:

KategoriOperatörlerSemantik
Oluşturmaof, from_iterable, interval, createBir stream’in nasıl başladığı
Dönüşümmap, scan, buffer, windowVeriyi item-bazlı veya window-bazlı şekillendirmek
Filtrelemefilter, distinct_until_changed, debounce, throttle_firstHacmi/gürültüyü azaltmak
Kombinasyonmerge, zip, combine_latest, with_latest_fromÇoklu stream kompozisyonu — bu dördü arasındaki farklar klasik bir mülakat tuzağıdır
Hata yönetimicatch, retry, on_error_resume_nextStream’in dışında değil, içinde hata kurtarma
Backpressuresample, buffer, throttle (RxPY’nin backpressure primitive’leri JVM Rx’ten daha zayıftır — bkz. 2.4)

combine_latest vs zip vs with_latest_from ayrımı, tam olarak:

2.4 RxPY’nin Dürüst Kısıtlaması

RxPY’nin backpressure hikayesi RxJava/Reactor’unkinden daha zayıftır, çünkü senkron Python iterable’ları üzerinden gerçek pull-based backpressure ile asyncio’nun push-based modeli, ekstra özen olmadan temiz bir şekilde birleşmez. Pratikte, gerçek backpressure’a ihtiyaç duyan production Python sistemleri şunlara dayanır:

import asyncio

async def producer(queue: asyncio.Queue, n: int):
    for i in range(n):
        await queue.put(i)  # queue doluysa bloklar — bu backpressure'IN KENDİSİ
    await queue.put(None)  # sentinel

async def consumer(queue: asyncio.Queue):
    while (item := await queue.get()) is not None:
        await asyncio.sleep(0.1)  # yavaş işlemeyi simüle et
        queue.task_done()

async def main():
    q = asyncio.Queue(maxsize=10)
    await asyncio.gather(producer(q, 100), consumer(q))

Author-level çıkarım: reactive kütüphaneler (RxPY), event dönüşümlerini deklaratif şekilde kompoze etmek için kullanışlıdır; reactive mimari (message-driven, resilient, elastic sistemler) sadece Rx kütüphanesiyle değil, transport seçimin (Kafka), kuyruklama disiplinin ve backpressure-farkında consumer tasarımınla elde edilir.

2.5 Reactive vs. Async/Await — Hangisi Ne Zaman Kullanılır

Konuasyncio/awaitRxPY / Reactive Streams
Tek asenkron operasyon, tek sonuç✅ doğal uyum⚠️ gereğinden fazla
Birden fazla bağımsız event stream’ini kompoze etmek (UI event’leri, sensör verisi, market tick’leri)⚠️ hantal, manuel fan-in✅ doğal uyum
Karmaşık temporal operatörler (debounce, sliding window, throttle)❌ elle yazılmış ve hataya açık✅ yerleşik, iyi test edilmiş
Backpressure-kritik, yüksek throughput’lu pipeline’lar⚠️ açık bounded queue gerektirir⚠️ özen gerektirir (2.4) — genelde Kafka’nın kendi flow control’ü kazanır

3. Kafka ve Event-Driven Mimari

3.1 Çekirdek Mimari, Tam Olarak

Topic: orders (3 partition, replication factor 3)

Partition 0: [Leader: Broker1] [Follower: Broker2] [Follower: Broker3]
Partition 1: [Leader: Broker2] [Follower: Broker3] [Follower: Broker1]
Partition 2: [Leader: Broker3] [Follower: Broker1] [Follower: Broker2]

3.2 Producer Semantiği — Tam Olarak

from confluent_kafka import Producer

conf = {
    "bootstrap.servers": "broker1:9092,broker2:9092",
    "acks": "all",                    # tüm in-sync replikaları bekle
    "enable.idempotence": True,       # retry'ları broker seviyesinde dedupe eder
    "max.in.flight.requests.per.connection": 5,  # idempotence açıkken 5'e kadar güvenli
    "compression.type": "zstd",
    "linger.ms": 10,                  # batch penceresi — throughput/latency ödünleşimi
    "batch.size": 65536,
    "retries": 2147483647,            # pratikte sonsuz; idempotence tekrarları önler
}

producer = Producer(conf)

def delivery_report(err, msg):
    if err is not None:
        print(f"Teslimat başarısız: {err}")
    else:
        print(f"Teslim edildi {msg.topic()}[{msg.partition()}]@{msg.offset()}")

producer.produce(
    topic="orders",
    key=str(order.customer_id).encode(),  # aynı key -> aynı partition -> sıralama korunur
    value=order.to_json().encode(),
    on_delivery=delivery_report,
)
producer.flush()

acks semantiği, tam olarak:

Idempotent producer: her producer’a bir PID (producer ID) ve partition başına monoton bir sıra numarası atar, broker’ın tekrarlanan gönderimleri dedupe etmesine izin verir. Bu, retry’lı en-az-bir-kez‘i broker’da tam-olarak-bir-kez’e çevirir, ama tek başına sana uçtan-uca tam-olarak-bir-kez vermez (bkz. 3.5).

Partitioning stratejisi: varsayılan hash-partitioner, murmur2(key) % num_partitions kullanır. Aynı key → her zaman aynı partition → sıralama garantini sağlayan mekanizma budur. Key’leri bilinçli seç: customer-başına sıralama için customer_id, order-başına için order_id — herhangi bir sıralama semantiğine ihtiyacın varsa asla yüksek kardinaliteli rastgele değerler üzerinden partition’lama.

3.3 Consumer Semantiği — Tam Olarak

from confluent_kafka import Consumer, KafkaException

conf = {
    "bootstrap.servers": "broker1:9092,broker2:9092",
    "group.id": "order-processor",
    "auto.offset.reset": "earliest",
    "enable.auto.commit": False,       # manuel commit = doğruluk kontrolü
    "isolation.level": "read_committed",  # yalnızca commit edilmiş transactional mesajları gör
    "max.poll.interval.ms": 300000,    # rebalance tetiklenmeden önceki zaman bütçesi
}

consumer = Consumer(conf)
consumer.subscribe(["orders"])

try:
    while True:
        msg = consumer.poll(timeout=1.0)
        if msg is None:
            continue
        if msg.error():
            raise KafkaException(msg.error())

        process_order(msg.value())          # iş mantığı
        consumer.commit(msg, asynchronous=False)  # BAŞARILI işlemden SONRA commit et
finally:
    consumer.close()

Consumer group mekaniği:

Offset commit stratejisi — gerçek doğruluk sorusu:

StratejiArıza modu
İşlemeden önce commitEn-fazla-bir-kez: commit’ten sonra, işlemeden önce çökme → mesaj kaybolur
İşledikten sonra commit (auto-commit kapalı)En-az-bir-kez: işledikten sonra, commit’ten önce çökme → mesaj yeniden işlenir
Atomik commit + işleme (transactional outbox / Kafka transaction’ları)Etkin-olarak-bir-kez: idempotent işleme veya Kafka transaction’ları gerektirir

Consumer’larını iş mantığı seviyesinde idempotent olacak şekilde tasarla (örn. order_id‘ye göre upsert, kör insert değil) — pratikte en-az-bir-kez teslimatı gerçekten güvenli kılan şey budur, ve uçtan uca tam-olarak-bir-kez peşinde koşmaktan çok daha sağlam bir mühendislik varsayılanıdır.

3.4 Schema Yönetimi

Kontrolsüz JSON payload’ları, ölçekte production Kafka olaylarının #1 nedenidir. Uyumluluk modlarını zorlayan Confluent Schema Registry‘yi Avro veya Protobuf ile kullan:

from confluent_kafka.schema_registry import SchemaRegistryClient
from confluent_kafka.schema_registry.avro import AvroSerializer
from confluent_kafka.serialization import SerializationContext, MessageField

sr_client = SchemaRegistryClient({"url": "http://schema-registry:8081"})

order_schema_str = """
{
  "type": "record",
  "name": "Order",
  "fields": [
    {"name": "order_id", "type": "string"},
    {"name": "customer_id", "type": "string"},
    {"name": "amount", "type": "double"},
    {"name": "created_at", "type": "long", "logicalType": "timestamp-millis"}
  ]
}
"""

avro_serializer = AvroSerializer(sr_client, order_schema_str)
serialized = avro_serializer(order.__dict__, SerializationContext("orders", MessageField.VALUE))

Kural: şema evrimi yalnızca default değerli opsiyonel alan ekleme veya zaten opsiyonel olan alanları kaldırma şeklinde olmalıdır. Bir alanı asla yerinde yeniden adlandırma veya tipini değiştirme — yeni bir alan ekle ve eskisini deprecate et.

3.5 Exactly-Once Semantics (EOS) — Gerçekte Ne Garanti Ediyor

Kafka’nın transactional API’si (isolation.level=read_committed + transactional producer), read-process-write desenleri için (örn. Kafka Streams, ya da consume-transform-produce döngüleri) Kafka ekosistemi içinde tam-olarak-bir-kez sağlar, keyfi harici sistemlere karşı değil.

from confluent_kafka import Producer, Consumer

producer = Producer({
    "bootstrap.servers": "broker1:9092",
    "transactional.id": "order-enricher-1",
})
producer.init_transactions()

consumer = Consumer({
    "bootstrap.servers": "broker1:9092",
    "group.id": "enricher",
    "isolation.level": "read_committed",
    "enable.auto.commit": False,
})
consumer.subscribe(["raw-orders"])

while True:
    msg = consumer.poll(1.0)
    if msg is None:
        continue

    producer.begin_transaction()
    try:
        enriched = enrich(msg.value())
        producer.produce("enriched-orders", value=enriched)
        # Consumer offset'lerini transaction'IN PARÇASI olarak gönder — kritik bağlantı
        producer.send_offsets_to_transaction(
            consumer.position(consumer.assignment()),
            consumer.consumer_group_metadata(),
        )
        producer.commit_transaction()
    except Exception:
        producer.abort_transaction()
        raise

Neden çalışır: offset commit’i ve produce, aynı atomik transaction‘ın parçası hâline gelir. Ya ikisi de olur ya da hiçbiri — Kafka-to-Kafka pipeline’ları için klasik “işlendi ama commit edilmedi” veya “commit edildi ama işlenmedi” boşluğunu ortadan kaldırır. O döngü içinde Kafka-olmayan bir sisteme (bir veritabanı, bir HTTP çağrısı) yazdığın anda, tekrar transactional outbox pattern’ine veya idempotent yazmalara ihtiyaç duyarsın, çünkü Kafka transaction’ları harici bir sistemin transaction sınırını kapsayamaz.

3.6 Kafka ile Event Sourcing ve CQRS

# Bir read-model inşa eden minimal bir "projector" consumer
def project_order_events(consumer, db):
    while True:
        msg = consumer.poll(1.0)
        if msg is None:
            continue
        event = deserialize(msg.value())
        match event["type"]:
            case "OrderCreated":
                db.upsert_order(event["order_id"], status="created", **event["data"])
            case "OrderShipped":
                db.update_order_status(event["order_id"], status="shipped")
            case "OrderCancelled":
                db.update_order_status(event["order_id"], status="cancelled")
        consumer.commit(msg, asynchronous=False)

Transactional Outbox pattern (dual-write problemini çözer — “hem DB yazması hem Kafka publish’i gerçekten oldu mu?”):

  1. İş satırını ve bir “outbox” event satırını aynı yerel veritabanı transaction’ında yaz.
  2. Ayrı bir süreç (Debezium CDC connector veya polling bir publisher), outbox tablosunu okur ve Kafka’ya publish eder, sonra satırı publish edilmiş olarak işaretler.
  3. Bu, DB ve Kafka arasında dağıtık bir transaction gerektirmeden, event’in ancak ve ancak DB transaction’ı commit edilmişse publish edilmesini garanti eder.

3.7 Faust — Python-Native Stream Processing

JVM yerine Python’da Kafka Streams tarzı bir topoloji için Faust, karşılaştırılabilir bir model sağlar:

import faust

app = faust.App("order-processor", broker="kafka://broker1:9092")

class Order(faust.Record, serializer="json"):
    order_id: str
    customer_id: str
    amount: float

orders_topic = app.topic("orders", value_type=Order)
high_value_topic = app.topic("high-value-orders", value_type=Order)

# bir table = changelog-destekli, partition'lanmış, hataya dayanıklı key-value store
customer_totals = app.Table("customer-totals", default=float)

@app.agent(orders_topic)
async def process(orders):
    async for order in orders:
        customer_totals[order.customer_id] += order.amount
        if order.amount > 10_000:
            await high_value_topic.send(value=order)

Faust table’ları bir changelog topic tarafından desteklenir — state hataya dayanıklıdır ve failover’da otomatik olarak yeniden inşa edilir, Kafka Streams’in KTable‘ıyla aynı temel fikirdir.


4. Distributed Systems Temelleri

4.1 CAP Teoremi, Tam Olarak (ve Neden Sıklıkla Yanlış Uygulanır)

Bir network partition‘ı (P) verildiğinde, bir sistem şunlar arasında seçim yapmalıdır:

Kritik nüans: CAP yalnızca gerçek bir partition sırasında geçerlidir. Partition koşulları dışında, aslında seçim yapmaya zorlanmazsın — bu yüzden PACELC, günlük tasarım için daha kullanışlı bir zihinsel modeldir:

Partition: A ya da C seç. Else (partition yoksa): Latency ya da Consistency seç.

DynamoDB veya Cassandra gibi sistemlerin “ayarlanabilir tutarlılıklı AP” olmasının nedeni budur — quorum parametreleri (R, W, N) aracılığıyla, partition olmasa bile latency/consistency ekseninde nerede durduğunu operasyon-başına seçmene izin verirler.

4.2 Tutarlılık Modelleri — Gerçek Spektrum

En güçlüden en zayıfa:

  1. Linearizability: operasyonlar, çağrı ile yanıt arasındaki bir noktada, tek bir global gerçek-zamanlı sıraya tutarlı biçimde atomik olarak gerçekleşiyormuş gibi görünür. Pahalıdır — yazma başına tipik olarak consensus (Raft/Paxos) gerektirir.
  2. Sequential consistency: tüm operasyonlar, her process’in kendi program sırasıyla tutarlı bir toplam sırada görünür — ama bu sıranın gerçek zamanla eşleşmesi gerekmez.
  3. Causal consistency: nedensel olarak ilişkili operasyonlar (B, A‘nın yazdığı bir değeri okudu) herkes tarafından bu sırada görülür; eşzamanlı (ilişkisiz) operasyonlar farklı gözlemciler tarafından farklı sıralarda görülebilir.
  4. Eventual consistency: yeni yazma olmadığında, tüm replikalar nihayetinde aynı değere yakınsar. Ara dönemde sıralama garantisi yoktur.

Author-level içgörü: pratikte “eventually consistent” sistemlerin çoğu aslında en azından causal consistency‘ye ihtiyaç duyar, aksi halde kullanıcılar bug algılar (“bir yorum yaptım, sonra bir yanıt geldi, ama başka biri yanıttan önce yorumu gördü”). Vector clock’ların / Lamport timestamp’lerinin “AP” sistemlerde bile önemli olmasının sebebi budur.

4.3 Mantıksal Saatler

Lamport timestamp’leri toplam (ama eşzamanlı event’ler için mutlaka nedensel olarak doğru olmayan) bir sıra sağlar:

class LamportClock:
    def __init__(self):
        self.time = 0

    def tick(self) -> int:
        self.time += 1
        return self.time

    def receive(self, other_time: int) -> int:
        self.time = max(self.time, other_time) + 1
        return self.time

Vector clock’lar, event başına O(n) alan maliyetiyle (n = node sayısı) gerçek nedenselliği yakalar:

class VectorClock:
    def __init__(self, node_id: str, nodes: list[str]):
        self.node_id = node_id
        self.clock = {n: 0 for n in nodes}

    def tick(self) -> dict[str, int]:
        self.clock[self.node_id] += 1
        return dict(self.clock)

    def merge(self, other: dict[str, int]):
        for node, t in other.items():
            self.clock[node] = max(self.clock.get(node, 0), t)
        self.tick()

    @staticmethod
    def concurrent(a: dict[str, int], b: dict[str, int]) -> bool:
        """Ne a <= b ne de b <= a ise event'ler eşzamanlıdır (gerçek bir çakışma)."""
        a_leq_b = all(a.get(k, 0) <= b.get(k, 0) for k in set(a) | set(b))
        b_leq_a = all(b.get(k, 0) <= a.get(k, 0) for k in set(a) | set(b))
        return not a_leq_b and not b_leq_a

Bu concurrent() kontrolü, Riak/Dynamo gibi sistemlerin uygulama seviyesinde veya CRDT-tabanlı çözünürlük gerektiren yazma çakışmalarını tam olarak nasıl tespit ettiğidir.

4.4 Consensus: Raft, Somut Olarak

Raft, consensus’u üç anlaşılabilir alt probleme ayrıştırır: leader election, log replication, safety.

# Basitleştirilmiş leader-election çekirdeği (açıklayıcı, production-safe değil)
import random, time, enum

class Role(enum.Enum):
    FOLLOWER = "follower"
    CANDIDATE = "candidate"
    LEADER = "leader"

class RaftNode:
    def __init__(self, node_id: str, peers: list[str]):
        self.node_id = node_id
        self.peers = peers
        self.role = Role.FOLLOWER
        self.current_term = 0
        self.voted_for: str | None = None
        self.last_heartbeat = time.monotonic()
        self.election_timeout = random.uniform(0.15, 0.3)  # rastgele -> split vote'ları önler

    def on_election_timeout(self):
        if time.monotonic() - self.last_heartbeat < self.election_timeout:
            return
        self.role = Role.CANDIDATE
        self.current_term += 1
        self.voted_for = self.node_id
        votes = 1  # kendine oy
        for peer in self.peers:
            if self.request_vote(peer, self.current_term):
                votes += 1
        if votes > (len(self.peers) + 1) // 2:
            self.role = Role.LEADER

    def request_vote(self, peer: str, term: int) -> bool:
        ...  # peer'a RPC; term daha yeniyse ve log en az güncel ise peer oy verir

Bunu anlamak pratikte neden önemli: ister Kafka’nın KRaft controller quorum’unu, ister etcd’yi (Kubernetes tarafından kullanılır), ister Consul’u düşünüyor ol — production’da debug edeceğin arıza modları (network partition sırasında split-brain, okuma sunan bayat leader) doğrudan bu mekaniklere kadar iz sürer.

4.5 Dağıtık Transaction’lar: 2PC vs Saga

Two-Phase Commit (2PC): bir coordinator tüm katılımcılardan PREPARE yapmalarını ister; yalnızca hepsi evet oyu verirse COMMIT gönderir. Atomiklik garanti eder ama bloklayıcıdır — coordinator prepare’den sonra ama commit’ten önce çökerse, katılımcılar kilitleri süresiz tutar. Bu yüzden 2PC modern dağıtık/microservice sistemlerinde nadirdir.

Saga pattern: her biri, sonraki bir adım başarısız olursa geri almak için tanımlı bir compensating action‘a sahip bir dizi yerel transaction.

@dataclass
class SagaStep:
    action: Callable[[], None]
    compensation: Callable[[], None]

class Saga:
    def __init__(self, steps: list[SagaStep]):
        self.steps = steps
        self.completed: list[SagaStep] = []

    def execute(self):
        try:
            for step in self.steps:
                step.action()
                self.completed.append(step)
        except Exception:
            for step in reversed(self.completed):
                step.compensation()  # ters sırada geri al
            raise

saga = Saga([
    SagaStep(reserve_inventory, release_inventory),
    SagaStep(charge_payment, refund_payment),
    SagaStep(schedule_shipment, cancel_shipment),
])
saga.execute()

Choreography vs Orchestration Saga’lar:

Ölçekte, saf choreography, saga ~4-5 adımı aştığında genelde durable bir state machine ile orchestration‘a (örn. Temporal.io, AWS Step Functions) yenilir, çünkü “bu saga hangi durumda?” sorusu saf event choreography ile operasyonel olarak cevaplanamaz hale gelir.

4.6 Sharding ve Replikasyon

Sharding stratejileri:

import hashlib
import bisect

class ConsistentHashRing:
    def __init__(self, nodes: list[str], vnodes: int = 150):
        self.ring: dict[int, str] = {}
        self.sorted_keys: list[int] = []
        for node in nodes:
            for i in range(vnodes):
                key = self._hash(f"{node}#{i}")
                self.ring[key] = node
        self.sorted_keys = sorted(self.ring)

    def _hash(self, key: str) -> int:
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def get_node(self, key: str) -> str:
        if not self.ring:
            raise RuntimeError("Boş ring")
        h = self._hash(key)
        idx = bisect.bisect(self.sorted_keys, h) % len(self.sorted_keys)
        return self.ring[self.sorted_keys[idx]]

Replikasyon:

4.7 Distributed Tracing ve Observability

Bir isteğin bir düzine servise ve arada bir Kafka topic’ine yayıldığı bir sistemde, distributed tracing (OpenTelemetry) opsiyonel bir araç değildir — nedenselliği sonradan yeniden inşa etmenin tek yoludur.

from opentelemetry import trace
from opentelemetry.propagate import inject, extract

tracer = trace.get_tracer(__name__)

# Producer tarafı: trace context'ini Kafka header'larına enjekte et
def produce_with_trace(producer, topic, key, value):
    with tracer.start_as_current_span("produce_order") as span:
        headers = {}
        inject(headers)  # W3C traceparent header'ı burada enjekte edilir
        producer.produce(
            topic, key=key, value=value,
            headers=[(k, v.encode()) for k, v in headers.items()],
        )

# Consumer tarafı: trace'i çıkar ve devam ettir
def consume_with_trace(msg):
    headers = {k: v.decode() for k, v in (msg.headers() or [])}
    ctx = extract(headers)
    with tracer.start_as_current_span("consume_order", context=ctx):
        process_order(msg.value())

Message broker boyunca header propagation olmadan, her trace producer’da ölür ve “bu sipariş neden uçtan uca 4 saniye sürdü” sorusunu cevaplama yeteneğini kaybedersin — Kafka tabanlı observability kurulumlarındaki en yaygın tek boşluk.


5. Resilience Mühendisliği

5.1 Resilience Araç Seti — Beş Pattern, Tam Olarak

5.1.1 Timeout

Her network çağrısının açık bir timeout’a ihtiyacı vardır — güvenli bir varsayılan “sonsuza kadar bekle” diye bir şey yoktur.

import httpx

async def fetch_with_timeout(url: str):
    async with httpx.AsyncClient(timeout=httpx.Timeout(connect=2.0, read=5.0, write=5.0, pool=2.0)) as client:
        return await client.get(url)

Connect timeout‘u (bir TCP bağlantısı bile kurabiliyor muyuz) read timeout’tan (sunucu zamanında yanıt veriyor mu) ayır — bunları tek bir sayıya karıştırmak, production’da gerçekte hangi arıza modunu gördüğünü gizler.

5.1.2 Exponential Backoff ve Jitter ile Retry

Naif retry fırtınaları, kademeli kesintilerin (cascading outage) önde gelen nedenidir: her client aynı sabit aralıkta retry yaptığında, zaten zorlanan bir servise karşı bir “thundering herd” içinde senkronize olurlar.

import random
import asyncio
from typing import TypeVar, Callable, Awaitable

T = TypeVar("T")

async def retry_with_backoff(
    fn: Callable[[], Awaitable[T]],
    max_attempts: int = 5,
    base_delay: float = 0.1,
    max_delay: float = 10.0,
) -> T:
    for attempt in range(max_attempts):
        try:
            return await fn()
        except (ConnectionError, TimeoutError):
            if attempt == max_attempts - 1:
                raise
            # Full jitter (AWS Architecture Blog'un önerdiği formül)
            delay = min(max_delay, base_delay * (2 ** attempt))
            jittered = random.uniform(0, delay)
            await asyncio.sleep(jittered)
    raise RuntimeError("buraya ulaşılamaz")

Yalnızca idempotent operasyonları retry et, ya da bir idempotency key ile idempotent hale getirilmiş operasyonları. Bir dedup key olmadan idempotent-olmayan bir ödeme tahsilatını retry etmek, müşterileri çift faturalandırmaya doğrudan giden bir yoldur.

5.1.3 Circuit Breaker

Bir client’ın, zaten başarısız olan bir servisi tekrar tekrar çağırmasını önler, ona toparlanma zamanı verir ve client’ı boşuna kaynak harcamaktan korur.

import time
import enum
from dataclasses import dataclass, field

class CircuitState(enum.Enum):
    CLOSED = "closed"       # normal işleyiş
    OPEN = "open"           # hızlı başarısız oluyor, downstream'e çağrı yok
    HALF_OPEN = "half_open" # downstream'in toparlanıp toparlanmadığını test ediyor

@dataclass
class CircuitBreaker:
    failure_threshold: int = 5
    recovery_timeout: float = 30.0
    half_open_max_calls: int = 3

    state: CircuitState = field(default=CircuitState.CLOSED)
    failure_count: int = 0
    last_failure_time: float = 0.0
    half_open_calls: int = 0

    def call(self, fn: Callable[[], T]) -> T:
        if self.state == CircuitState.OPEN:
            if time.monotonic() - self.last_failure_time > self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
                self.half_open_calls = 0
            else:
                raise CircuitOpenError("Circuit açık — hızlı başarısız oluyor")

        if self.state == CircuitState.HALF_OPEN and self.half_open_calls >= self.half_open_max_calls:
            raise CircuitOpenError("Half-open çağrı bütçesi tükendi")

        try:
            result = fn()
        except Exception:
            self._on_failure()
            raise
        else:
            self._on_success()
            return result

    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.monotonic()
        if self.state == CircuitState.HALF_OPEN:
            self.state = CircuitState.OPEN  # half-open'da tek bir hata yeniden açar
        elif self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN

    def _on_success(self):
        if self.state == CircuitState.HALF_OPEN:
            self.half_open_calls += 1
            if self.half_open_calls >= self.half_open_max_calls:
                self.state = CircuitState.CLOSED
                self.failure_count = 0
        else:
            self.failure_count = 0

class CircuitOpenError(Exception):
    pass

Half-open state, tam olarak: küçük, sınırlı sayıda deneme çağrısının geçmesine izin verir. Half-open sırasında tek bir hata, circuit’i anında yeniden açar — bu asimetri (yeniden açmaya hızlı, tamamen kapatmaya temkinli) kasıtlıdır ve marjinal, kararsız koşullar altında open ile closed arasında “flapping"i önleyen tam olarak budur.

5.1.4 Bulkhead İzolasyonu

Gemi bölmelendirmesinden adını alır — bir yavaş/başarısız bağımlılığın, ilgisiz çağrıların ihtiyaç duyduğu kaynakları tüketememesi için her downstream bağımlılık başına kaynak havuzlarını (thread pool’lar, connection pool’lar, semaphore’lar) izole et.

import asyncio

class Bulkhead:
    def __init__(self, max_concurrent: int, max_queue: int):
        self._sem = asyncio.Semaphore(max_concurrent)
        self._queue_limiter = asyncio.Semaphore(max_concurrent + max_queue)

    async def execute(self, fn: Callable[[], Awaitable[T]]) -> T:
        if not self._queue_limiter.locked() and self._queue_limiter._value <= 0:
            raise BulkheadFullError("Bulkhead kuyruğu tükendi")
        async with self._queue_limiter:
            async with self._sem:
                return await fn()

class BulkheadFullError(Exception):
    pass

# Downstream bağımlılık başına bir bulkhead — tüm mesele bu
payment_bulkhead = Bulkhead(max_concurrent=10, max_queue=20)
inventory_bulkhead = Bulkhead(max_concurrent=20, max_queue=40)

Bulkhead’ler olmadan, tek bir yavaş downstream bağımlılık, paylaşılan bir thread/connection pool’unun %100’ünü tüketebilir ve sağlıklı bağımlılıklara giden istekleri aç bırakabilir — “kademeli arıza” başlıklı postmortem’lerdeki en yaygın kök neden etiketi budur.

5.1.5 Rate Limiting

Senin sistemini aşırı yüklenmekten korur (aşırı yüklenmiş bir downstream‘den caller’ları koruyan circuit breaker’ların aksine).

import time
import threading

class TokenBucket:
    def __init__(self, rate: float, capacity: int):
        self.rate = rate            # saniyede eklenen token
        self.capacity = capacity
        self.tokens = capacity
        self.last_refill = time.monotonic()
        self._lock = threading.Lock()

    def allow(self, cost: int = 1) -> bool:
        with self._lock:
            now = time.monotonic()
            elapsed = now - self.last_refill
            self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
            self.last_refill = now
            if self.tokens >= cost:
                self.tokens -= cost
                return True
            return False

Token bucket vs. sliding window vs. fixed window: token bucket, uzun vadeli ortalama oranı zorlarken capacity‘ye kadar kontrollü patlamalara izin verir — genelde doğru varsayılan. Fixed window en basitidir ama pencere sınırlarında 2x patlamaya izin verir (bilinmesi değerli, nadiren kullanılması değerli). Sliding window log kesindir ama key başına O(n) bellek gerektirir; sliding window counter bunu O(1)‘de yaklaşık olarak sağlar.

5.2 Pattern’ları Birleştirmek — Gerçek Production Yığını

Bu pattern’lar alternatif değildir; dayanıklı bir çağrı yolu hepsini şu sırayla uygulanmış şekilde kompoze eder:

Rate Limiter (kendini koru)
  → Bulkhead (bu bağımlılığın kaynak havuzunu izole et)
    → Circuit Breaker (bu bağımlılık sağlıksızsa hızlı başarısız ol)
      → Timeout (herhangi bir çağrının ne kadar sürebileceğini sınırla)
        → Retry (backoff+jitter ile) (geçici arızadan toparlan)
          → [gerçek network çağrısı]
async def resilient_call(fn: Callable[[], Awaitable[T]], breaker: CircuitBreaker, bulkhead: Bulkhead) -> T:
    async def with_timeout():
        return await asyncio.wait_for(fn(), timeout=5.0)

    async def with_retry():
        return await retry_with_backoff(with_timeout, max_attempts=3)

    async def with_breaker():
        return breaker.call(lambda: asyncio.get_event_loop().run_until_complete(with_retry()))
        # (açıklayıcı — gerçek async kodda, async-native bir breaker implementasyonu kullan)

    return await bulkhead.execute(with_retry)

Kritik bir sıralama kuralı: retry, circuit breaker’ın hata sayımının dışında değil, içinde olmalıdır — aksi halde her retry denemesi, mantıksal çağrı başına breaker’ın hata sayacını bağımsız olarak birden çok kez tetikler, bu da gerçek downstream sağlığına göre çok daha agresif bir şekilde açılmasına neden olur.

5.3 Zarafetle Bozulma (Graceful Degradation) ve Fallback’ler

Resilience yalnızca başarısız olmamak değildir — faydalı şekilde başarısız olmakla ilgilidir.

async def get_recommendations(user_id: str) -> list[Recommendation]:
    try:
        return await ml_recommendation_service.get(user_id, timeout=0.3)
    except (TimeoutError, CircuitOpenError):
        # Fallback: daha ucuz, cache'lenmiş, "yeterince iyi" bir yanıta düş
        return await get_popular_items_cached()

Fallback stratejilerini bozulma maliyetine göre sırala:

  1. Bayat cache’i sun (en iyisi: neredeyse tam işlevsellik, biraz eski veri).
  2. Daha ucuz/basit hesaplanmış bir sonuç sun (kısmi işlevsellik).
  3. Statik bir varsayılan sun (minimal işlevsellik, her zaman kullanılabilir).
  4. Tüm isteği değil, o özelliği başarısız kıl (blast radius’u izole et — örn. tüm sayfayı 500’lemek yerine, sayfayı öneri widget’ı olmadan render et).

5.4 Chaos Engineering

Sistemini enjekte edilmiş arıza altında doğrulamadan dayanıklı olduğunu bilemezsin — chaos testi olmayan resilience pattern’ları doğrulanmamış varsayımlardır.

İlkeler (Principles of Chaos Engineering’den):

  1. Steady state‘i ölçülebilir bir çıktı olarak tanımla (dahili metrikler değil — başarılı checkout oranı gibi iş/kullanıcı-yönelimli metrikler).
  2. Steady state’in hem kontrol hem deneysel grupta geçerli olduğunu hipotez et.
  3. Gerçek dünya event’leri enjekte et: broker/node arızası, latency enjeksiyonu, network partition, kaynak tükenmesi, clock skew.
  4. Hipotezi çürütmeye çalış — amaç, zayıflıkları production’da seni bulmadan önce bulmaktır.
  5. Blast radius’u minimize et: staging’de başla, sonra production trafiğinin ufak bir yüzdesi, güven arttıkça artır.
# Test ortamında chaos testi için minimal bir latency/failure enjeksiyon wrapper'ı
import random

class ChaosMiddleware:
    def __init__(self, failure_rate: float = 0.0, latency_ms: tuple[int, int] = (0, 0)):
        self.failure_rate = failure_rate
        self.latency_ms = latency_ms

    async def __call__(self, fn: Callable[[], Awaitable[T]]) -> T:
        if self.latency_ms != (0, 0):
            await asyncio.sleep(random.uniform(*self.latency_ms) / 1000)
        if random.random() < self.failure_rate:
            raise ConnectionError("Chaos ile enjekte edilmiş arıza")
        return await fn()

Production-grade araçlar: Kubernetes-native fault injection için Chaos Mesh ve LitmusChaos; yönetilen chaos-as-a-service için Gremlin; özellikle Kafka için, load test sırasında broker’ları/partition leader’larını öldürmek, çoğu ekibin asla çalıştırmadığı en yüksek değerli tek chaos deneyidir.

5.5 Resilience Ön Koşulu Olarak Observability

Gözlemleyemediğin bir sisteme resilience inşa edemezsin. Üç sütun, ve her birinin gerçekte ne için olduğu:

Herhangi bir servis için golden signal’lar (Google SRE kitabı): Latency, Traffic, Errors, Saturation. Production’daki bir Kafka consumer’ı için her dashboard’un en az şunlara ihtiyacı vardır: consumer lag (partition başına), işleme latency’si, hata/DLQ oranı ve rebalance sıklığı — özellikle rebalance sıklığı, çünkü sık rebalancing’in kendisi bir resilience kırmızı bayrağıdır (genelde yavaş işleme nedeniyle aşılan max.poll.interval.ms‘den kaynaklanır, bu da orijinal sorunu daha da kötüleştirir).

5.6 Dead Letter Queue’lar ve Zehirli Mesaj Yönetimi

Tek bir hatalı biçimlendirilmiş mesaj, tüm bir consumer group’unu asla durduramamalıdır.

def process_with_dlq(consumer, producer, dlq_topic: str, max_retries: int = 3):
    while True:
        msg = consumer.poll(1.0)
        if msg is None:
            continue

        retry_count = get_retry_count(msg.headers())
        try:
            process_order(msg.value())
            consumer.commit(msg, asynchronous=False)
        except PoisonPillError as e:
            if retry_count >= max_retries:
                producer.produce(
                    dlq_topic,
                    key=msg.key(),
                    value=msg.value(),
                    headers=[
                        ("original_topic", msg.topic().encode()),
                        ("original_partition", str(msg.partition()).encode()),
                        ("original_offset", str(msg.offset()).encode()),
                        ("error", str(e).encode()),
                        ("retry_count", str(retry_count).encode()),
                    ],
                )
                producer.flush()
                consumer.commit(msg, asynchronous=False)  # zehirli mesajı geç
            else:
                increment_retry_and_requeue(msg)

DLQ header’larında tam kaynak bilgisini (original_topic, original_partition, original_offset, hata detayı) koru — bu olmadan, DLQ mesajları kurtarılabilir bir kuyruk değil, yeniden üretilemez, debug edilemez bir mezarlık haline gelir.


6. Hepsini Birleştirmek: Referans Mimari

Python’da dayanıklı, reactive, event-driven bir sipariş işleme pipeline’ı:

[API Gateway] --HTTP--> [Order Service]
                              |
                    (1. Bir transaction içinde yerel DB'ye yaz)
                    (2. Aynı transaction'da outbox satırı yaz)
                              |
                    [Debezium CDC / Outbox Publisher]
                              |
                              v
                    Kafka Topic: orders.created (3 partition, RF=3, key=customer_id)
                         /              \
                        /                \
        [Inventory Consumer Group]   [Notification Consumer Group]
         (idempotent upsert,          (idempotent, rate-limited,
          inventory DB'ye circuit      bulkhead-izole edilmiş
          breaker, DLQ)                email/SMS provider çağrıları)
                |
                v
    Kafka Topic: orders.inventory-reserved
                |
                v
     [Payment Saga Orchestrator] --çağırır--> [Payment Service]
        (payment çağrısında circuit             (order_id başına
         breaker + retry+jitter +                idempotency key)
         timeout + bulkhead)
                |
        başarı -> orders.completed
        başarısızlık -> compensating action'lar -> orders.cancelled

Bu mimarinin kodladığı tasarım kararları:


7. İleri Okuma


Bu doküman, ekosistemin 2026’nın erken-orta dönemindeki durumunu yansıtır. Kafka tamamen KRaft’a geçmiştir (ZooKeeper kaldırıldı); Python’ın free-threaded build’i hâlâ deneyseldir; sürüme özgü detayları değişebilir olarak ele al ve production kararlarından önce güncel release notlarıyla doğrula.