İ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.
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.
# 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):
FireAndForget— Task allocation yok, await yok- Paralel Task fire + toplu await — 1 roundtrip'te N komut
Hashkullan — kısmi read/write, daha az bandwidth- Büyük value'ları parçala — GC LOH pressure azalt
ReadOnlyMemory<byte>ile çalış — string allocation'dan kaçın- 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.