Stress Test ile Sistemin Sınır Kapasitesini Belirleme

Sistemin gerçekten ne kadar yük kaldırabileceğini öğrenmek istiyorsanız, bunu production’da öğrenmemek lazım. Bu cümleyi yıllar içinde kendi tecrübelerimden çıkarttım. Bir e-ticaret müşterisinde Black Friday öncesi “sistem dayanır” diye düşünürken sunucunun 3000 concurrent user’da çöktüğünü gördüm. O günden beri her kritik sistem için stress test, deployment pipeline’ının vazgeçilmez bir parçası haline geldi.

Stress test, yük testinin bir adım ötesidir. Yük testi “normal yük altında sistem nasıl davranıyor?” sorusunu cevaplarken, stress test “sistem nerede kırılır?” sorusunu sorar. İkisi arasındaki fark hem metodolojik hem de zihinsel bir fark. Stress test yaparken amacınız sistemi başarılı tutmak değil, sınır noktasını bulmak.

Stress Test ile Yük Testi Farkını Anlamak

Çoğu ekip bu iki kavramı birbirine karıştırıyor. Yük testi, önceden belirlediğiniz bir kullanıcı sayısıyla sisteminizin SLA’larını karşılayıp karşılamadığını doğrular. Örneğin “500 concurrent user’da response time 200ms altında kalıyor mu?” sorusu bir yük testi sorusudur.

Stress test ise kapasiteyi keşfetmek için tasarlanır:

  • Sistem kaç kullanıcıda hata vermeye başlıyor?
  • Memory leak var mı, yoksa sıkıştırma altında sistem stabil kalıyor mu?
  • Yük azaldığında sistem eski haline dönebiliyor mu (recovery)?
  • Bottleneck nerede: CPU mu, RAM mi, veritabanı mı, network mi?

Bu soruları cevaplamak için farklı araçlar farklı güçlere sahip. Ben bugün k6, Locust ve JMeter üçlüsüne bakacağım, ama sadece kurulum anlatmayacağım. Gerçek senaryolarda nasıl kullandığımı paylaşacağım.

k6 ile Stress Test Senaryoları

k6, Grafana Labs tarafından geliştirilen Go tabanlı bir yük test aracı. JavaScript ile senaryo yazılıyor ve kaynak tüketimi açısından son derece verimli. Aynı donanımda JMeter’dan çok daha fazla sanal kullanıcı oluşturabiliyor.

Bir stress test senaryosunun en önemli kısmı executor ve ramp-up stratejisi. Aşağıdaki örnek, yükü kademeli artırıp sistemin kırıldığı noktayı bulmak için tasarlanmış:

// stress-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const errorRate = new Rate('errors');

export const options = {
  stages: [
    { duration: '2m', target: 100 },   // Normal yük - referans nokta
    { duration: '3m', target: 500 },   // Orta yük
    { duration: '3m', target: 1000 },  // Yüksek yük
    { duration: '3m', target: 2000 },  // Stres bölgesi
    { duration: '3m', target: 3000 },  // Kritik bölge
    { duration: '2m', target: 0 },     // Recovery testi
  ],
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1500'],
    http_req_failed: ['rate<0.05'],
    errors: ['rate<0.1'],
  },
};

export default function () {
  const BASE_URL = __ENV.TARGET_URL || 'http://localhost:3000';

  const responses = http.batch([
    ['GET', `${BASE_URL}/api/products`],
    ['GET', `${BASE_URL}/api/categories`],
  ]);

  responses.forEach((res) => {
    const success = check(res, {
      'status is 200': (r) => r.status === 200,
      'response time OK': (r) => r.timings.duration < 1000,
    });
    errorRate.add(!success);
  });

  sleep(Math.random() * 2 + 1); // 1-3 saniye arası random bekleme
}

Bu senaryoyu çalıştırmak için:

# Temel çalıştırma
k6 run stress-test.js

# Environment variable ile hedef URL belirleme
k6 run -e TARGET_URL=https://staging.example.com stress-test.js

# InfluxDB'ye metric göndererek Grafana'da izleme
k6 run --out influxdb=http://localhost:8086/k6 stress-test.js

# HTML rapor üretme (k6-reporter kullanarak)
k6 run --out json=results.json stress-test.js

