hkucuk

CAP Teoremi: Dağıtık Sistemlerde Temel Kısıtlamalar

6 Nisan 2025 • ☕️☕️ 8 dk okuma • 🏷 bilgisayar, yazılım, algoritma

Yazar tarafından şu dillere çevrildi: English


CAP teoremi, dağıtık sistem tasarımındaki en etkili prensiplerden biri olarak, mühendislerin dayanıklı ve ölçeklenebilir uygulamalar oluştururken karşılaştıkları temel kısıtlamaları ifade eder. İlk olarak Eric Brewer tarafından 2000 yılında öne sürülen ve daha sonra Seth Gilbert ve Nancy Lynch tarafından 2002 yılında matematiksel olarak kanıtlanan [1] bu teorem, dağıtık veri sistemlerinin aynı anda şu üç garantiden en fazla ikisini sağlayabileceğini belirtir: Tutarlılık (Consistency), Erişilebilirlik (Availability) ve Bölünme Toleransı (Partition tolerance).

Bu makale, CAP teoreminin teorik temellerini, pratik etkilerini ve modern dağıtık sistemlerin bu çerçevede nasıl stratejik ödünleşmeler yaptığını incelemektedir. Dağıtık sistemlerle çalışan profesyonel mühendisler için, bu kısıtlamaları anlamak, iş gereksinimlerine ve teknik gerçeklere uygun bilinçli mimari kararlar almak için esastır.

CAP Teoremi


Üç Garantiyi Anlamak

Tutarlılık (Consistency - C)

Tutarlılık, dağıtık bir sistemdeki tüm düğümlerin aynı zamanda aynı verileri görmesini sağlar. Kesin tutarlı sistemlerde, herhangi bir okuma işlemi, hangi düğümün isteği aldığına bakılmaksızın en son yazma işleminin değerini döndürür. Bu özellik, veriler yazıldıktan ve onaylandıktan sonra, tüm sonraki okumaların bu yazma işlemini yansıtacağını garanti eder.

Resmi olarak, tutarlılık doğrusallık (linearizability) anlamına gelir - işlemler, operasyonların gerçek zamanlı sıralamasına uyan sıralı bir düzende yürütülüyor gibi görünür [2].

Erişilebilirlik (Availability - A)

Erişilebilirlik, arızalı olmayan her düğüme yapılan her isteğin, yanıtın en güncel verileri içerdiği garantisi olmadan bir yanıt aldığı anlamına gelir. Erişilebilir bir sistem, bazı bileşenler başarısız olsa bile hizmetin çalışır durumda kalmasını sağlar ve tüm istemci isteklerine yanıt verir.

Matematiksel olarak, erişilebilirlik, her isteğin sonunda bir yanıt alacağı anlamına gelir (canlılık özelliği), ancak yanıt en son durumu yansıtmayabilir [3].

Bölünme Toleransı (Partition Tolerance - P)

Bölünme toleransı, bir sistemin ağ bölünmeleri - bazı düğümlerin diğerleriyle iletişim kurmasını engelleyen ağ arızaları - olmasına rağmen çalışmaya devam etme yeteneğini ifade eder. Bölünmüş bir ağda, bir bölümdeki düğümlerden gönderilen mesajlar diğer bölümlerdeki düğümlere ulaşamaz.

Bölünme toleransı, gerçek dünya dağıtık sistemlerinde isteğe bağlı değildir; ağ arızaları kaçınılmaz olarak meydana gelecektir. Gilbert ve Lynch’in kanıtı, bölünme toleransı gerektiriyorsak, tutarlılık ve erişilebilirlik arasında seçim yapmamız gerektiğini gösteriyor [1].

Teorem: Üçünden İkisini Seçin

CAP teoreminin temel içgörüsü, bir ağ bölünmesi meydana geldiğinde, dağıtık bir sistemin ya tutarlılıktan ya da erişilebilirlikten vazgeçmesi gerektiğidir:

1. CP Sistemleri (Tutarlı ve Bölünme Toleranslı): Bu sistemler, erişilebilirliğe göre tutarlılığa öncelik verir. Bir bölünme meydana geldiğinde, tutarlılığı korumak için bazı düğümler erişilemez hale gelir.

