Performans Optimizasyonu ve Ölçekleme

Uygulama performansını artırmak için kullanılan teknikler (caching, async/await, profiling, indexing), yatay/dikey ölçekleme stratejileri, kapasite planlama, load testing ve .NET'te performans optimizasyonu ipuçları ele alınır.

Performans Optimizasyonu ve Ölçekleme

Performans Optimizasyonu ve Ölçekleme: Uygulamalarınızı Hızlandırma ve Büyütme Sanatı

Bir uygulama, başlangıçta küçük bir kullanıcı kitlesi için yeterli performansa sahip olabilir. Ancak zamanla kullanıcı sayısı, veri hacmi ve işlem yükü arttıkça, performans sorunları kaçınılmaz hale gelir. Bu noktada devreye performans optimizasyonu ve ölçekleme (scaling) stratejileri girer. Bu yazıda, uygulama performansını artırmak için kullanılan teknikleri, yatay ve dikey ölçekleme stratejilerini, kapasite planlamasını, load testing'i ve .NET'te performans optimizasyonu ipuçlarını detaylıca ele alacağız.


1. Performans Optimizasyonu: Nereden Başlamalı?

Performans optimizasyonu, tahmin yürütmek değil, ölçmek ve veriye dayalı karar vermek ile başlar. "Bu kod daha hızlı olabilir" yerine, "Bu kod şu anda 500ms sürüyor, hedefim 100ms" demek çok daha anlamlıdır.

Adım 1: Profiling (Ölçümleme)
Uygulamanızda hangi bölümlerin yavaş çalıştığını tespit etmek için profiling yapmalısınız.

  • .NET Profiling Araçları:

    • Visual Studio Diagnostic Tools: Geliştirme ortamında kullanımı kolay, CPU ve bellek profili çıkarır.

    • JetBrains dotMemory / dotTrace: Bellek sızıntıları (memory leak) ve CPU darboğazlarını tespit etmek için gelişmiş araçlar.

    • PerfView: Microsoft'un ücretsiz, derinlemesine analiz aracı (özellikle GC ve JIT için).

    • Application Insights / Azure Monitor: Production ortamında performansı izlemek için.

csharp

// BenchmarkDotNet ile mikro-benchmark örneği
[MemoryDiagnoser]
public class StringConcatBenchmark
{
    [Params(10, 100, 1000)]
    public int N;

    [Benchmark(Baseline = true)]
    public string UsingPlusOperator()
    {
        string result = "";
        for (int i = 0; i < N; i++)
            result += i.ToString();
        return result;
    }

    [Benchmark]
    public string UsingStringBuilder()
    {
        var sb = new StringBuilder();
        for (int i = 0; i < N; i++)
            sb.Append(i);
        return sb.ToString();
    }
}

Adım 2: Darboğazları Tespit Etme

  • Veritabanı: Yavaş sorgular, eksik indeksler, çok fazla JOIN.

  • Ağ (Network): Yavaş API çağrıları, büyük veri transferleri.

  • CPU: Karmaşık hesaplamalar, gereksiz döngüler.

  • Bellek: GC baskısı, bellek sızıntıları, aşırı allocation.

  • I/O: Disk okuma/yazma, dosya işlemleri, log yazma.


2. Kodsal (Code-Level) Optimizasyon Teknikleri

A. Veri Yapıları ve Algoritmalar
Doğru veri yapısı ve algoritmayı seçmek, performans üzerinde büyük etkiye sahiptir.

csharp

// LinkedList vs List: Arama işlemleri için LinkedList O(n) vs List O(1)
var list = new List<int>(1000000);
var linkedList = new LinkedList<int>();

// Arama işlemi için HashSet kullan (O(1))
var hashSet = new HashSet<int>(list);
bool contains = hashSet.Contains(12345);

B. Async/Await Kullanımı
I/O işlemlerinde (veritabanı, HTTP, dosya) async/await kullanarak thread havuzunu bloklamayın. Ancak CPU-bound işlemler için async kullanmanın bir faydası yoktur (sadece overhead ekler).

csharp

// ✅ Doğru: I/O işlemi
public async Task<Product> GetProductAsync(int id)
{
    return await _dbContext.Products.FindAsync(id);
}

// ❌ Yanlış: CPU işlemi için async (gereksiz overhead)
public async Task<int> CalculateAsync(int x, int y)
{
    await Task.Delay(1); // Simüle edilmiş bir await - gereksiz!
    return x + y;
}

C. Caching (Önbellekleme)
Sık erişilen verileri önbelleğe alarak veritabanı veya harici servis çağrılarını azaltın.

csharp

// MemoryCache ile basit önbellekleme
public class ProductService
{
    private readonly IMemoryCache _cache;
    private readonly AppDbContext _db;