k6’nın güzel yanı threshold mekanizması. Stres test boyunca p95 response time’ınız 500ms’i aşarsa test “fail” olarak işaretleniyor. Bu CI/CD pipeline’ına entegre etmek için mükemmel.

k6 ile Spike Test

Spike test, stress testin özel bir versiyonu. Ani ve sert trafik artışlarını simüle eder. Bir kampanya maili gönderdiniz ve 10 bin kişi aynı anda siteye girdi gibi durumlar için:

// spike-test.js
import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 50 },    // Normal trafik
    { duration: '30s', target: 5000 }, // SPIKE - aniden 5000 kullanıcı
    { duration: '1m', target: 5000 },  // Spike'ı koru
    { duration: '30s', target: 50 },   // Ani düşüş
    { duration: '2m', target: 50 },    // Recovery izleme
    { duration: '30s', target: 0 },
  ],
};

export default function () {
  http.get(__ENV.TARGET_URL || 'http://localhost:3000');
  sleep(1);
}

Spike test sonrası sistemin recovery süresini mutlaka ölçün. Bazı sistemler spike altında çökmez ama spike sonrasında connection pool dolduğu için dakikalarca hata vermeye devam eder.

Locust ile Python Tabanlı Stress Test

Locust’u tercih ettiğim durumlar var: karmaşık kullanıcı davranışları modellemek, dinamik veri üretmek veya ekibin Python biliyor olması. k6’ya göre kaynak tüketimi daha yüksek ama esneklik açısından üstün.

# locustfile.py
from locust import HttpUser, task, between, events
from locust.runners import MasterRunner
import random
import json

class ECommerceUser(HttpUser):
    wait_time = between(1, 3)
    
    def on_start(self):
        """Kullanıcı oturumu başlatma"""
        self.client.post("/api/auth/login", json={
            "email": f"user{random.randint(1, 10000)}@test.com",
            "password": "testpassword123"
        })
    
    @task(5)
    def browse_products(self):
        """En sık yapılan işlem: ürün listeleme"""
        page = random.randint(1, 50)
        with self.client.get(
            f"/api/products?page={page}&limit=20",
            catch_response=True,
            name="/api/products"
        ) as response:
            if response.status_code == 200:
                data = response.json()
                if len(data.get('items', [])) == 0:
                    response.failure("Empty product list returned")
            else:
                response.failure(f"Unexpected status: {response.status_code}")
    
    @task(2)
    def view_product_detail(self):
        """Ürün detay sayfası"""
        product_id = random.randint(1, 1000)
        self.client.get(f"/api/products/{product_id}", name="/api/products/[id]")
    
    @task(1)
    def add_to_cart(self):
        """Sepete ekleme - veritabanı yazma işlemi"""
        self.client.post("/api/cart/add", json={
            "product_id": random.randint(1, 1000),
            "quantity": random.randint(1, 5)
        }, name="/api/cart/add")

Locust’u headless modda (CI/CD için) çalıştırmak:

# Web UI olmadan, 1000 kullanıcıya 10 kullanıcı/sn ramp-up ile
locust -f locustfile.py 
  --headless 
  --users 1000 
  --spawn-rate 10 
  --run-time 10m 
  --host https://staging.example.com 
  --html report.html 
  --csv results

# Distributed test için master node
locust -f locustfile.py --master --users 5000 --spawn-rate 50

# Worker node (her worker ayrı makinede)
locust -f locustfile.py --worker --master-host=192.168.1.100

Locust’un distributed test özelliği önemli. Tek bir makineden 5000+ gerçekçi kullanıcı simüle etmek zor. Master-worker mimarisiyle yükü birden fazla makineye dağıtabilirsiniz.

JMeter ile Kurumsal Stress Test

JMeter eski ama güçlü. Özellikle SOAP servisleri, karmaşık authentication akışları veya legacy sistemler için hâlâ vazgeçilmez. GUI’si var ama production stress testleri mutlaka CLI’dan çalıştırın.

# JMeter CLI ile stress test çalıştırma
jmeter -n 
  -t stress-test-plan.jmx 
  -l results.jtl 
  -e 
  -o ./report 
  -Jusers=2000 
  -Jrampup=300 
  -Jduration=600 
  -Jtarget_host=staging.example.com

