Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

Bir sistemi tek sunucudan çıkarıp birden fazla bölgeye, veritabanına veya servise yaydığında ilk kazancın ölçeklenebilirlik olur. Ama sen de fark etmişsindir ki veri çoğaldıkça başka bir sorun doğar: aynı bilginin her yerde aynı kalması. Bu yazıda consistency kavramını, neden zorlaştığını, leader-follower yaklaşımını ve two phase commit protokolünü sade bir akışla ele alacağız.
Bir sistemi tek sunucudan çıkarıp birden fazla bölgeye, veritabanına veya servise yaydığında ilk kazancın ölçeklenebilirlik olur. Ama sen de fark etmişsindir ki veri çoğaldıkça başka bir sorun doğar: aynı bilginin her yerde aynı kalması. Bu yazıda consistency kavramını, neden zorlaştığını, leader-follower yaklaşımını ve two phase commit protokolünü sade bir akışla ele alacağız.
Consistency, yani tutarlılık. Yani şöyle düşün: aynı verinin birden fazla kopyası varsa, bu kopyaların birbirleriyle çelişmemesi gerekir.
Örneğin küresel çalışan bir alışveriş uygulamasında kullanıcının adres bilgisi hem Avrupa’daki hem Amerika’daki veri merkezinde tutuluyor olabilir. Kullanıcı adresini değiştirdiğinde sadece bir bölgede yeni adres, diğer bölgede eski adres görünüyorsa sistem tutarsız hale gelir.
flowchart LR
U["Kullanıcı"] --> A["ABD Veri Merkezi"]
U --> E["Avrupa Veri Merkezi"]
A --> AData["Adres: Yeni adres"]
E --> EData["Adres: Eski adres"]
AData -.-> Problem["Tutarsız veri"]
EData -.-> ProblemBu diyagram aynı kullanıcının verisinin iki farklı bölgede farklı değerler taşıdığı durumu gösteriyor. Consistency problemi tam olarak burada başlar: sistem hızlı ve erişilebilir olabilir, ama okunan veri her zaman doğru olmayabilir.
İlk aşamada her şey tek sunucuda tutulduğunda consistency kolaydır. Çünkü verinin yalnızca bir kopyası vardır. Okuma da yazma da aynı yere gider.
flowchart TD
C1["Kullanıcı 1"] --> S["Tek Sunucu"]
C2["Kullanıcı 2"] --> S
C3["Kullanıcı 3"] --> S
S --> DB[("Tek Veri Kopyası")]Bu yapı basittir ve tutarlılık açısından rahattır. Ama tek sunucu hem arıza noktasıdır hem de büyüyen trafiği sonsuza kadar taşıyamaz.
Single point of failure, yani tek hata noktası. Bunu şöyle hayal edebilirsin: sistemin bütün yükünü taşıyan tek bir kolon var; o kolon çökerse bina da çöker. Sunucu kapanırsa sistem gider, disk bozulursa veri riske girer, ağ koparsa herkes etkilenir.
Vertical scaling, yani dikey ölçekleme. Yani şöyle düşün: aynı makineye daha fazla CPU, RAM veya disk ekleyerek güçlendirmeye çalışırsın. Bir yere kadar işe yarar, ama hem pahalılaşır hem de fiziksel bir sınıra dayanır.
Bir sonraki adım genelde veriyi bölgelere ayırmaktır. Avrupa kullanıcıları Avrupa sunucusuna, Amerika kullanıcıları Amerika sunucusuna gider. Bu latency sorununu azaltır ve yükü dağıtır.
Latency, yani gecikme. Bunu şöyle hayal edebilirsin: isteğin fiziksel olarak uzak bir veri merkezine gidip dönmesi zaman alır; ağ ışık hızına yakın çalışsa bile mesafe hâlâ gerçektir.
flowchart LR
subgraph US["Amerika Bölgesi"]
U1["Amerika Kullanıcıları"] --> USServer["ABD Sunucusu"]
USServer --> USDB[("ABD Kullanıcı Verisi")]
end
subgraph EU["Avrupa Bölgesi"]
U2["Avrupa Kullanıcıları"] --> EUServer["Avrupa Sunucusu"]
EUServer --> EUDB[("Avrupa Kullanıcı Verisi")]
endBu model yerel erişimi hızlandırır. Fakat bölgeler birbirinin verisine ihtiyaç duymaya başladığında problem geri gelir: uzak bölgeye gitmek hâlâ yavaştır ve her bölge kendi verisi için ayrı bir hata noktası olabilir.
Cache, yani önbellek. Yani şöyle düşün: sık kullanılan veriyi ana kaynağa tekrar tekrar sormak yerine yakındaki hızlı bir yerde tutarsın.
Bir kullanıcı Avrupa’daki bir ürünü Amerika’dan sık sık görüntülüyorsa, bu ürün bilgisi Amerika bölgesinde cache’lenebilir. Böylece sonraki okumalar daha hızlı olur.
sequenceDiagram
participant User as Kullanıcı
participant Local as Yerel Sunucu
participant Cache as Cache
participant Remote as Uzak Sunucu
User->>Local: Ürün bilgisini iste
Local->>Cache: Cache'te var mı?
Cache-->>Local: Yok
Local->>Remote: Ana kaynaktan getir
Remote-->>Local: Ürün bilgisi
Local->>Cache: Cache'e yaz
Local-->>User: Yanıt dönBu akış cache miss durumunu gösteriyor. İlk istek uzak sisteme gider; sonraki istekler cache üzerinden daha hızlı cevaplanabilir.
Ama cache tutarlılık problemini çözmez, sadece okuma maliyetini azaltır. Ana veri değiştiğinde cache eski kalabilir. Bu yüzden cache kullanılan sistemlerde “veri ne kadar süre eski kalabilir?” sorusu çok önemlidir.
Replication, yani veriyi çoğaltma. Bunu şöyle hayal edebilirsin: aynı belgeyi farklı şehirlerdeki kasalara koyuyorsun. Bir kasa erişilemez olursa diğerinden devam edebilirsin.
flowchart LR
subgraph US["ABD"]
A[("Kullanıcı A")]
B[("Kullanıcı B")]
C1[("Kullanıcı C: v2")]
end
subgraph EU["Avrupa"]
A2[("Kullanıcı A")]
B2[("Kullanıcı B")]
C2[("Kullanıcı C: v1")]
end
C1 -. "Güncelleme gönderilmeli" .-> C2Replikasyon arıza dayanıklılığını ve yerel okuma hızını artırır. Ancak veri güncellendiği anda bütün kopyalar aynı anda değişmiyorsa consistency yeniden ana problem haline gelir.
Buradaki zor soru şudur: bir bölgede yapılan değişiklik diğer bölgelere ne zaman ve nasıl ulaştırılacak?
Acknowledgement, yani onay mesajı. Yani şöyle düşün: bir sisteme “bu veriyi güncelle” dersin, o da “tamam, yaptım” diye cevap verir.
İlk bakışta bu yeterli gibi görünür. Değişikliği gönder, karşı taraf uygulasın, sonra onay dönsün. Ama ağ güvenilmezdir. Mesaj kaybolabilir, onay kaybolabilir veya karşı taraf değişikliği yaptıktan hemen sonra erişilemez hale gelebilir.
sequenceDiagram
participant A as Sunucu A
participant B as Sunucu B
A->>B: Veriyi v1'den v2'ye güncelle
B->>B: Güncellemeyi uygula
B--xA: ACK kaybolur
A->>B: Tekrar deneBu diyagramda B değişikliği yapmış olabilir, ama A bunu kesin olarak bilemez. A tekrar denerse aynı işlemin yan etkisiz şekilde tekrar uygulanabilmesi gerekir.
Idempotency, yani aynı işlemi tekrar çalıştırınca sonucu bozmama özelliği. Bunu şöyle hayal edebilirsin: “sipariş durumunu kargoda yap” işlemi beş kez çalışsa da sonuç yine “kargoda” kalır; beş ayrı kargo oluşturmaz.
Tutarlılığı yönetmenin yaygın yollarından biri yazma sorumluluğunu tek bir lidere vermektir. Leader, yani lider düğüm. Yani şöyle düşün: sistemde yazı yazma yetkisi tek bir merkezde toplanır; diğerleri bu merkezden gelen değişiklikleri takip eder.
flowchart LR
Client["İstemci"] --> Leader["Leader"]
Leader --> LeaderDB[("Ana Veri")]
Leader ==>|"Değişiklikleri yayar"| F1["Follower 1"]
Leader ==>|"Değişiklikleri yayar"| F2["Follower 2"]
F1 --> F1DB[("Kopya Veri")]
F2 --> F2DB[("Kopya Veri")]Bu model sistemi sadeleştirir çünkü yazma çatışmaları azalır. Ancak follower güncellemeyi henüz almadıysa oradan yapılan okuma eski veri döndürebilir.
Burada tercih yapmak zorunda kalırsın. Ya follower güncel hale gelene kadar okumaları bekletirsin, bu availability kaybı yaratır. Ya da okumaya izin verirsin, bu kez stale read yani eski veri okuma ihtimalini kabul edersin.
Availability, yani erişilebilirlik. Bunu şöyle hayal edebilirsin: sistem ayakta ve cevap verebilir durumda mı? Cevap veriyor ama eski veri döndürüyorsa erişilebilir olabilir, fakat tamamen tutarlı değildir.
Dağıtık sistemlerde consistency ile availability çoğu zaman aynı anda maksimum seviyede tutulamaz. Özellikle ağ bölünmeleri, mesaj kayıpları ve sunucu arızaları devreye girdiğinde karar vermek gerekir.
flowchart TD
Start["Veri güncellemesi geldi"] --> Need["Tüm kopyalar aynı anda güncel olmalı mı?"]
Need -->|Evet| Block["Okuma/yazma işlemlerini beklet"]
Block --> Consistent["Daha güçlü consistency"]
Block --> LessAvailable["Daha düşük availability"]
Need -->|Hayır| Serve["Yanıt vermeye devam et"]
Serve --> Available["Daha yüksek availability"]
Serve --> Inconsistent["Geçici tutarsızlık riski"]Bu diyagram temel dengeyi gösteriyor. Sistem tutarlılığı sıkılaştırdıkça bekleme, kilitleme ve gecikme artabilir; erişilebilirliği artırdıkça geçici tutarsızlıkları kabul etmen gerekebilir.
Two phase commit, yani iki aşamalı commit protokolü. Yani şöyle düşün: lider önce herkese “bu işlemi yapmaya hazır mısınız?” diye sorar, herkes hazırsa ikinci aşamada “tamam, şimdi kesin olarak kaydedin” der.
Bu protokol özellikle birden fazla sistemin aynı transaction içinde birlikte hareket etmesi gerektiğinde kullanılır.
Transaction, yani işlem bütünlüğü. Bunu şöyle hayal edebilirsin: para transferinde bir hesaptan düşüp diğer hesaba ekleme tek bir mantıksal işlem gibi davranmalıdır; yarısı olup yarısı olmamalıdır.
sequenceDiagram
participant C as Coordinator
participant S1 as Servis 1
participant S2 as Servis 2
participant S3 as Servis 3
C->>S1: Prepare
C->>S2: Prepare
C->>S3: Prepare
S1-->>C: Hazırım
S2-->>C: Hazırım
S3-->>C: Hazırım
C->>C: Commit kararı al
C->>S1: Commit
C->>S2: Commit
C->>S3: Commit
S1-->>C: Commit ACK
S2-->>C: Commit ACK
S3-->>C: Commit ACKBu diyagram two phase commit’in mutlu yolunu gösteriyor. İlk aşama hazırlık, ikinci aşama kesin kayıttır.
Prepare aşamasında servisler işlemi uygular gibi hazırlanır ama dış dünyaya kesin sonucu göstermeyebilir. Commit aşamasında ise işlem kalıcı hale gelir.
Eğer prepare aşamasında servislerden biri hazır olmadığını söylerse veya zamanında cevap vermezse coordinator işlemi iptal eder. Rollback, yani geri alma. Bunu şöyle hayal edebilirsin: işlem henüz kesinleşmeden önce yapılan geçici değişiklikler silinir ve veri eski haline döner.
sequenceDiagram
participant C as Coordinator
participant S1 as Servis 1
participant S2 as Servis 2
C->>S1: Prepare
C->>S2: Prepare
S1-->>C: Hazırım
S2--xC: Cevap yok
C->>S1: Rollback
C->>S2: Rollback
S1-->>C: Rollback ACKBu akışta coordinator yeterli onayı alamadığı için işlemi iptal eder. Böylece bazı servislerin işlemi yapıp bazılarının yapmaması engellenmeye çalışılır.
Asıl zor kısım commit aşamasından sonra başlar. Coordinator commit kararı aldıysa artık bazı servisler commit etmiş, bazıları commit mesajını henüz almamış olabilir. Bu yüzden sistem transaction id kullanır ve commit mesajlarını tekrar gönderebilir.
flowchart TD
A["Coordinator commit kararı aldı"] --> B["Commit mesajı gönderilir"]
B --> C{"ACK geldi mi?"}
C -->|Evet| D["Bu katılımcı tamamlandı"]
C -->|Hayır| E["Aynı transaction id ile tekrar gönder"]
E --> B
D --> F["Tüm katılımcılar tamamlandı mı?"]
F -->|Hayır| B
F -->|Evet| G["Transaction tamamlandı"]Bu diyagram commit tekrarlarının neden güvenli olması gerektiğini gösteriyor. Aynı transaction id ile gelen tekrar commit mesajı yeni bir işlem yaratmamalı, var olan işlemin devamı olarak görülmelidir.
Two phase commit güçlü consistency sağlar, ama bunu bedelsiz yapmaz. İşlem tamamlanana kadar ilgili kayıtlar kilitlenebilir. Lock, yani kilit. Bunu şöyle hayal edebilirsin: bir belge üzerinde kritik bir düzenleme yapılırken başka kimsenin aynı belgeyi okuyup yanlış ara durumu görmesine izin verilmez.
stateDiagram-v2
[*] --> Idle
Idle --> Prepared: Prepare alındı
Prepared --> Locked: Kayıt kilitlendi
Locked --> Committed: Commit alındı
Locked --> RolledBack: Rollback alındı
Committed --> [*]
RolledBack --> [*]Bu durum diyagramı bir katılımcının transaction boyunca hangi aşamalardan geçtiğini gösteriyor. Prepared ve Locked durumlarında sistem tutarlılığı korur, ama ilgili veri için bekleme süresi oluşabilir.
Bu yüzden strong consistency, yani güçlü tutarlılık her sistem için doğru tercih değildir. Bankacılık, ödeme, stok azaltma veya rezervasyon gibi alanlarda çok önemlidir. Ama sosyal medya beğeni sayısı, öneri listesi veya analiz paneli gibi alanlarda kısa süreli tutarsızlık kabul edilebilir.
Eventual consistency, yani nihai tutarlılık. Yani şöyle düşün: sistem kısa süreliğine farklı kopyalarda farklı veriler gösterebilir, ama yeni güncelleme gelmezse zaman içinde bütün kopyalar aynı hale gelir.
Bu yaklaşım yüksek availability ve performans isteyen sistemlerde sık görülür. Her okumanın en güncel veriyi döndürmesi zorunlu değilse, sistem daha hızlı ve dayanıklı çalışabilir.
flowchart LR
W["Yazma işlemi"] --> P["Birincil kopya güncellendi"]
P --> R1["Kopya 1 eski olabilir"]
P --> R2["Kopya 2 eski olabilir"]
P -. "Arka planda yayılım" .-> R1
P -. "Arka planda yayılım" .-> R2
R1 --> Final["Zamanla aynı veriye yaklaşırlar"]
R2 --> FinalBu diyagram eventual consistency yaklaşımını gösteriyor. Veri ilk anda her yerde aynı olmayabilir, ama sistem arka planda kopyaları güncelleyerek zamanla tutarlı hale gelir.
Dağıtık sistemlerde consistency, yalnızca “veri doğru mu?” sorusu değildir; aynı zamanda “bu doğruluğu hangi gecikme, maliyet ve erişilebilirlik bedeliyle sağlıyoruz?” sorusudur. Tek sunucuda tutarlılık kolaydır ama ölçek, arıza dayanıklılığı ve latency sorunları büyür. Veriyi çoğaltınca sistem daha dayanıklı hale gelir, fakat bu kez kopyaların aynı kalması zorlaşır. Leader-follower modeli yazmaları sadeleştirir, two phase commit güçlü tutarlılık sağlar, eventual consistency ise daha yüksek erişilebilirlik için geçici farkları kabul eder. İyi sistem tasarımı, her veri türü için doğru tutarlılık seviyesini seçebilmektir.
Bu yazı Data Consistency and Tradeoffs in Distributed Systems videosundan ilham alınarak yazılmıştır.