RRedis Handbook

İLERİ

Connection Pooling & Performans

StackExchange.Redis varsayılan olarak tek TCP bağlantı üzerinden multiplexing yapar. Çoğu senaryo için yeterli. Ama çok yoğun senaryolarda (>50K ops/s) tek connection bottleneck olabilir.

Kod örneği görünümü Bu sayfadaki eşleşen örnekleri seçilen istemciye göre gösterir.

Ne Zaman Connection Pool Kullan / Kullanma

Kullan (Pool) Kullanma (Tek Multiplexer yeterli) Gerçek Hayat
>50K ops/s throughput, tek pipe doyuyor <50K ops/s — çoğu web app High-traffic API: 100K req/s → 2-4 multiplexer pool
Büyük value'lar (>10KB) sık transfer Küçük key/value (<1KB) operasyonları Media: Thumbnail cache — 50KB value'lar pipeline'ı tıkar
Uzun süren blocking komutlar (BLPOP) var Sadece async non-blocking komutlar Queue: BLPOP worker'ları ayrı connection kullanmalı
CPU-bound serialize/deserialize ağır Serialize hafif (küçük JSON) ML: Model result cache — 100KB JSON serialize → ayrı pipe

Gerçek hayat senaryosu — E-ticaret Black Friday: Normal günlerde tek multiplexer 15K ops/s taşıyor. Black Friday'de 80K ops/s'e çıkıyor → ThreadPool starvation + timeout spike. Çözüm: 4 multiplexer'lık pool → her biri 20K ops/s taşır, timeout kaybolur.

Threads Thread 1 Thread 2 Thread 3 Thread N Multiplexer Pipeline + Interleave 1 TCP connection TCP (single connection) Redis Single-threaded ~200K ops/s 4 thread × binlerce concurrent komut → TEK boru → sıralı işlem. Roundtrip = 1.
# Aktif bağlantıları gör
CLIENT LIST

# Bağlantı limiti
CONFIG GET maxclients            # varsayılan 10000
CONFIG SET maxclients 20000

# Bağlantı bazlı istatistik
INFO clients
# connected_clients:45
# blocked_clients:0
# tracking_clients:0

# Idle bağlantıları temizle
CONFIG SET timeout 300           # 5dk idle → disconnect
CLIENT NO-EVICT on               # bu client'ı evict etme
// === Yaklaşım 1: Tek ConnectionMultiplexer (çoğu uygulama için yeterli) ===
// Neden yeterli: Multiplexer tek TCP üzerinden binlerce concurrent komutu pipeline'lar.
// IDatabase/ISubscriber lightweight — GC pressure yok.
// 💡 SocketManager: Varsayılan 1 I/O thread. >100K ops/s'de tıkanma olursa:
//    config.SocketManager = new SocketManager("Redis", workerCount: Environment.ProcessorCount);
//    Ama önce Pool (Yaklaşım 2) dene — SocketManager nadir gereken optimizasyon.
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var config = ConfigurationOptions.Parse(
        builder.Configuration.GetConnectionString("Redis")!);
    config.AbortOnConnectFail = false;
    config.AsyncTimeout = 3000;
    config.SyncTimeout = 3000;
    return ConnectionMultiplexer.Connect(config);
});
// === Yaklaşım 2: Connection Pool (çok yoğun senaryolar — >50K ops/s) ===
//
// ⚠️ MALIYET ANALİZİ — her yeni ConnectionMultiplexer:
//   1. TCP 3-way handshake         → ~0.5ms (aynı DC), ~50-100ms (cross-region)
//   2. TLS handshake (ssl=true)    → +2-10ms (ECDHE key exchange, cert validation)
//   3. AUTH komutu                 → +0.1ms
//   4. CLIENT SETNAME              → +0.1ms
//   5. Toplam bağlantı maliyeti    → 3-110ms (ortama bağlı)
//   6. Server-side: file descriptor + ~10KB memory + keepalive timer
//   7. GC: ConnectionMultiplexer ~Gen2 object (~80KB) — Dispose edilmezse LOH leak
//
// Bu maliyet startup'ta BİR KERE ödenir (pool Singleton olduğu için).
// Ama pool boyutunu gereksiz büyütme — her connection = server resource.
//
public class RedisConnectionPool : IDisposable
{
    private readonly ConnectionMultiplexer[] _connections;
    private int _index;