# Sonuçları özetleme
jmeter -g results.jtl -o ./html-report

JMeter’da test planını XML olarak da düzenleyebilirsiniz, ama bunun yerine JMeter’ın Groovy script desteğini kullanmayı tercih ediyorum:

// JMeter JSR223 Sampler - Groovy script
// Dinamik threshold kontrolü
import org.apache.jmeter.services.FileServer

def responseTime = prev.getTime()
def sampleLabel = prev.getSampleLabel()

// 2000ms üzerindeki istekleri loglama
if (responseTime > 2000) {
    log.warn("SLOW REQUEST: ${sampleLabel} took ${responseTime}ms")
    
    // Custom metric dosyasına yazma
    def writer = new FileWriter("/tmp/slow_requests.log", true)
    writer.write("${new Date()},${sampleLabel},${responseTime}n")
    writer.close()
}

// Response body kontrolü
def responseData = prev.getResponseDataAsString()
if (responseData.contains("error") || responseData.contains("exception")) {
    prev.setSuccessful(false)
    prev.setResponseMessage("Error detected in response body")
}

Sistemin Gerçek Kapasitesini Bulmak

Stress test sonuçlarını nasıl yorumlayacağınızı bilmezseniz, rakamlar size bir şey ifade etmez. Şu metriklere odaklanın:

Throughput (RPS – Request Per Second): Sistem saniyede kaç istek işliyor? Bu sayı düşmeye başladığı nokta ilk alarm noktasıdır.

Response Time Dağılımı: Ortalama değil, p95 ve p99’a bakın. Ortalama 100ms olsa bile p99 10 saniyeyse, kullanıcılarınızın yüzde biri çok kötü deneyim yaşıyor demektir.

Error Rate: Hata oranı yüzde 1’i geçmeye başladığı nokta kritik eşik. Bazı sistemlerde yüzde 0.1 bile kabul edilemez.

Recovery Süresi: Yük kaldırıldıktan sonra sistem ne kadar sürede normale dönüyor? 30 saniye mi, 5 dakika mı?

Gerçek bir stress test sırasında sunucu tarafını da izlemeniz şart. Tek başına k6 output’una bakmak yetersiz:

# Stress test sırasında sistem kaynaklarını izleme
# vmstat ile CPU ve memory
vmstat 2 >> /tmp/vmstat_during_test.log &

# iostat ile disk I/O
iostat -x 2 >> /tmp/iostat_during_test.log &

# netstat ile network connection durumu
watch -n 2 'netstat -an | grep ESTABLISHED | wc -l'

# PostgreSQL için aktif connection sayısı
watch -n 2 'psql -c "SELECT count(*) FROM pg_stat_activity WHERE state = '"'"'active'"'"';"'

# Test bitince logları durdur
kill %1 %2

Bottleneck Analizi: Kırılma Noktasını Anlamak

Sistem stres altında bozulduğunda “neden bozuldu?” sorusu en önemli sorudur. Ben bu analizi üç katmanda yapıyorum:

Uygulama Katmanı: APM aracı kullanıyorsanız (Datadog, New Relic, Elastic APM) slow transaction’ları görürsünüz. Kullanmıyorsanız uygulama loglarını analiz edin.

# Nginx access log'unda 5xx hataları ve response time analizi
awk '$9 >= 500 {print $7, $9, $NF}' /var/log/nginx/access.log | 
  sort | uniq -c | sort -rn | head -20

# Belirli bir zaman dilimindeki hata oranı
awk -v start="10:00:00" -v end="10:30:00" 
  '$4 > "[01/Jan/2024:" start && $4 < "[01/Jan/2024:" end && $9 >= 500' 
  /var/log/nginx/access.log | wc -l

Veritabanı Katmanı: Çoğu zaman bottleneck uygulamada değil, veritabanında. Stres altında query planları değişebilir, index’ler yetersiz kalabilir.

# PostgreSQL slow query log aktifleştirme
# postgresql.conf içinde:
# log_min_duration_statement = 100  # 100ms üzeri sorguları logla
# log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '

# Çalışan sorguları anlık izleme
psql -c "
SELECT pid, now() - pg_stat_activity.query_start AS duration,
       query, state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '1 seconds'
ORDER BY duration DESC;
"

