İçeriğe Atla
Mustafa Erbay
Rehberler · 8 dk okuma · görüntülenme Read in English

Dağıtık Sistemlerde Idempotency Tasarımı: Güncel Yaklaşım

API isteklerinde timeout sonrası yaşanan 'işlem yapıldı mı?' kaosunu çözmek için idempotency anahtarlarını ve veritabanı stratejilerini nasıl kurguluyorum?

100%

Dağıtık sistemlerde bir API isteği gönderip “timeout” aldığımda, o işlemin sunucu tarafında gerçekten yapılıp yapılmadığını bilmemek kadar sinir bozucu çok az şey vardır. TCP bağlantısı kopmuş olabilir, load balancer 504 Gateway Timeout dönmüş olabilir ama arka planda veritabanı commit’i çoktan atmış olabilir. Bu belirsizlik anında istemci (client) “ne olur ne olmaz” diyerek isteği tekrar gönderdiğinde, eğer sistemim idempotency (eşgüçlülük) prensibine göre tasarlanmamışsa, aynı siparişten iki tane oluşur veya aynı fatura iki kez kesilir.

Saha tecrübemde gördüğüm en büyük yanılgı, idempotency’nin sadece “bir kontrol ekleyelim” denilerek çözülebileceği düşüncesidir. Gerçekte ise bu, network katmanından veritabanı transaction yönetimine kadar uzanan mimari bir karardır. Bir üretim ERP’si üzerinde çalışırken, operatör ekranlarından gelen mükerrer üretim emirleri yüzünden hammadde stoklarının saptığını fark ettiğimizde, sorunun “hızlı tıklanan butonlar” değil, zayıf idempotency tasarımı olduğunu anladım. Bu yazıda, modern sistemlerde bu problemi nasıl çözdüğümü ve hangi trade-off’ları göze aldığımı anlatacağım.

Network Yalancıdır: Retry Mekanizmalarının Karanlık Yüzü

Bir network paketinin hedefe ulaştığından emin olmanın tek yolu onay (ACK) almaktır, ancak o onayın size ulaşmaması paketinin gitmediği anlamına gelmez. Ben buna “Ağ Katmanı Paradoksu” diyorum. Özellikle mobil uygulamalarda veya fabrika sahasındaki zayıf Wi-Fi ağlarında çalışan cihazlarda, paket sunucuya ulaşır, işlenir, ancak yanıt dönerken bağlantı kopar. İstemci tarafındaki “retry” mantığı, bu durumda sistemin en büyük düşmanı haline gelir.

Zayıf bağlantı altında çalışan mobil istemcilerde kullanıcılar aynı formu farkında olmadan üst üste birkaç kez gönderebilir. Eğer backend tarafında her isteği “yeni bir emir” gibi kabul ederseniz, veritabanı logları şişer ve tutarsızlıklar başlar. İşte bu noktada Idempotency-Key kavramı devreye giriyor. İstemci, her benzersiz işlem için bir UUID oluşturup bunu header olarak göndermeli. Sunucu ise bu anahtarı gördüğünde “ben bu filmi daha önce izledim mi?” diye sormalı.

POST /api/v1/orders
Host: api.mustafaerbay.com
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{
  "product_id": "SKU-123",
  "quantity": 10
}

Bu yapıda, sunucu 201 Created döndükten sonra aynı anahtarla bir istek daha gelirse, işlemi tekrar yapmak yerine daha önce oluşturduğu cevabı (cache’ten veya DB’den) dönmelidir. Ancak burada kritik bir detay var: HTTP 409 Conflict mi dönmelisiniz yoksa orijinal cevabı mı? Ben genellikle orijinal cevabı dönmeyi tercih ediyorum ki client tarafındaki state makinesi bozulmasın.

Veritabanı Seviyesinde Idempotency: PostgreSQL Yaklaşımı

Uygulama kodunda if exists kontrolü yapmak çoğu zaman yeterli değildir çünkü “race condition” dediğimiz o sinsi bela her an karşınıza çıkabilir. İki paralel istek milisaniyeler farkıyla geldiğinde, ikisi de “kayıt yok” kontrolünden geçebilir. Bu yüzden ben her zaman veritabanı seviyesinde “Unique Constraint” ve PostgreSQL’in INSERT ... ON CONFLICT yeteneklerine güveniyorum.