    public RedisConnectionPool(string connectionString, int poolSize = 4)
    {
        // poolSize önerisi: Math.Min(Environment.ProcessorCount, 8)
        // 8'den fazla diminishing returns — Redis single-thread!
        _connections = new ConnectionMultiplexer[poolSize];
        var config = ConfigurationOptions.Parse(connectionString);
        config.AbortOnConnectFail = false;

        for (int i = 0; i < poolSize; i++)
        {
            _connections[i] = ConnectionMultiplexer.Connect(config);
        }
    }

    public IDatabase GetDatabase(int db = -1)
    {
        var idx = Interlocked.Increment(ref _index) % _connections.Length;
        return _connections[idx].GetDatabase(db);
    }

    public ISubscriber GetSubscriber()
    {
        var idx = Interlocked.Increment(ref _index) % _connections.Length;
        return _connections[idx].GetSubscriber();
    }

    public void Dispose()
    {
        // ConnectionMultiplexer IDisposable — uygulama kapanırken temizle
        foreach (var conn in _connections)
            conn?.Dispose();
    }
}

// DI kayıt (Singleton — startup'ta 1 kez pool oluştur)
builder.Services.AddSingleton<RedisConnectionPool>(sp =>
    new RedisConnectionPool(
        builder.Configuration.GetConnectionString("Redis")!,
        poolSize: Math.Min(Environment.ProcessorCount, 8)));
// ❌ ASLA YAPMA: Request/scope başına yeni multiplexer
// Her request'te handshake maliyeti + connection explosion + Gen2 GC baskısı
public class BadService
{
    public async Task DoWorkAsync()
    {
        // ❌ Her çağrıda ~3-110ms handshake + server resource leak
        using var mux = ConnectionMultiplexer.Connect("redis:6379");
        var db = mux.GetDatabase();
        await db.StringGetAsync("key");
    } // Dispose → TCP close, ama GC pressure zaten oluştu
}
// === Performans İpuçları ===
public class PerformanceTipsService
{
    private readonly IDatabase _redis;

    public PerformanceTipsService(IConnectionMultiplexer mux)
        => _redis = mux.GetDatabase();

    // 1. FireAndForget — response bekleme (0 await, 0 Task allocation)
    public void TrackPageView(string page)
    {
        _redis.StringIncrement($"views:{page}", flags: CommandFlags.FireAndForget);
    }

    // 2. Batch — tek roundtrip'te çoklu komut
    public async Task WarmupCacheAsync(IEnumerable<(string key, string value)> items)
    {
        var batch = _redis.CreateBatch();
        var tasks = items.Select(i =>
            batch.StringSetAsync(i.key, i.value, TimeSpan.FromMinutes(30))).ToList();
        batch.Execute();
        await Task.WhenAll(tasks);
    }

    // 3. Pipeline-friendly: await'leri sona topla
    public async Task<(string? a, string? b, string? c)> GetMultipleAsync()
    {
        // 3 komut TEK TCP üzerinden pipeline'lanır — 1 roundtrip
        var taskA = _redis.StringGetAsync("key:a");
        var taskB = _redis.StringGetAsync("key:b");
        var taskC = _redis.StringGetAsync("key:c");
        return (await taskA, await taskB, await taskC);
    }

    // 4. GC-friendly: büyük value'ları ReadOnlyMemory<byte> ile oku
    // string dönüşümü heap allocation yapar, byte[] direkt kullanım daha iyi
    public async Task<byte[]?> GetRawAsync(string key)
    {
        RedisValue val = await _redis.StringGetAsync(key);
        return val.HasValue ? (byte[])val! : null;
    }

    // 5. Avoid: büyük value (>1MB) — network buffer + GC LOH
    // 6. Avoid: sık KEYS/SCAN — client-side index tut
    // 7. Prefer: Hash over JSON string (kısmi read/write = less bandwidth)
}

Performans Karşılaştırması

Yaklaşım Throughput Latency GC Pressure Handshake Ne Zaman
Tek Multiplexer ~200K ops/s <1ms p99 Sıfır (Singleton) 1× startup Çoğu uygulama
Pool (4 conn) ~600K ops/s <0.5ms p99 ~320KB Gen2 (4×80KB) 4× startup >50K ops/s, CPU-bound
GetDatabase(n≠default) Aynı +14 byte/cmd Sıfır Yok Multi-tenant namespace
FireAndForget ~1M+ ops/s 0 (beklemez) Sıfır (no Task) Analytics, counter
Batch (100 cmd) ~500K ops/s Toplu Task[] alloc Warmup, migration
Per-request Connect ~1K ops/s 3-110ms!! Gen2 churn HER REQUEST ASLA

Pool maliyeti:

  • Her ConnectionMultiplexer = 1 TCP + TLS handshake (startup'ta 1 kez).
  • Server-side: +1 file descriptor, ~10KB buffer, keepalive timer per connection.
  • Client-side: ConnectionMultiplexer ~80KB Gen2 object. Pool'un Dispose edilmemesi = memory leak.
  • Redis single-threaded — 8'den fazla connection client bottleneck'i çözer ama Redis CPU'yu hızlandırmaz.
  • 4-8 arası genellikle yeterli. Ölç, sonra artır.

Optimizasyon sırası (önce ucuz olanlar):

  1. FireAndForget — Task allocation yok, await yok
  2. Paralel Task fire + toplu await — 1 roundtrip'te N komut
  3. Hash kullan — kısmi read/write, daha az bandwidth
  4. Büyük value'ları parçala — GC LOH pressure azalt
  5. ReadOnlyMemory<byte> ile çalış — string allocation'dan kaçın
  6. Pool (son çare) — yukarıdakiler yetmezse multiplexer sayısını artır

Reconnection Events & Logging

Bağlantı koptuğunda ne olduğunu bilmek debugging için kritik. StackExchange.Redis event'leri structured logging ile yakalanmalı.

builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var logger = sp.GetRequiredService<ILogger<Program>>();
    var config = ConfigurationOptions.Parse(
        builder.Configuration.GetConnectionString("Redis")!);
    config.AbortOnConnectFail = false;

    var mux = ConnectionMultiplexer.Connect(config);

    mux.ConnectionFailed += (sender, e) =>
    {
        logger.LogError(e.Exception,
            "Redis connection failed: {EndPoint}, {FailureType}, {ConnectionType}",
            e.EndPoint, e.FailureType, e.ConnectionType);
    };

    mux.ConnectionRestored += (sender, e) =>
    {
        logger.LogInformation(
            "Redis connection restored: {EndPoint}, {ConnectionType}",
            e.EndPoint, e.ConnectionType);
    };

    mux.ErrorMessage += (sender, e) =>
    {
        logger.LogWarning("Redis server error: {EndPoint}, {Message}",
            e.EndPoint, e.Message);
    };

    mux.InternalError += (sender, e) =>
    {
        logger.LogError(e.Exception,
            "Redis internal error: {EndPoint}, {Origin}",
            e.EndPoint, e.Origin);
    };

    return mux;
});

Event handler'da blocking I/O yapma. Log yaz, metric artır, alert tetikle — ama DB sorgusu veya HTTP call yapma. Event thread StackExchange.Redis'in internal pipeline'ını bloklar.