2. AP Sistemleri (Erişilebilir ve Bölünme Toleranslı): Bu sistemler, tutarlılığa göre erişilebilirliğe öncelik verir. Bir bölünme sırasında, tüm düğümler erişilebilir kalır, ancak eski veya tutarsız veriler döndürebilir.

3. CA Sistemleri (Tutarlı ve Erişilebilir): Bu sistemler bölünmelere tolerans gösteremez. Pratikte, gerçek CA sistemleri yalnızca dağıtık olmayan ortamlarda var olabilir, çünkü dağıtık ortamlarda ağ bölünmeleri kaçınılmazdır.

Matematiksel Kanıt Sezgisi

Gilbert ve Lynch’in matematiksel kanıtı [1], tutarlılık, erişilebilirlik ve bölünme toleransını aynı anda elde etmenin imkansızlığını basit bir senaryo kullanarak gösterir: İki düğümlü (A ve B) ve v değeriyle tutarlı bir durumda başlayan dağıtık bir sistem düşünün. Bir ağ bölünmesi meydana gelir ve A’yı B’den izole eder. Bir istemci, A düğümüne yeni bir v’ değeri yazar. Ardından, farklı bir istemci B düğümünden okur.

Bu senaryoda:

  • Sistem tutarlılığa öncelik veriyorsa, B düğümü bir değer döndüremez (erişilemez) çünkü en son durumu doğrulayamaz.
  • Sistem erişilebilirliğe öncelik veriyorsa, B düğümü A düğümündaki güncellenmiş v’ değeriyle tutarsız olan v değerini döndürmelidir.

Bu, bir bölünme sırasında çözülemez bir ikilem yaratır: ya yanıt vermeyi reddederek erişilebilirlikten vazgeçin, ya da potansiyel olarak eski veri döndürerek tutarlılıktan vazgeçin.


Modern Sistemlerde Pratik Örnekler

CP Sistem Örneği: Güçlü Tutarlılık ile Redis Cluster

Redis Cluster, aşağıdaki yapılandırma kullanılarak tutarlılığa erişilebilirlikten daha fazla öncelik verecek şekilde yapılandırılabilir:

import (
    "context"
    "github.com/go-redis/redis/v8"
)

func setupConsistentRedisCluster() *redis.ClusterClient {
    rdb := redis.NewClusterClient(&redis.ClusterOptions{
        Addrs:    []string{":7000", ":7001", ":7002"},
        // Require acknowledgment from majority of nodes
        ReadOnly: false,
        // Wait for data to be replicated to majority of nodes
        RouteByLatency: false,
    })
    
    // Test connection
    ctx := context.Background()
    if err := rdb.Ping(ctx).Err(); err != nil {
        panic(err)
    }
    
    return rdb
}

Bu yapılandırmada, bir ağ bölünmesi meydana geldiğinde, yalnızca düğümlerin çoğunluğunu içeren bölüm çalışır durumda kalır. Azınlık bölümü erişilemez hale gelir, tutarlılığı korumak için erişilebilirlikten fedakarlık yapar.

AP Sistem Örneği: Nihai Olarak Tutarlı Okumalarla DynamoDB

Amazon DynamoDB, erişilebilirliğe anında tutarlılık yerine öncelik veren, nihai olarak tutarlı okumalar için bir seçenek sunar:

const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();

// Eventually consistent read (default)
async function fetchDataWithEventualConsistency(key) {
    const params = {
        TableName: 'MyTable',
        Key: { 'id': key },
        // ConsistentRead: false is the default (eventually consistent)
    };
    
    try {
        const result = await dynamoDB.get(params).promise();
        return result.Item;
    } catch (error) {
        console.error("Error fetching data:", error);
        throw error;
    }
}

Bu yapılandırmayla DynamoDB, bölünmeler sırasında okuma hizmeti vermeye devam edecek ancak bölünme iyileşip çoğaltma yetişene kadar eski veriler döndürebilir.

CRDT Tabanlı Sistemler: CAP Ödünleşmeleri Arasında Gezinme

Çatışmasız Çoğaltılabilir Veri Türleri (Conflict-free Replicated Data Types - CRDTs), CAP ödünleşmelerini ele almak için yenilikçi bir yaklaşım sunar:

// Simplified example of a G-Counter (Grow-only Counter) CRDT in Go
type GCounter struct {
    Counts map[string]int // Node ID -> Count
    NodeID string
}

func NewGCounter(nodeID string) *GCounter {
    return &GCounter{
        Counts: make(map[string]int),
        NodeID: nodeID,
    }
}

func (c *GCounter) Increment() {
    c.Counts[c.NodeID]++
}

func (c *GCounter) Value() int {
    total := 0
    for _, count := range c.Counts {
        total += count
    }
    return total
}

// Merge function for eventual consistency
func (c *GCounter) Merge(other *GCounter) {
    for nodeID, count := range other.Counts {
        if existingCount, exists := c.Counts[nodeID]; !exists || count > existingCount {
            c.Counts[nodeID] = count
        }
    }
}

Bu G-Counter gibi CRDT’ler, bölünmeler iyileştiğinde çatışmaları otomatik olarak çözmek için tasarlanmış veri yapıları kullanarak, bölünmeler sırasında erişilebilirliği korurken nihai tutarlılık sağlar [4].


Sistem Tasarımı için Pratik Hususlar

İş Gereksinimlerini Analiz Etme

Dağıtık sistemler tasarlarken, öncelikle spesifik gereksinimlerinizi analiz edin:

1. Veri Tutarlılığı Gereksinimleri: Uygulamanızın güçlü tutarlılık (örn. finansal işlemler) gerektirip gerektirmediğini veya nihai tutarlılığı (örn. sosyal medya beğenileri) tolere edip edemeyeceğini belirleyin.

2. Erişilebilirlik Gereksinimleri: Uygulamanız için kabul edilebilir kesinti süresini değerlendirin. Kritik öneme sahip sistemler, anında tutarlılık yerine erişilebilirliğe öncelik verebilir.

3. Ağ Ortamı: Altyapınızda ağ bölünmelerinin olasılığını ve sıklığını dikkate alın.

Pratik Ödünleşmeleri Uygulama

Modern sistemler genellikle ağ koşullarına göre uyarlanan nüanslı yaklaşımlar uygular:

Ayarlanabilir Tutarlılık

Birçok sistem, mühendislerin işlem türlerine göre tutarlılık ve erişilebilirlik arasında denge kurmasına olanak tanıyan ayarlanabilir tutarlılık seviyeleri sunar:

// MongoDB example with different write and read concerns
const { MongoClient } = require('mongodb');

async function performOperationsWithDifferentConsistencyLevels() {
    const client = new MongoClient('mongodb://localhost:27017');
    await client.connect();
    const db = client.db('mydb');
    const collection = db.collection('documents');
    
    // Write with strong consistency (majority write concern)
    await collection.insertOne(
        { item: 'example', value: 42 },
        { writeConcern: { w: 'majority', wtimeout: 5000 } }
    );
    
    // Read with eventual consistency (primary read preference)
    const result = await collection.findOne(
        { item: 'example' },
        { readPreference: 'primary' }
    );
    
    await client.close();
    return result;
}

Devre Kesiciler ve Yedek Planlar

Bölünmeleri tespit etmek ve davranışı buna göre ayarlamak için devre kesiciler uygulayın:

package main

import (
    "context"
    "errors"
    "time"
    
    "github.com/sony/gobreaker"
)

type DataStore struct {
    ConsistentStore StronglyConsistentDB
    AvailableStore EventuallyConsistentCache
    circuitBreaker *gobreaker.CircuitBreaker
}

func NewDataStore() *DataStore {
    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        "database-circuit",
        MaxRequests: 5,
        Timeout:     10 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 10 && failureRatio >= 0.5
        },
    })
    
    return &DataStore{
        // initialize stores...
        circuitBreaker: cb,
    }
}

func (ds *DataStore) GetData(ctx context.Context, key string) (interface{}, error) {
    // Try strongly consistent store with circuit breaker
    result, err := ds.circuitBreaker.Execute(func() (interface{}, error) {
        return ds.ConsistentStore.Get(ctx, key)
    })
    
    if err != nil {
        // Circuit open or consistent store failed
        // Fall back to eventually consistent store
        return ds.AvailableStore.Get(ctx, key)
    }
    
    return result, nil
}

Bu model, sistemlerin mevcut ağ koşullarına bağlı olarak CP ve AP modları arasında dinamik olarak geçiş yapmasına olanak tanır.


CAP Teoreminin Ötesinde

PACELC Uzantısı

PACELC teoremi, hem bölünmeler sırasında hem de normal çalışma durumunda sistem davranışını ele alarak CAP’i genişletir [5]:

  • PA/EL: Bir Bölünme meydana gelirse, Erişilebilirlik ve tutarlılık (Latency) arasında seçim yapın; Aksi takdirde (normal çalışmada), tutarlılık ve Latency arasında seçim yapın.

Bu uzantı, bölünmeler yokluğunda bile, dağıtık sistemlerin tutarlılık ve gecikme arasında temel bir ödünleşmeyle karşı karşıya olduğunu kabul eder.

Tutarlılık Modelleri Spektrumu

Tutarlılığı ikili olarak görmek yerine, modern sistemler bir spektrum boyunca çeşitli tutarlılık modelleri uygular:

1. Güçlü Tutarlılık: Doğrusallık, sıralı tutarlılık

2. Nedensel Tutarlılık: Nedensel olarak ilişkili işlemler tüm gözlemcilere aynı sırada görünür

3. Nihai Tutarlılık: Tüm çoğaltmalar sonunda aynı duruma yakınsar

// Redis example with different consistency models
const Redis = require('ioredis');

// Strong consistency with synchronous replication
const strongConsistentRedis = new Redis.Cluster([
    { host: 'redis-node1', port: 6379 },
    { host: 'redis-node2', port: 6379 },
], {
    scaleReads: 'master', // Only read from master node
    waitCommand: true     // Wait for commands to complete
});

// Eventual consistency with asynchronous replication
const eventuallyConsistentRedis = new Redis.Cluster([
    { host: 'redis-node1', port: 6379 },
    { host: 'redis-node2', port: 6379 },
], {
    scaleReads: 'all',    // Read from any node
    enableOfflineQueue: true
});

CAP teoremi, dağıtık sistem mühendislerinin kabul etmesi ve yönetmesi gereken temel kısıtlamalar sunar. Bu kısıtlamalar sınırlamalar olarak görülmek yerine, belirli uygulama gereksinimlerine dayalı bilinçli ödünleşmeler yapmak için bir çerçeve olarak anlaşılmalıdır.

Modern dağıtık sistemler giderek değişen ağ koşullarına ve veri erişim modellerine uyum sağlayan sofistike stratejiler uygulamaktadır. İş gereksinimlerini dikkatle analiz ederek ve CAP teoreminin teorik temellerini anlayarak, mühendisler spesifik kullanım durumları için tutarlılık, erişilebilirlik ve bölünme toleransı arasında optimal dengeyi sağlayan sistemler tasarlayabilirler.


Kaynaklar

[1] Gilbert, S., & Lynch, N. (2002). Brewer’s conjecture and the feasibility of consistent, available, partition-tolerant web services. ACM SIGACT News, 33(2), 51-59.

[2] Herlihy, M. P., & Wing, J. M. (1990). Linearizability: A correctness condition for concurrent objects. ACM Transactions on Programming Languages and Systems, 12(3), 463-492.

[3] Bailis, P., & Ghodsi, A. (2013). Eventual consistency today: Limitations, extensions, and beyond. Communications of the ACM, 56(5), 55-63.

[4] Shapiro, M., Preguiça, N., Baquero, C., & Zawirski, M. (2011). Conflict-free replicated data types. In Symposium on Self-Stabilizing Systems (pp. 386-400). Springer.

[5] Abadi, D. (2012). Consistency tradeoffs in modern distributed database system design: CAP is only part of the story. Computer, 45(2), 37-42.