Yüksek hacimli yazma yapan sistemlerde, örneğin makinelerden gelen sensör verilerini işlerken network dalgalanmaları nedeniyle aynı veri paketleri tekrar edebilir. Bu hacimde uygulama katmanında her istek için ayrıca bir kontrol sorgusu çalıştırmak veritabanına belirgin bir ek yük bindirir. Çözümü veritabanı şemasını şu şekilde kurgulamakta buluyorum:

CREATE TABLE idempotency_keys (
    id UUID PRIMARY KEY,
    response_code SMALLINT NOT NULL,
    response_body JSONB NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- İşlem anında kullanım:
INSERT INTO idempotency_keys (id, response_code, response_body)
VALUES ('550e8400-e29b-41d4-a716-446655440000', 201, '{"status": "success"}')
ON CONFLICT (id) DO UPDATE SET 
    created_at = EXCLUDED.created_at -- Sadece varlığını doğrula ve mevcut kaydı dön
RETURNING *;

Bu yaklaşım, atomik bir işlem sağlar. PostgreSQL 16+ sürümlerinde bu tür yoğun yazma operasyonlarında WAL (Write Ahead Log) şişmesini önlemek için idempotency_keys tablosunu belirli aralıklarla (örneğin 24 saat sonra) “vacuum” etmek veya partition kullanarak eski anahtarları temizlemek şart. Unutmayın, idempotency anahtarları sonsuza kadar saklanmamalı; işlemin “finality” süresine göre bir TTL (Time To Live) belirlenmelidir.

Redis vs PostgreSQL: State Saklama Stratejileri

Hangi depolama birimini seçeceğiniz, işlemin kritikliği ile doğrudan ilgilidir. Eğer bir finansal hesaplayıcı veya ödeme sistemi geliştiriyorsam, asla Redis’e güvenmem. Redis, varsayılan konfigürasyonunda bir “cache” birimidir ve bir crash anında (persistence ayarları çok agresif değilse) son birkaç saniyelik veriyi kaybedebilir. Bu da mükerrer ödeme demektir. Ancak, bir “beğeni” butonu veya “okundu” bilgisi gibi düşük riskli işlemler için Redis mükemmeldir.

Redis kullanırken SET key value NX PX 86400000 (Not Exists ve Expiry ile) komutu hayat kurtarır. Ancak Redis cluster’da “failover” süreci sırasında, master değişirken milisaniyelik bir senkronizasyon kaybı yaşanabilir ve aynı anahtar iki farklı node’a yazılabilir. Bu risk nedeniyle, “para” veya “stok” içeren her türlü operasyonda PostgreSQL’de bir idempotency tablosu tutmayı kural haline getirdim.

PostgreSQL kullanmanın bir diğer avantajı da transaction bütünlüğüdür. İşlem başarısız olursa (rollback), idempotency anahtarını da silersiniz. Redis’te uygulama kodu crash olursa anahtar orada asılı kalabilir ve istemci “zaten yapıldı” hatası almasına rağmen aslında işlem DB’ye yansımamış olabilir.

Mesaj Kuyrukları ve “Exactly-Once” Yalanı

Distributed sistemlerde RabbitMQ veya Kafka kullanırken size “Exactly-Once Delivery” vaat edenlere şüpheyle yaklaşın. Network katmanında bu neredeyse imkansızdır. Gerçekçi olan yaklaşım “At-Least-Once Delivery” ve “Idempotent Consumer” ikilisidir. Yani mesajın en az bir kez geleceğini kabul edip, tüketici (consumer) tarafını buna göre hazırlamaktır.

Mesaj kuyruğu tabanlı entegrasyonlarda, kuyruktan gelen siparişlerin zaman zaman birden fazla kez işlendiğini görmek olağandır. Tipik sorun, consumer’ın mesajı işleyip tam “ACK” gönderecekken bağlantısının kopmasıdır. Mesaj kuyruğu (broker), ACK gelmediği için mesajı tekrar sıraya koyar. Çözüm, her mesajın içine bir message_id gömmek ve consumer tarafında bu ID’yi bir “processed_messages” tablosuna yazmaktır.

# FastAPI + PostgreSQL Consumer Örneği
async def process_order(message: OrderMessage):
    async with db.transaction():
        # 1. Mesaj daha önce işlendi mi kontrol et (Idempotency check)
        is_processed = await db.execute(
            "INSERT INTO processed_messages (msg_id) VALUES (:id) ON CONFLICT DO NOTHING",
            {"id": message.msg_id}
        )
        
        if not is_processed:
            # 2. Daha önce işlenmiş, sessizce geç
            return 

        # 3. Gerçek iş mantığı
        await create_production_order(message.data)

Bu yapı sayesinde, aynı mesaj 100 kez de gelse, veritabanı seviyesindeki unique constraint sayesinde sadece ilk mesaj “gerçek” işi tetikler. Diğerleri ise sadece loglarda bir satır olarak kalır. Daha önce PostgreSQL performans tuning yazımda bahsettiğim gibi, bu tür tabloların indexlerini doğru kurgulamazsanız, sistem büyüdükçe consumer lag (gecikme) artmaya başlar.

API Tasarımında Idempotency: Sadece POST mu?

Genel kanı sadece POST isteklerinin idempotent olması gerektiği yönündedir. Çünkü GET, PUT ve DELETE zaten doğası gereği idempotent kabul edilir. Ancak pratik dünya her zaman RFC dökümanlarındaki gibi toz pembe değil. Örneğin, bir kaynağı silen DELETE /user/1 isteği, ikinci kez çağrıldığında 404 döner. Bu teknik olarak idempotenttir (sunucu durumu değişmez), ancak istemci tarafında “hata” olarak algılanabilir.

Benim yaklaşımım, özellikle kritik “state” değişimlerinde (iptal etme, onaylama, arşivleme) her zaman açık bir anahtar kullanmak. Bir “iptal” işlemi (PUT/PATCH) yaparken bile request_id takibi yapmak, sistemin izlenebilirliğini (observability) artırıyor. İzlenebilirliğe önem veren mimarilerde tüm mutasyon (yazma) isteklerini bir Operation-ID ile zorunlu kılmak işe yarar; bu sayede loglarda bir işlemin tüm serüvenini tek bir ID ile takip edebilirsiniz.

Ayrıca, idempotency anahtarlarının formatı da önemlidir. Sadece bir rastgele sayı yerine, işlemin tipini ve zamanını içeren deterministic anahtarlar (örneğin: order_create_user123_ts20260517) bazen debugging aşamasında hayat kurtarabilir. Ancak güvenlik açısından UUID v4 her zaman en sağlam limandır; tahmin edilemezlik, bir kullanıcının diğerinin anahtarını “çakıştırmasını” engeller.

Hata Yönetimi ve Operasyonel Deneyimler

Idempotency tasarımı yaparken en çok yapılan hata, “anahtar zaten var” durumunda sadece hata kodu dönmektir. Kullanıcıya “Bu işlemi zaten yaptınız” demek yerine, işlemin sonucunu sanki yeni yapılmış gibi göstermek (transparent retry) kullanıcı deneyimini (UX) muazzam iyileştirir. Kullanıcı butona bastığında interneti kopmuşsa ve tekrar bastığında “Siparişiniz Başarıyla Alındı” (ikinci kez oluşturulmadan) mesajını görüyorsa, sisteminiz başarılı demektir.

Bir üretim ERP’sinin operatör panelinde, bir iş emri onaylanırken veritabanı lock’a düşmüştü. Operatör yanıt gelmeyince butona üst üste basmış. Eğer idempotency anahtarı sadece “insert” anında kontrol edilseydi, ilk istek lock’ta beklerken diğerleri hata alacaktı. Çözüm olarak “Processing” (İşleniyor) statüsünü de anahtar tablosuna ekledik. Eğer bir anahtar için durum “processing” ise, gelen diğer isteklere “Lütfen bekleyin, işleminiz devam ediyor” (HTTP 102 Processing) dönmeye başladık.

// HTTP 102 Response
{
  "status": "processing",
  "message": "İşleminiz şu an bir worker tarafından ele alınıyor.",
  "retry_after": 2
}

Bu yaklaşım, özellikle uzun süren arka plan işlerinde (rapor oluşturma, AI ile üretim planlama gibi) sistemin aynı işi birden fazla worker’a atamasını engelliyor. Kaynak tüketimini (CPU/RAM) optimize etmek istiyorsanız, bu tür “kilit mekanizmaları” (locking) idempotency ile harmanlamalısınız.

Sonuç: Pragmatik Bir Yaklaşım

Dağıtık sistemlerde %100 garanti diye bir şey yoktur; sadece riskleri minimize etmek vardır. Idempotency, bu riskleri yönetmek için elimizdeki en güçlü araçtır. Tasarım yaparken kendinize şu soruları sorun: “Bu işlem iki kez yapılırsa şirket ne kadar zarar eder?” ve “Bu kontrolü nerede yaparsam en az performans kaybını yaşarım?”.

Benim net pozisyonum şudur: Finansal ve operasyonel her türlü yazma işleminde veritabanı seviyesinde zorunlu idempotency anahtarı kullanılmalıdır. Uygulama kodundaki kontroller sadece bir “ön katman” olabilir ama son sözü her zaman veritabanı söylemelidir. Karmaşık kütüphaneler veya “magic” framework’ler yerine, basit bir idempotency_keys tablosu ve iyi kurgulanmış bir middleware, sisteminizi yıllarca ayakta tutmaya yeter.

Bir sonraki yazıda, bu anahtarların temizliği için systemd timer ve PostgreSQL partitioning ikilisini nasıl kullandığımı anlatacağım. Sonraki adımlar için Dağıtık Log İzleme rehberine göz atabilirsiniz.

Paylaş:

Bu yazı faydalı oldu mu?

Yükleniyor...

Bu yazı nasıldı?

Sıkça Sorulanlar

Bu makale ile ilgili okurların sorduğu yaygın sorular.

Dağıtık sistemlerde idempotency tasarımı nasıl uygulanır?
Ben, dağıtık sistemlerde idempotency tasarımı uygularken, network katmanından veritabanı transaction yönetimine kadar uzanan bir mimari karar veriyorum. Bu, sadece bir kontrol eklemek değil, sistemimin eşgüçlülük prensibine göre tasarlanması requiredir. Örneğin, bir API isteğinin sunucu tarafında gerçekten yapılıp yapılmadığını bilmemek kadar sinir bozucu çok az şey vardır, bu nedenle sistemimi bu tür belirsizliklere karşı hazırlamaktayım.
Idempotency anahtarlarını ve veritabanı stratejilerini nasıl kurguluyorsunuz?
Ben, idempotency anahtarlarını ve veritabanı stratejilerini kurgularken, sistemimin réseau katmanından başlayarak, her bir bileşenin eşgüçlülük prensibine göre tasarlanmasını sağlıyorum. Örneğin, bir üretim ERP'sinde çalışırken, operatör ekranlarından gelen mükerrer üretim emirleri yüzünden hammadde stoklarının saptığını fark ettiğimde, sorunun 'hızlı tıklanan butonlar' değil, zayıf idempotency tasarımı olduğunu anladım ve bu nedenle sistemimi bu tür hatalara karşı hazırlamaktayım.
Idempotency tasarımı uygularken nelere dikkat edilmelidir?
Ben, idempotency tasarımı uygularken, sistemimin network katmanından veritabanı transaction yönetimine kadar uzanan bir mimari karar verdiğimi ve bu kararı verirken, sistemimin eşgüçlülük prensibine göre tasarlanması gerektiğini dikkate alıyorum. Ayrıca, retry mekanizmalarının karanlık yüzünü ve ağ katmanı paradoksunu da dikkate alıyorum ve sistemimi bu tür hatalara karşı hazırlamaktayım.
ME

Mustafa Erbay

Sistem Mimarisi · Network Uzmanı · Altyapı, Güvenlik ve Yazılım

2006'dan bu yana sistem mimarisi, network, sunucu altyapıları, büyük yapıların kurulumu, yazılım ve sistem güvenliği ekseninde çalışıyorum. Bu blogda sahada karşılığı olan teknik deneyimlerimi paylaşıyorum.

Kişisel Notlar

Bu notlar sadece sizde saklanır. Tarayıcınızda yerel olarak tutulur.

Hazır 0 karakter

Yorumlar

Sunucu Taraflı AI Moderasyon

Yorumlar sunucuda yapay zeka ile denetlenir ve kalıcı olarak saklanır.

?
0/2000

Sunucu taraflı AI denetim

✉️ Ücretsiz · Spam yok · İstediğin an çık

Yeni yazılardan haberdar olun

Yeni içerikler ve teknik notlar e-postanıza gelsin.

  • 📌
    Haftanın en iyisi Sadece okumaya değer tek yazı
  • 🔧
    Alet çantası Bu hafta kullandığım araçlar
  • 🧠
    Perde arkası Blog'a girmeyen notlar

Spam yapmıyoruz. İstediğiniz zaman ayrılabilirsiniz. · Sadece Umami (self-hosted, Google yok) ile takip.

Okuma İstatistikleriniz

0

Yazı Okundu

0dk

Okuma Süresi

0

Gün Serisi

-

Favori Kategori

İlgili Yazılar