Dağıtık sistemler kurarken çoğu zaman farkında olmadan bir “eventual consistency” modeline doğru itiliriz. Bu, verinin eninde sonunda tutarlı olacağı, ancak her an tutarlı kalmayacağı anlamına gelir. İlk başta kulağa mantıklı ve ölçeklenebilir bir çözüm gibi gelse de, bu modelin geliştiricinin zihniyetine ve sistemin genel karmaşıklığına ne kadar derin bir etki bıraktığını yıllar içinde defalarca deneyimledim. Bu yazı, bu zihniyet değişiminin getirdiği zorlukları ve benim bu durumla nasıl başa çıktığımı anlatıyor.
Bir ERP sisteminde stok takibi yaparken, bir ürünün çıkışı yapıldığında stoğun hemen düşmesini bekleriz. Ancak, eğer bu işlem dağıtık bir yapıdaysa ve stok güncellemesi asenkron bir kuyruktan geçiyorsa, kullanıcı bir saniye sonra stok durumuna baktığında eski değeri görebilir. Bu durum, “ne oldu şimdi?” sorusunu akla getirir ve geliştirici olarak bu “zaman gecikmeli tutarlılık” ile yaşamayı öğrenmemiz gerekir. Bu öğrenme süreci, sadece teknik bir konu olmaktan çıkıp, problem çözme ve sistem tasarımı yaklaşımımızı kökten değiştirir.
Eventual Consistency’nin Vaadi ve Beklenmedik Gerçekleri
Eventual consistency, dağıtık sistemlerde yüksek erişilebilirlik ve performans elde etmek için sıklıkla tercih ettiğimiz bir model. Özellikle mikroservis mimarilerinde veya coğrafi olarak dağıtık sistemlerde, her işlemi anında, tam tutarlılıkla gerçekleştirmek ağ gecikmeleri ve kaynak kilitlenmeleri nedeniyle pratik olmayabilir. Bu yüzden, veri kopyalarının bir süre farklı olabileceği, ancak sonunda aynı duruma geleceği varsayılır. Bu vaat, ölçeklenebilir sistemler inşa etmenin temel taşlarından biri gibi görünür.
Ancak gerçekler, bu vaadin ne kadar ince bir çizgi üzerinde yürüdüğünü gösterir. Bir üretim firmasının ERP sisteminde, sipariş oluşturma anında stok kontrolü yapıp, sonra sevkiyat emri verildiğinde stoğu düşürdüğümüz bir senaryomuz vardı. Sevkiyat emri, asenkron bir mesaj kuyruğuna atılıyor ve gerçek stok düşüşü kısa bir süre sonra gerçekleşiyordu. Eğer kullanıcı aynı anda ikinci bir sipariş oluşturmaya çalışırsa ve sistem henüz ilk siparişin stok düşüşünü işlemediyse, stokta olmayan bir ürün için sipariş oluşturma riskimiz doğuyordu. Bu durum, “eventual” dediğimiz zaman diliminin ne kadar kritik olabileceğini, hatta kısacık bir gecikmenin bile iş akışını nasıl bozabileceğini gösterdi. Çözüm olarak, sipariş anında “reserved stock” kavramını devreye aldık ve asıl stok düşüşü gerçekleşene kadar bu rezervasyonu kullandık. Bu da ek bir karmaşıklık getirdi.
Geliştirici Zihniyetine Yüklediği Ek Yük
Strong consistency modeline alışkın bir geliştirici için eventual consistency’ye geçiş, ciddi bir zihinsel değişim gerektirir. Artık “veriyi kaydettim, şimdi okuyabilirim” basitliği ortadan kalkar. Yerine, “veriyi kaydettim, bir süre sonra okunabilir olacak, bu arada eski veri görünebilir” gibi çok daha karmaşık bir düşünce yapısı gelir. Bu, her sorguda, her veri okumasında “bu veri şu an güncel mi?” sorusunu akla getirir. Bu şüphecilik, geliştirme sürecini yavaşlatır ve hata yapma olasılığını artırır.
Kullanıcıların asenkron çalışan bir işlemi tamamladığında anında sonuç görmesini beklediği durumlar tipik bir örnektir. Kullanıcıya “işleminiz alındı, kısa süre içinde sonuçlanacak” mesajı göstermek kolaydır, ama asıl zorluk, kullanıcı daha sonraki bir ekranda bu işlemin sonucunu görmeye çalıştığında verinin henüz senkronize olmamış olmasıdır. Bunun yaygın çözümü, her bir işlem için bir “işlem durumu” (processing, completed, failed) tutmak ve arayüzü bu duruma göre güncellemektir. Bu, sadece bir veritabanı sütunu eklemekten ibaret değildir; UI’da polling mekanizmaları kurmak, WebSocket ile bildirim göndermek ve kullanıcıya bu geçişi açıkça belirtmek gibi ek işler getirir. Bu tür bir yaklaşım, geliştiricinin sadece kodu yazmakla kalmayıp, kullanıcı deneyimini ve sistemin davranışını çok daha derinlemesine düşünmesini gerektirir.
# Gelen bir siparişin asenkron işlenmesi
def process_order_async(order_id):
# Siparişin durumunu "processing" olarak işaretle
db.orders.update_one({"_id": order_id}, {"$set": {"status": "processing"}})
# Mesaj kuyruğuna gönder
message_queue.publish("order_processing_queue", {"order_id": order_id})
# Kullanıcıya hemen bir yanıt dön
return {"message": "Siparişiniz alındı, işleniyor.", "order_id": order_id, "status": "processing"}
# Arka plan worker'ı
def worker_process_order(message):
order_id = message["order_id"]
# Gerçek stok düşüşü ve diğer iş mantığı
success = perform_stock_deduction(order_id)
if success:
db.orders.update_one({"_id": order_id}, {"$set": {"status": "completed", "completed_at": datetime.now()}})
else:
db.orders.update_one({"_id": order_id}, {"$set": {"status": "failed", "failed_at": datetime.now()}})
# Kullanıcının sipariş durumunu sorgulaması
def get_order_status(order_id):
order = db.orders.find_one({"_id": order_id})
return {"order_id": order_id, "status": order["status"], "details": order.get("details")}
Yukarıdaki basit örnek bile, status alanının ve arka plan worker’ının gerekliliğini gösteriyor. Bu, strong consistency’de tek bir atomik işlemle halledilebilecek bir durumu, iki farklı aşamaya ayırarak geliştiricinin zihninde ek bir yük oluşturuyor.
Transaction Outbox ve Idempotency: Kaçınılmaz Desenler
Eventual consistency ile çalışırken, sistemin karmaşıklığını yönetmek için bazı mimari desenler neredeyse kaçınılmaz hale gelir. Bunların başında “Transaction Outbox” ve “Idempotency” gelir. Bir işlem atomik olarak hem veritabanına kayıt yapıp hem de bir mesaj kuyruğuna mesaj göndermeyi gerektirdiğinde, Transaction Outbox deseni devreye girer. Bu sayede, veritabanı işlemi ve mesaj gönderme işlemi “aynı transaction” içinde olmasa bile, veritabanı işleminden sonra mesajın mutlaka gönderileceği garanti altına alınır.
Bir üretim ERP’sinde, üretim emri tamamlandığında hem stok hareketlerini kaydetmemiz hem de ilgili finans modülüne bir olay göndermemiz gerekiyordu. Başlangıçta bu iki işlemi ayrı ayrı yapıyorduk, ancak zaman zaman mesaj kuyruğuna mesaj gönderilemediği durumlar oluyordu ve bu da stok ile finans kayıtları arasında tutarsızlıklara yol açıyordu. Çözüm olarak, bir outbox tablosu oluşturduk. Stok hareketi veritabanına kaydedilirken, aynı transaction içinde outbox tablosuna da bir kayıt ekleniyordu. Ayrı bir servis, bu outbox tablosunu sürekli tarıyor ve mesajları kuyruğa gönderiyordu. Mesaj başarıyla gönderildiğinde outbox kaydı siliniyordu. Bu sayede, finans modülü her zaman doğru ve eksiksiz bilgiyi almayı garanti altına aldı.
-- PostgreSQL'de Transaction Outbox deseni için örnek tablo
CREATE TABLE outbox_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_type VARCHAR(255) NOT NULL,
aggregate_id UUID NOT NULL,
event_type VARCHAR(255) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
processed_at TIMESTAMP WITH TIME ZONE,
is_processed BOOLEAN DEFAULT FALSE
);
-- Bir işlem örneği
BEGIN;
-- Ana iş mantığı
INSERT INTO stock_movements (product_id, quantity, type) VALUES ('prod123', 10, 'OUT');
-- Outbox'a mesaj ekle
INSERT INTO outbox_messages (aggregate_type, aggregate_id, event_type, payload)
VALUES ('stock_movement', 'prod123', 'stock_deducted', '{"product_id": "prod123", "quantity": 10}');
COMMIT;
Bu deseni kullanmak, geliştiricinin zihninde “bir işlemi sadece bir kez yapabilirim” düşüncesini “bir işlemi birden fazla kez yapabilirim, ama sonucun hep aynı olmasını sağlamalıyım” yani “idempotency” düşüncesine iter. Mesaj kuyruklarında mesajların bazen birden fazla kez gönderilebilmesi (at-least-once delivery), alıcı tarafın bu durumu idempotent olarak ele almasını zorunlu kılar. Bir işlemi iki kez alırsak, ikinci kez geldiğinde ilk işlemin etkilerini tekrarlamamalıyız. Örneğin, bir kullanıcının bakiyesine 10 TL ekleme işlemi, mesaj iki kez gelse bile bakiyeyi sadece 10 TL artırmalıdır, 20 TL değil. Bu, her bir işlem için benzersiz bir işlem kimliği (transaction ID) tutmak ve bu kimliği kontrol etmekle sağlanır. dağıtık sistemlerde idempotent işlemler yazımda bu konuya daha detaylı değinmiştim.
Tutarlılık Modelleri ve Trade-off’lar: Seçim Nedenleri
Sistem tasarlarken, hangi tutarlılık modelini seçeceğimiz, iş gereksinimleri ve teknik kısıtlamalar arasında yapılan bir trade-off’tur. Strong consistency (güçlü tutarlılık), bir işlem tamamlandığında, o işlemin etkilerinin tüm sistemde anında görünür olmasını garanti eder. Bu, genellikle tek bir veritabanı veya dağıtık transaction yöneticileri ile sağlanır, ancak ölçeklenebilirlik ve erişilebilirlik açısından maliyetli olabilir. Eventual consistency ise tam tersine, verinin bir süre sonra tutarlı olacağını garanti eder, ancak bu süre içinde tutarsızlıklar yaşanabilir. Bu model, yüksek ölçeklenebilirlik ve erişilebilirlik gerektiren sistemler için daha uygundur.
Benim deneyimimde, bu kararı verirken her zaman iş akışının kritiklik seviyesini göz önünde bulundururum. Örneğin, bankacılık gibi finansal işlemlerde strong consistency mutlak bir gerekliliktir. Bir transferin yarıda kalması veya yanlış bilgi göstermesi kabul edilemez. Bu tür sistemlerde, dağıtık transaction’ların getirdiği performans düşüşünü veya karmaşıklığı göze alırız. Ancak, bir sosyal medya uygulamasındaki beğeni sayısı gibi, anlık tutarsızlığın büyük bir iş kaybına yol açmadığı durumlarda eventual consistency daha mantıklıdır. Kullanıcı bir beğeniyi görmese bile, birkaç saniye sonra görmesi büyük bir sorun yaratmaz ve sistemin milyarlarca kullanıcıya hizmet vermesini kolaylaştırır.
Coğrafi olarak dağıtık bir stok yönetiminde de benzer bir desen ortaya çıkar: dünyanın farklı yerlerindeki depoların stok bilgilerini anında senkronize etmek ağ gecikmeleri nedeniyle pratik değildir. Bunun yerine her bölge kendi stoğunu yönetir, belirli aralıklarla ana merkeze güncel bilgileri gönderir. Bu, bölgesel olarak strong consistency sağlarken, globalde eventual consistency’ye sahip olduğumuz anlamına gelir. Bir bölgede stok tükendiğinde diğer bölgelerdeki kullanıcılar bunu anında göremeyebilir, ancak bu çoğu zaman müşteri memnuniyeti açısından kabul edilebilir bir trade-off’tur.
Üretim Ortamında Gözlemlediğim Zorluklar ve Çözümler
Eventual consistency ile ilgili en büyük zorluklardan biri, üretim ortamında ortaya çıkan beklenmedik durumları tespit etmek ve hata ayıklamaktır. Veri tutarsızlıkları anlık olmadığı için, sorunlar genellikle birikerek ve daha büyük bir iş problemine dönüşerek kendini gösterir. Örneğin, bir ay boyunca küçük stok sapmaları birikip, ay sonunda yapılan envanter kontrolünde büyük bir fark yaratabilir. Bu, “sinsi” problemlerin başında gelir.
Tipik bir örnek, sevkiyat raporlarında sürekli küçük eksikliklerin görülmesidir. Başlangıçta göz ardı edilse de, yüksek sevkiyat hacminde bu küçük fark bile birikerek ciddi kayıplara yol açabilir. Bu tür sorunların kök nedeni çoğu zaman sevkiyat emri oluşturulduktan hemen sonra, stok güncellenmeden önce yapılan bir “son kontrol” adımıdır: bu adım asenkron stok düşüşünü henüz yansıtmadığı için, stokta görünmeyen ama aslında ayrılmış olan ürünleri gözden kaçırır. Çözüm, son kontrol adımını stok düşüşü tamamlandıktan sonra çalışacak şekilde yeniden tasarlamak ve aradaki tutarsızlık penceresini kapatmaktır.
Bu tür sorunları tespit etmek için, observability araçlarına yatırım yapmak şart. Sadece loglar ve metrikler değil, aynı zamanda dağıtık tracing (distributed tracing) de eventual consistency sorunlarını anlamak için kritik. Bir işlemin farklı servisler arasında nasıl yayıldığını ve verinin ne zaman tutarlı hale geldiğini görselleştirmek, sorunun kök nedenini bulmada paha biçilmezdir. Benim üretim sistemlerimde, OpenTelemetry tabanlı trace’ler kullanarak bir işlemin başlangıcından bitişine kadar geçen süreyi ve hangi servislerin hangi sırayla etkilendiğini izliyorum. Hatta bazen, belirli bir verinin ne zaman “güncel” hale geldiğini gösteren özel metrikler bile ekliyorum. observability ile sistem sorunlarını avlamak yazımda bu konuya değinmiştim.
Benim Yaklaşımım ve Geliştirici Olgunluğu
Eventual consistency ile yaşamak ve bu modeli başarıyla uygulamak, bir geliştirici olarak belirli bir olgunluk seviyesi gerektiriyor. Bu, sadece teknik bilgiyi değil, aynı zamanda iş akışlarını derinlemesine anlama ve olası hata senaryolarını öngörme yeteneğini de kapsar. Benim bu konudaki yaklaşımım birkaç ana prensibe dayanıyor:
- Tutarlılık Sınırlarını Belirlemek: Her zaman “Bu veri ne kadar süreyle tutarsız kalabilir ve bu durum işi ne kadar etkiler?” sorusunu sorarım. Bu sınırları iş birimleriyle birlikte net bir şekilde belirlerim. Finansal veriler için anlık tutarlılık şartken, raporlama için dakikalar mertebesinde, kullanıcı arayüzü için ise saniyelik gecikmeler gibi farklı toleranslar olabilir.
- Explicit Design: Eventual consistency’nin varlığını sistemin her katmanında açıkça belirtirim. Bu, API dokümantasyonundan kullanıcı arayüzü mesajlarına kadar her yerde bu durumu yansıtır. “İşleminiz şu anda işleniyor, sonuçları kısa süre içinde görünecektir” gibi mesajlar, kullanıcı beklentilerini yönetmede önemlidir.
- Hata Telafisi ve Geri Dönüş (Compensation & Rollback): Eventual consistency’nin getirdiği en büyük risklerden biri, bir hata durumunda sistemin nasıl eski bir tutarlı duruma döneceğidir. Bu yüzden, her dağıtık işlem için bir telafi mekanizması (compensation logic) veya geri dönüş stratejisi tasarlarım. Örneğin, bir stok düşüşü başarısız olursa, otomatik olarak stok geri artırılır ve ilgili taraflara bildirim gönderilir.
- Monitoring ve Alerting: Tutarsızlık pencerelerini aktif olarak izlerim. Eğer bir mesaj kuyruğunda bekleyen mesaj sayısı belirli bir eşiğin üzerine çıkarsa veya bir işlemin tamamlanma süresi anormal derecede uzarsa, hemen alarm alırım. Bu, potansiyel tutarsızlık sorunlarını erkenden tespit etmeme yardımcı olur.
- Test Stratejileri: Sadece birim ve entegrasyon testleri değil, aynı zamanda “kaos mühendisliği” (chaos engineering) yaklaşımlarıyla sistemin tutarsızlık durumlarında nasıl davrandığını test ederim. Ağ gecikmeleri, mesaj kuyruğu kesintileri gibi senaryoları simüle ederek sistemin dayanıklılığını ölçerim.
Benzer bir senaryo, kullanıcı bir işlemi tamamladığında başka bir sistemdeki raporun güncellenmesinin gerektiği durumdur. Başlangıçta basit bir event gönderimiyle çözülse de, raporun güncellenmesi kimi zaman gözle görülür bir gecikmeyle gerçekleşir ve bu süre içinde kullanıcı raporu kontrol ettiğinde eski veriyi görür. Bu, “ne kadar eventual?” sorusunu tekrar gündeme getirir. Bir çözüm, raporlama sistemine de bir transaction_id göndermek ve raporun sadece o transaction_id ile en son güncellenmiş halini göstermesini sağlamaktır. Böylece kullanıcı, raporun güncel olmadığını bile bile eski veriyi değil, “veri güncelleniyor” mesajını görür. Bu, geliştiricinin sadece teknik bir sorunu çözmekle kalmayıp, aynı zamanda kullanıcı deneyimi ve iş beklentilerini de yönetmesi gerektiğinin güzel bir örneğidir.
Sonuç olarak, eventual consistency ile çalışmak, bana sistem tasarımında daha esnek ve gerçekçi olmayı öğretti. Her zaman tam tutarlılık peşinde koşmak yerine, ne zaman ve nerede eventual consistency’nin kabul edilebilir bir trade-off olduğunu anlamak, daha sağlam ve ölçeklenebilir sistemler inşa etmemi sağladı. Bu, aynı zamanda geliştirici olarak problem çözme kaslarımı da güçlendirdi.