    public async Task<Product> GetProductAsync(int id)
    {
        string cacheKey = $"product_{id}";
        if (_cache.TryGetValue(cacheKey, out Product product))
            return product;

        product = await _db.Products.FindAsync(id);
        _cache.Set(cacheKey, product, TimeSpan.FromMinutes(10));
        return product;
    }
}

D. Lazy Loading vs Eager Loading (EF Core)
Veritabanı sorgularında Include() kullanarak (Eager Loading) N+1 probleminden kaçının.

csharp

// ❌ N+1 problem (Her order için ayrı sorgu)
var orders = await _db.Orders.ToListAsync();
foreach (var order in orders)
    Console.WriteLine(order.Customer.Name);

// ✅ Eager Loading (Tek sorgu)
var orders = await _db.Orders
    .Include(o => o.Customer)
    .ToListAsync();

E. Pooling (Havuzlama)
Nesne havuzları (Object Pooling) kullanarak, sık oluşturulan ve yok edilen nesnelerin maliyetini azaltın.

csharp

// .NET Core'daki ObjectPool
builder.Services.AddSingleton<ObjectPool<HttpClient>>(sp =>
{
    var policy = new DefaultObjectPoolPolicy<HttpClient>();
    return new DefaultObjectPool<HttpClient>(policy, maximumRetained: 10);
});

3. Veritabanı Seviyesinde Optimizasyon

A. Indexing (İndeksleme)
Sorguların sık kullanılan WHERE, JOIN, ORDER BY sütunlarına indeks oluşturun. Ancak, indeksler yazma işlemlerini (INSERT/UPDATE/DELETE) yavaşlatır.

sql

-- Örnek indeks
CREATE INDEX IX_Orders_CustomerId_OrderDate ON Orders (CustomerId, OrderDate DESC);

B. Sorgu Optimizasyonu

  • SELECT * yerine sadece ihtiyaç duyulan sütunları seçin.

  • N+1 probleminden kaçının (EF Core'da Include).

  • Karmaşık sorguları stored procedure veya view ile optimize edin.

C. Connection Pooling
Bağlantı havuzu (connection pool) kullanarak bağlantı açma/kapama maliyetini azaltın. Varsayılan olarak .NET'te zaten aktiftir.

D. Read-Only Sorgularda AsNoTracking() Kullanımı
EF Core'da, sadece okuma (SELECT) işlemlerinde AsNoTracking() kullanarak Change Tracker yükünü azaltın.

csharp

var products = await _context.Products
    .AsNoTracking()
    .ToListAsync();

4. Ölçekleme (Scaling) Stratejileri

Ölçekleme Türü Açıklama Avantajları Dezavantajları Örnek
Vertical Scaling (Dikey) Mevcut sunucunun donanım kaynaklarını artırmak (CPU, RAM, disk). Basit, uygulama değişikliği gerektirmez. Fiziksel sınır vardır (max CPU/RAM). Pahalıdır. 4GB RAM → 16GB RAM
Horizontal Scaling (Yatay) Birden fazla sunucu (instance) ekleyerek yükü dağıtmak. Esnek, sınırsız ölçeklenebilir. Uygulama state'siz (stateless) olmalıdır. Dağıtık sistem zorlukları ekler. 1 instance → 5 instance (Load Balancer ile)

Load Balancer (Yük Dengeleyici):
Yatay ölçeklemede, gelen istekleri birden fazla instance'a dağıtmak için load balancer (Azure Load Balancer, AWS ELB, NGINX, YARP) kullanılır.

Horizontal Scaling'de Dikkat Edilmesi Gerekenler:

  • Stateless (Durumsuz) Uygulama: Session state, cache gibi verileri harici bir depoda (Redis) tutun.

  • Veritabanı Ölçeklemesi: Veritabanı da darboğaz olabilir. Read replica, sharding veya caching ile destekleyin.

  • Dağıtık Cache: Redis gibi bir dağıtık cache kullanarak, farklı instance'lar arasında cache paylaşımı sağlayın.

YARP (Yet Another Reverse Proxy) ile Load Balancing:

csharp

// Program.cs
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

// appsettings.json - Round Robin load balancing
{
  "ReverseProxy": {
    "Routes": {
      "order-route": {
        "ClusterId": "order-cluster",
        "Match": { "Path": "/orders/{**catch-all}" }
      }
    },
    "Clusters": {
      "order-cluster": {
        "LoadBalancingPolicy": "RoundRobin",
        "Destinations": {
          "dest1": { "Address": "https://localhost:7001" },
          "dest2": { "Address": "https://localhost:7002" },
          "dest3": { "Address": "https://localhost:7003" }
        }
      }
    }
  }
}

5. Kapasite Planlama (Capacity Planning)

Kapasite planlaması, mevcut ve gelecekteki yükleri tahmin ederek kaynak ihtiyacını belirleme sürecidir.

Adımlar:

  1. Mevcut Yükü Ölçün: İstek sayısı, CPU kullanımı, bellek kullanımı, veritabanı sorgu süreleri, ortalama yanıt süresi (response time).

  2. Büyüme Tahmini: Kullanıcı sayısı, veri hacmi ve işlem yoğunluğunun ne kadar artacağını tahmin edin.

  3. Darboğazları Belirleyin: Hangi bileşenin (CPU, bellek, I/O, ağ) sınırlayıcı olduğunu tespit edin.

  4. Kaynak İhtiyacını Hesaplayın: Mevcut yük + büyüme tahmini = gelecekteki kaynak ihtiyacı.

  5. Stres Testi (Load Testing): Beklenen yük altında sistemin nasıl davrandığını test edin.


6. Load Testing (Yük Testi) ve Stres Testi

  • Load Testing (Yük Testi): Sistemi, beklenen normal yük altında test etmek.

  • Stress Testing (Stres Testi): Sistemi, sınırların ötesine zorlayarak çökme noktasını bulmak.

  • Soak Testing (Dayanıklılık Testi): Sistemi uzun süre (saatler/günler) orta düzeyde yük altında test ederek bellek sızıntıları (memory leak) gibi sorunları tespit etmek.

Popüler Yük Testi Araçları:

  • k6: Açık kaynak, JavaScript tabanlı, modern.

  • JMeter: Java tabanlı, eski ama güçlü.

  • Azure Load Testing: Bulut tabanlı, Azure entegrasyonu.

  • Vegeta: Hafif, CLI tabanlı.

k6 ile Basit Yük Testi:

javascript

import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
    stages: [
        { duration: '30s', target: 20 }, // 20 kullanıcıya rampa
        { duration: '1m', target: 20 },  // 1 dakika 20 kullanıcı
        { duration: '30s', target: 0 },  // 0'a düş
    ],
};

export default function () {
    let res = http.get('https://test-api.example.com/products');
    check(res, { 'status was 200': (r) => r.status == 200 });
    sleep(1);
}

7. .NET Performans Optimizasyonu İpuçları

  1. String Concatenation: Döngü içinde + operatörü kullanmayın; StringBuilder kullanın.

  2. LINQ Kullanımı: Sorguları optimize edin. Any() yerine Count() > 0 kullanmayın (Any daha hızlıdır).

  3. JSON Serialization: System.Text.Json kullanın (Newtonsoft'dan daha hızlı). JsonSerializerContext ile Source Generator kullanarak AOT ve daha iyi performans elde edin.

  4. GC (Garbage Collector): GC.Collect()'i asla manuel çağırmayın. ArrayPool<T> veya ObjectPool kullanarak allocation'ı azaltın.

  5. ThreadPool: Task.Run veya new Thread kullanmak yerine, Task ve async/await kullanarak thread havuzunu verimli yönetin.

  6. Dependency Injection (DI): Scoped/Transient/Singleton lifetime'ları doğru seçin. Singleton servislerde stateless olmasına dikkat edin.

  7. Önbellekleme: IMemoryCache ve IDistributedCache (Redis) kullanın.

  8. Compression: Yanıtları GZIP ile sıkıştırarak ağ trafiğini azaltın.

  9. CDN: Statik dosyaları (image, CSS, JS) CDN üzerinden servis edin.

  10. Logging: Log seviyesini Production'da Warning veya Error olarak ayarlayın. Gereksiz loglama performansı düşürür.


8. Performans Metrikleri (Neyi Ölçmeli?)

Metrik Açıklama Hedef
Response Time (Yanıt Süresi) İstek gönderildikten sonra yanıt alınana kadar geçen süre. < 200ms (API), < 2s (sayfa yüklenme)
Throughput (Verim) Saniyedeki istek sayısı (RPS - Requests Per Second). Yüksek (ör. 1000+ RPS)
Error Rate (Hata Oranı) Başarısız isteklerin yüzdesi. < 1%
CPU Usage (CPU Kullanımı) Sunucu CPU kullanımı. < 70% (ortalamada)
Memory Usage (Bellek Kullanımı) Uygulamanın bellek tüketimi. Sürekli artmamalı (memory leak yok).
GC (Garbage Collection) GC sayısı ve süresi. Gen 0/1 koleksiyonları az olmalı.
Database Query Time Veritabanı sorgu süreleri. < 50ms
Thread Pool Starvation Kullanılabilir thread sayısı. Yeterli boş thread olmalı.

Sonuç:

Performans optimizasyonu, bir uygulamanın yaşam döngüsü boyunca sürekli yapılması gereken bir faaliyettir. Profiling, doğru kodlama teknikleri, önbellekleme, veritabanı optimizasyonu ve ölçekleme stratejileri ile uygulamanızı hızlı, ölçeklenebilir ve dayanıklı hale getirebilirsiniz.

Unutmayın:

  • Önce ölç, sonra optimize et.

  • Optimizasyon, okunabilirlikten daha önemli değildir.

  • Performans bir özelliktir (feature), sürekli test edilmeli ve izlenmelidir.

Tüm yazılar

İlgili Yazılar