Redis Cluster ile Yatay Ölçekleme ve Yüksek Erişilebilirlik

Prodüksiyonda Redis ile çalışmaya başladığınızda, tek node’luk bir kurulumun ne kadar hızlı yetersiz kalabileceğini görürsünüz. Bir e-ticaret platformunda saniyede 50.000 mesaj işleyen bir Pub/Sub sistemi kurduğumuzda, tek Redis instance’ının memory limiti ve CPU bottleneck’i neredeyse anında karşımıza çıktı. İşte o noktada Redis Cluster’a geçiş kaçınılmaz oldu. Bu yazıda gerçek bir prodüksiyon ortamında Redis Cluster’ı nasıl kuracağınızı, Pub/Sub ve mesaj kuyruğu senaryolarında nasıl kullanacağınızı ve yüksek erişilebilirlik için nelere dikkat etmeniz gerektiğini aktaracağım.

Redis Cluster Nedir ve Neden Gereklidir

Redis Cluster, verinizi otomatik olarak birden fazla node arasında dağıtan ve bazı node’lar erişilemez olsa bile çalışmaya devam edebilen bir yapıdır. Tek node Redis’ten farklı olarak:

  • Sharding: Veriler hash slot’larına göre node’lara dağıtılır. Toplamda 16384 hash slot vardır ve her master node bunların bir kısmını yönetir.
  • Replication: Her master’ın en az bir replica’sı olur, master çökerse replica otomatik olarak master’a terfi eder.
  • Automatic failover: Sentinel’e gerek kalmadan cluster kendi içinde failover yönetir.

Mesaj kuyruğu ve Pub/Sub senaryolarında bu yapı kritik önem taşır. Çünkü kuyruktaki bir mesajı kaybetmek, çoğu zaman para kaybı veya kullanıcı deneyimi bozulması anlamına gelir.

Cluster Kurulumu: Adım Adım

Altyapı Hazırlığı

Minimum production cluster için 6 node öneriyorum: 3 master + 3 replica. Test ortamı için aynı makinede farklı portlarda çalıştırabilirsiniz ancak prodüksiyonda kesinlikle farklı fiziksel makineler veya farklı availability zone’lardaki VM’ler kullanın.

# Her node için Redis konfigürasyonu (redis-7001.conf örneği)
port 7001
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 5000
appendonly yes
appendfilename "appendonly-7001.aof"
dir /var/lib/redis/7001
logfile /var/log/redis/redis-7001.log
bind 0.0.0.0
protected-mode no
maxmemory 4gb
maxmemory-policy allkeys-lru

Aynı konfigürasyonu 7002’den 7006’ya kadar olan portlar için de oluşturun. Sistematik bir şekilde yapmak için basit bir script yazabilirsiniz:

#!/bin/bash
# cluster-setup.sh
BASE_PORT=7001
NODE_COUNT=6

for i in $(seq 1 $NODE_COUNT); do
  PORT=$((BASE_PORT + i - 1))
  DIR="/var/lib/redis/$PORT"
  
  mkdir -p $DIR
  
  cat > /etc/redis/redis-$PORT.conf << EOF
port $PORT
cluster-enabled yes
cluster-config-file nodes-$PORT.conf
cluster-node-timeout 5000
appendonly yes
dir $DIR
logfile /var/log/redis/redis-$PORT.log
bind 0.0.0.0
protected-mode no
maxmemory 4gb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
EOF

  # Servisi başlat
  redis-server /etc/redis/redis-$PORT.conf --daemonize yes
  echo "Redis $PORT başlatıldı"
done

Cluster Oluşturma

Node’lar ayağa kalktıktan sonra cluster’ı kurmak için redis-cli kullanıyoruz:

# Redis 7.x ile gelen yeni yöntem
redis-cli --cluster create 
  192.168.1.10:7001 
  192.168.1.10:7002 
  192.168.1.10:7003 
  192.168.1.11:7001 
  192.168.1.11:7002 
  192.168.1.11:7003 
  --cluster-replicas 1

# Cluster durumunu kontrol et
redis-cli -p 7001 cluster info
redis-cli -p 7001 cluster nodes

Çıktıda cluster_state:ok görüyorsanız cluster hazır demektir. cluster_slots_assigned:16384 değeri de tüm hash slot’larının atandığını gösterir.

Redis Pub/Sub Cluster Ortamında Nasıl Çalışır

Burada önemli bir noktayı belirtmem gerekiyor: Redis Cluster’da Pub/Sub mesajları tüm node’lara broadcast edilir. Yani bir client 7001 node’una subscribe olup, başka bir client 7003 node’una publish yapsa bile mesaj iletilir. Bu davranış, key-based sharding’den farklıdır.

