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
yesyerinenokullanı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.