# Index kullanılmayan ve yavaş sorguları bulma
psql -c "
SELECT schemaname, tablename, attname, n_distinct, correlation
FROM pg_stats
WHERE tablename = 'orders'
ORDER BY n_distinct;
"

Sistem Katmanı: CPU, RAM, network, disk. Bu sırayla değil, aynı anda bakın.

# Kapsamlı sistem durumu snapshot'ı
echo "=== CPU ===" && mpstat 1 5
echo "=== Memory ===" && free -h
echo "=== Disk I/O ===" && iostat -x 1 5
echo "=== Network ===" && ss -s
echo "=== Open Files ===" && cat /proc/sys/fs/file-nr
echo "=== TCP Connections ===" && ss -tan | awk '{print $1}' | sort | uniq -c

Otomatik Stress Test Pipeline’ı

Manuel stress test yapmak değerlidir ama her deploy’dan önce temel bir stress test otomatik çalışmalı. İşte bir CI/CD entegrasyonu örneği:

#!/bin/bash
# run-stress-test.sh
# Jenkins veya GitHub Actions'tan çağrılır

TARGET_URL=$1
ENVIRONMENT=$2
THRESHOLD_ERROR_RATE=0.05
THRESHOLD_P95=500

echo "Starting stress test for ${TARGET_URL} (${ENVIRONMENT})"

# k6 ile stress test çalıştır
k6 run 
  --out json=results_${ENVIRONMENT}_$(date +%Y%m%d_%H%M%S).json 
  -e TARGET_URL=${TARGET_URL} 
  -e ENVIRONMENT=${ENVIRONMENT} 
  --summary-trend-stats="avg,min,med,max,p(90),p(95),p(99)" 
  stress-test.js

EXIT_CODE=$?

# k6 exit code kontrolü
if [ $EXIT_CODE -ne 0 ]; then
  echo "STRESS TEST FAILED: Thresholds exceeded"
  # Slack notification gönder
  curl -s -X POST $SLACK_WEBHOOK 
    -H 'Content-type: application/json' 
    --data "{
      "text": ":red_circle: Stress Test FAILED on ${ENVIRONMENT}",
      "attachments": [{
        "color": "danger",
        "text": "Target: ${TARGET_URL}nCheck the test results for details."
      }]
    }"
  exit 1
fi

echo "Stress test passed successfully"
exit 0

Sık Yapılan Hatalar

Yıllarca stress test yaparak gördüğüm en yaygın hatalar:

  • Production veritabanı ile test yapmak: Stress test verileri temizlemek zordur. Mutlaka izole bir test ortamı kullanın.
  • Test aracını test edilen sunucuda çalıştırmak: k6 veya Locust sisteminizin CPU ve network’ünü kullanır. Ayrı bir makine zorunlu.
  • Sadece HTTP 200 kontrolü yapmak: Response body kontrolü yapmadan garbage dönen bir API’yi başarılı sayabilirsiniz.
  • Tek bir endpoint’i test etmek: Gerçek kullanıcı davranışını modelleyin. Sadece ana sayfayı basmak gerçekçi değil.
  • Sonuçları arşivlememek: Her stress test sonucunu tarih ve versiyon bilgisiyle saklayın. Regresyon tespiti için şart.
  • Think time eklemeksizin test yapmak: Gerçek kullanıcılar sayfalar arasında bekleme yapar. Sıfır think time yapay sonuç üretir.

Sonuç

Stress test, bir sistemin gerçek kapasitesini ortaya koyan en dürüst yöntem. Söylediklerinizi değil, sistemin gerçekte ne yaptığını gösterir. k6 hız ve kaynak verimliliği için, Locust karmaşık senaryolar ve Python ekosistemi için, JMeter kurumsal ve legacy sistemler için güçlü tercihler.

Ama aracın seçiminden daha önemli olan şey test stratejisi ve sonuçların yorumlanması. Sistemin nerede kırıldığını bulmak tek başına yeterli değil; neden kırıldığını, recovery süresini ve bottleneck noktasını anlamak asıl hedef olmalı.

Production’da sürpriz yaşamamak için staging’de sistematik stress test yapın. Kapasiteyi bilerek ilerlemek, kapasiteyi tahmin ederek ilerlemekten her zaman daha güvenli.

Bir yanıt yazın

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