Ancak bu aynı zamanda bir limitation’dır. Cluster büyüdükçe her Pub/Sub mesajı tüm node’lar arasında dolaşır ve bu inter-node traffic oluşturur. Bunu yönetmek için channel naming convention önemlidir.

Pub/Sub İmplementasyonu

Python ile bir producer-consumer örneği yapalım:

# producer.py - Redis Cluster Pub/Sub Producer
import redis
from redis.cluster import RedisCluster
import json
import time

startup_nodes = [
    {"host": "192.168.1.10", "port": "7001"},
    {"host": "192.168.1.10", "port": "7002"},
    {"host": "192.168.1.11", "port": "7001"},
]

rc = RedisCluster(
    startup_nodes=startup_nodes,
    decode_responses=True,
    skip_full_coverage_check=True,
    max_connections=50,
    retry_on_timeout=True
)

def publish_order_event(order_id, event_type, payload):
    channel = f"orders:{event_type}"
    message = json.dumps({
        "order_id": order_id,
        "event_type": event_type,
        "payload": payload,
        "timestamp": time.time()
    })
    
    result = rc.publish(channel, message)
    print(f"Mesaj yayinlandi: {channel}, {result} subscriber aldı")
    return result

# Kullanım örneği
if __name__ == "__main__":
    while True:
        publish_order_event(
            order_id="ORD-12345",
            event_type="created",
            payload={"product_id": "P-001", "quantity": 2, "price": 299.99}
        )
        time.sleep(0.1)  # Saniyede 10 mesaj
# consumer.py - Redis Cluster Pub/Sub Consumer
from redis.cluster import RedisCluster
import json
import threading

startup_nodes = [
    {"host": "192.168.1.10", "port": "7001"},
    {"host": "192.168.1.10", "port": "7002"},
    {"host": "192.168.1.11", "port": "7001"},
]

def create_cluster_client():
    return RedisCluster(
        startup_nodes=startup_nodes,
        decode_responses=True,
        skip_full_coverage_check=True
    )

def order_processor(channel_pattern):
    rc = create_cluster_client()
    # Cluster'da pubsub için node_flag kullanımı
    pubsub = rc.pubsub()
    pubsub.psubscribe(channel_pattern)
    
    print(f"Dinleniyor: {channel_pattern}")
    
    for message in pubsub.listen():
        if message['type'] == 'pmessage':
            try:
                data = json.loads(message['data'])
                print(f"Siparis alindi: {data['order_id']} - {data['event_type']}")
                # İş mantığı burada
                process_order(data)
            except json.JSONDecodeError as e:
                print(f"JSON parse hatasi: {e}")
            except Exception as e:
                print(f"İslem hatasi: {e}")

def process_order(data):
    # Gerçek iş mantığı
    print(f"İşleniyor: {data['order_id']}")

if __name__ == "__main__":
    # Birden fazla consumer thread
    threads = []
    patterns = ["orders:created", "orders:updated", "orders:cancelled"]
    
    for pattern in patterns:
        t = threading.Thread(target=order_processor, args=(pattern,))
        t.daemon = True
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()

Redis Cluster ile Güvenilir Mesaj Kuyruğu

Pub/Sub’un at-most-once delivery garantisi var, yani subscriber offline iken gönderilen mesajlar kaybolur. Prodüksiyonda çoğu zaman at-least-once veya exactly-once garantisi istersiniz. Bunun için Redis Lists veya Redis Streams kullanmak gerekir.

Redis Streams ile Kalıcı Mesaj Kuyruğu

Redis Streams, Cluster ortamında da mükemmel çalışır ve Kafka benzeri consumer group desteği sunar:

# Stream oluştur ve mesaj ekle
redis-cli -p 7001 XADD orders:stream '*' 
  order_id "ORD-12345" 
  product_id "P-001" 
  quantity "2" 
  status "created"

# Consumer group oluştur
redis-cli -p 7001 XGROUP CREATE orders:stream order-processors $ MKSTREAM

# Mesajları oku
redis-cli -p 7001 XREADGROUP GROUP order-processors worker-1 
  COUNT 10 BLOCK 2000 STREAMS orders:stream >

Python ile tam bir consumer implementasyonu:

# stream_consumer.py - Production-ready Stream Consumer
from redis.cluster import RedisCluster
import json
import time
import signal
import sys

class OrderStreamConsumer:
    def __init__(self, startup_nodes, stream_name, group_name, consumer_name):
        self.rc = RedisCluster(
            startup_nodes=startup_nodes,
            decode_responses=True,
            skip_full_coverage_check=True,
            retry_on_timeout=True,
            max_connections=20
        )
        self.stream_name = stream_name
        self.group_name = group_name
        self.consumer_name = consumer_name
        self.running = True
        
        # Graceful shutdown için signal handler
        signal.signal(signal.SIGTERM, self._shutdown)
        signal.signal(signal.SIGINT, self._shutdown)
        
        self._ensure_group_exists()
    
    def _ensure_group_exists(self):
        try:
            self.rc.xgroup_create(
                self.stream_name, 
                self.group_name, 
                id='0',
                mkstream=True
            )
            print(f"Consumer group olusturuldu: {self.group_name}")
        except Exception as e:
            if "BUSYGROUP" in str(e):
                print(f"Consumer group zaten mevcut: {self.group_name}")
            else:
                raise
    
    def _process_pending(self):
        """Önce acknowledge edilmemiş mesajları işle"""
        pending = self.rc.xpending_range(
            self.stream_name, 
            self.group_name,
            min='-', max='+', count=100
        )
        
        if pending:
            print(f"{len(pending)} bekleyen mesaj bulundu, isleniyor...")
            ids = [p['message_id'] for p in pending]
            messages = self.rc.xclaim(
                self.stream_name, self.group_name,
                self.consumer_name, min_idle_time=30000,
                message_ids=ids
            )
            self._handle_messages(messages)
    
    def _handle_messages(self, messages):
        for message_id, fields in messages:
            try:
                print(f"İsleniyor: {message_id} - Order: {fields.get('order_id')}")
                # Asıl iş mantığı
                self._do_work(fields)
                # Başarıyla işlendiyse acknowledge et
                self.rc.xack(self.stream_name, self.group_name, message_id)
                print(f"Onaylandi: {message_id}")
            except Exception as e:
                print(f"İslem hatasi {message_id}: {e}")
                # Hata durumunda acknowledge etme, tekrar işlenecek
    
    def _do_work(self, fields):
        # Gerçek iş mantığı buraya
        time.sleep(0.01)  # Simüle edilmiş işlem süresi
    
    def _shutdown(self, signum, frame):
        print("Graceful shutdown başlatiliyor...")
        self.running = False
    
    def run(self):
        # Önce pending mesajları işle
        self._process_pending()
        
        print(f"Dinleniyor: {self.stream_name} / {self.group_name}")
        
        while self.running:
            try:
                messages = self.rc.xreadgroup(
                    groupname=self.group_name,
                    consumername=self.consumer_name,
                    streams={self.stream_name: '>'},
                    count=10,
                    block=2000  # 2 saniye bekle
                )
                
                if messages:
                    for stream, msg_list in messages:
                        self._handle_messages(msg_list)
                        
            except Exception as e:
                print(f"Okuma hatasi: {e}")
                time.sleep(1)
        
        print("Consumer durduruldu")

if __name__ == "__main__":
    startup_nodes = [
        {"host": "192.168.1.10", "port": "7001"},
        {"host": "192.168.1.11", "port": "7001"},
    ]
    
    consumer = OrderStreamConsumer(
        startup_nodes=startup_nodes,
        stream_name="orders:stream",
        group_name="order-processors",
        consumer_name=f"worker-{sys.argv[1] if len(sys.argv) > 1 else '1'}"
    )
    consumer.run()

Yüksek Erişilebilirlik: Failover Senaryoları

Cluster’ın en güçlü yanı otomatik failover’dır. Ama bunu test etmeden prodüksiyona almak büyük risk. Şu test senaryosunu mutlaka çalıştırın:

# Bir master node'u öldür ve failover'ı izle
redis-cli -p 7001 debug sleep 30  # Node'u 30 saniyeliğine askıya al

# Başka bir terminalde cluster durumunu izle
watch -n 1 'redis-cli -p 7002 cluster nodes | grep -E "master|slave"'

# Failover süresini ölç
# cluster-node-timeout değerinin yaklaşık 2 katı sürer

Cluster konfigürasyonunda dikkat etmeniz gereken parametreler:

  • cluster-node-timeout 5000: Node’un erişilemez sayılması için geçmesi gereken süre (ms). Çok düşük koyarsanız yanlış failover tetikler, çok yüksek koyarsanız kesinti uzar. Prodüksiyon için 5000-15000 arası önerim.
  • cluster-migration-barrier 1: Master’ın kaç replica’sı varken replica migration izin verilir.
  • cluster-require-full-coverage no: Bazı slot’lar erişilemez olsa bile cluster çalışmaya devam eder. E-ticaret gibi kritik sistemlerde yes yerine no kullanın.
  • replica-lazy-flush yes: Replica, master’dan yeniden sync olurken eski veriyi lazy olarak temizler, performansı artırır.

Monitoring ve Alerting

Cluster’ı kör uçmadan izlemek için Prometheus + Grafana kombinasyonu standarttır. redis_exporter ile metrikleri toplamak için:

# redis_exporter kurulumu
docker run -d 
  --name redis_exporter 
  -p 9121:9121 
  oliver006/redis_exporter 
  --redis.addr=redis://192.168.1.10:7001 
  --redis.addr=redis://192.168.1.10:7002 
  --redis.addr=redis://192.168.1.11:7001 
  --redis.cluster-master-only-metrics=true

# Kritik metrikler için alert kuralı (Prometheus alert rule)
cat > /etc/prometheus/redis_alerts.yml << 'EOF'
groups:
  - name: redis_cluster
    rules:
      - alert: RedisClusterNodeDown
        expr: redis_cluster_stats_messages_sent_total == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Redis Cluster node erişilemez"
          
      - alert: RedisMemoryUsageHigh
        expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Redis memory kullanimi %85 uzerinde"
          
      - alert: RedisStreamLag
        expr: redis_stream_length > 10000
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Redis stream consumer lag yüksek"
EOF

Cluster’a Node Ekleme ve Çıkarma

Prodüksiyonda cluster’ı scale etmek kaçınılmaz. Doğru yapılmazsa veri kaybı yaşanabilir:

# Yeni master node ekle
redis-cli --cluster add-node 
  192.168.1.12:7001 
  192.168.1.10:7001

# Yeni node'a hash slot'larını taşı
# Bu işlem online yapılabilir, servis kesintisi olmaz
redis-cli --cluster reshard 192.168.1.10:7001 
  --cluster-from <kaynak-node-id> 
  --cluster-to <hedef-node-id> 
  --cluster-slots 4096 
  --cluster-yes

# Node'u cluster'dan güvenli şekilde çıkar
# Önce slot'ları taşı, sonra node'u kaldır
redis-cli --cluster del-node 
  192.168.1.10:7001 
  <cikarilacak-node-id>

# Cluster sağlık kontrolü
redis-cli --cluster check 192.168.1.10:7001

Resharding sırasında --cluster-pipeline parametresini kullanarak taşıma hızını artırabilirsiniz:

redis-cli --cluster reshard 192.168.1.10:7001 
  --cluster-pipeline 25 
  --cluster-from all 
  --cluster-to <hedef-node-id> 
  --cluster-slots 2000 
  --cluster-yes

Sık Karşılaşılan Sorunlar ve Çözümleri

MOVED ve ASK hataları: Cluster’da key yanlış node’a gönderildiğinde alırsınız. İyi bir Redis client bu yönlendirmeleri otomatik yapar, ancak eski client versiyonları ya da cluster-aware olmayan clientlar bu hataları uygulamaya fırlatır. Her zaman cluster-aware client kullanın.

Hot spot problemi: Bir hash tag kullandığınızda {user:1234}:orders gibi, tüm bu key’ler aynı slot’a düşer. Çok fazla key aynı slot’ta birikirse o node üzerinde yük yoğunlaşır. Hash tag’leri bilinçli kullanın.

Memory dengesizliği: Bazı node’lar diğerlerinden çok daha fazla memory kullanıyorsa resharding gerekebilir. redis-cli --cluster rebalance komutu bunu otomatik yapar:

# Cluster'ı otomatik dengele
redis-cli --cluster rebalance 192.168.1.10:7001 
  --cluster-use-empty-masters 
  --cluster-threshold 5

Sonuç

Redis Cluster, doğru kurulduğunda ve doğru araçlarla izlendiğinde gerçekten güçlü bir çözüm. Pub/Sub senaryolarında cluster-wide broadcast’in inter-node traffic yarattığını, bu nedenle yüksek frekanslı mesajlar için Redis Streams’in daha uygun olduğunu başta söylesem daha az yol kaybederdiniz. Streams ile at-least-once garantisi, consumer group’lar ve backpressure yönetimi mümkün. Pub/Sub ise düşük gecikme gerektiren, kayıp tolere edilebilen bildirim senaryoları için idealdir.

Cluster kurulumunu test ortamında yeteri kadar yorun: failover test edin, resharding deneyin, node’ları öldürüp uygulamanızın nasıl davrandığını gözlemleyin. Prodüksiyonda sürprizle karşılaşmamak için en iyi yol budur. Monitoring olmadan cluster kurmak ise araba kullanırken gözleri kapatmaya benzer. redis_exporter minimal eforla ciddi görünürlük sağlar, bunu ihmal etmeyin.

Son olarak: cluster-node-timeout değerini iş gereksinimlerinize göre ayarlayın, cluster-require-full-coverage no ile kısmi erişilemezlik durumunda bile servisin ayakta kalmasını sağlayın ve her zaman en az 3 master + 3 replica ile başlayın. İki master ile kurulan cluster’larda quorum sağlanamadığı için tek node çöküşünde bile tüm cluster erişilemez hale gelebilir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir